From 4f93f805c7c825ad06a763083d4c5df76fa5c570 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 11:30:44 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E5=8D=87=E7=BA=A7=EF=BC=9Ago-zero=20v1.3?= =?UTF-8?q?.0=E2=86=92v1.10.3=20=E5=85=A8=E6=A0=88=E7=8E=B0=E4=BB=A3?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Go 1.17→1.26,grpc 1.84 / protobuf 1.36,jwt 迁 golang-jwt/v4(修 CVE-2020-26160) - 七牛 SDK 迁 go-sdk/v7(旧仓库已归档);olivere/elastic v7.0.32 - redis.NewRedis 新签名修复(3 处);合并 user/votes/common 三组重复 main - 参数解析适配新版 tag 规则:GET 类型补 form tag(.api 与 types.go 同步) - 全部 .api 补 syntax="v1" 并适配新版 goctl 语法 - server.sh 镜像升级:MySQL 8.4 / Redis 8 / etcd 3.5.21 / 官方 ES 7.17.28 - sql.sql 全库统一 utf8mb4,修复 3 处导入报错的存量语法问题 - 修复存量 bug:按次数计票 SQL 参数错位(Sprintf 参数不匹配)、ES 日志占位符、占位 URL 测试 - 新增 docs/UPGRADE.md(升级明细与验证记录)、docs/ROADMAP.md(数据中台演进路线图) --- .gitignore | 7 +- README.md | 42 +- common/rpc/common.go | 37 - common/rpc/common/common.pb.go | 4 +- common/rpc/etc/rpc.example.yaml | 2 +- datacenter.api | 50 +- docs/ROADMAP.md | 147 +++ docs/UPGRADE.md | 93 ++ go.mod | 149 +-- go.sum | 905 +++++------------- internal/logic/common/qiuniutokenlogic.go | 4 +- internal/logic/datacenterlogic.go | 2 +- internal/svc/servicecontext.go | 2 +- internal/types/types.go | 20 +- questions.api | 9 +- questions/model/appquestionsawardsmodel.go | 2 +- .../logic/postquestionschangelogic.go | 2 +- .../rpc/internal/logic/postturntablelogic.go | 2 +- .../rpc/internal/server/questionsserver.go | 2 +- questions/rpc/internal/svc/servicecontext.go | 2 +- questions/rpc/questions/questions.pb.go | 12 +- questions/rpc/questionsclient/questions.go | 2 +- search.api | 13 +- search/rpc/dao/article.go | 4 +- search/rpc/internal/logic/articleinitlogic.go | 6 +- server.sh | 32 +- shared/tools.go | 4 +- shared/tools_test.go | 33 +- sql.sql | 66 +- user.api | 25 +- user/api/internal/logic/loginlogic.go | 2 +- user/api/user.api | 9 +- user/rpc/rpc.go | 2 +- user/rpc/user.go | 37 - user/rpc/user/user.pb.go | 14 +- votes.api | 17 +- votes/model/appenrollmodel.go | 2 +- votes/model/appvotesactivitymodel.go | 4 +- votes/model/appvotesmodel.go | 2 +- votes/rpc/internal/svc/servicecontext.go | 2 +- votes/rpc/votes.go | 37 - votes/rpc/votes/votes.pb.go | 12 +- 42 files changed, 786 insertions(+), 1034 deletions(-) delete mode 100644 common/rpc/common.go create mode 100644 docs/ROADMAP.md create mode 100644 docs/UPGRADE.md delete mode 100644 user/rpc/user.go delete mode 100644 votes/rpc/votes.go diff --git a/.gitignore b/.gitignore index ee0ed2e..7aca9ee 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,9 @@ user/api/etc/user-api.yaml mysql8 redis etcd -elasticsearch \ No newline at end of file +elasticsearch +user/rpc/etc/rpc.yaml +common/rpc/etc/rpc.yaml +votes/rpc/etc/rpc.yaml +search/rpc/etc/search.yaml +questions/rpc/etc/questions.yaml diff --git a/README.md b/README.md index d662fac..ec6b5d3 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,14 @@ syslog查询、syslog实时查看、登录日志、审计日志 # 基于go-zero 框架写的一个数据中台中心 +> **2026-10 全栈升级**:Go 1.26 + go-zero v1.10.3 + MySQL 8.4 + Redis 8 + etcd 3.5 + ES 7.17,升级明细见 [docs/UPGRADE.md](docs/UPGRADE.md)。 +> 项目正在从"活动业务中台"向真正的数据中台演进,路线图见 [docs/ROADMAP.md](docs/ROADMAP.md)。 + +### 环境要求 +- Go **1.24+**(go.mod 声明 1.26) +- goctl v1.10.2(`go install github.com/zeromicro/go-zero/tools/goctl@latest`,仅重新生成代码时需要) +- protoc 36.x + protoc-gen-go + protoc-gen-go-grpc(仅重新生成 RPC 时需要) + ### 详细介绍 [我是如何用go-zero实现一个中台系统的](https://www.cnblogs.com/jackluo/p/14148518.html) ## 架构图 @@ -109,39 +117,9 @@ es es 04b37b58f10411fa8ab5894c917266cc7bc7f9a96988908fff9b3734e6259ad4 ``` -### 需要注意的是 elasticsearch 第一次用的时候,需要初始化密码 执行下面的操作 -```bash -➜ ~ docker exec -it es /bin/bash -[root@04b37b58f104 elasticsearch]# sh /usr/share/elasticsearch/bin/elasticsearch-setup-passwords auto -Initiating the setup of passwords for reserved users elastic,apm_system,kibana,logstash_system,beats_system,remote_monitoring_user. -The passwords will be randomly generated and printed to the console. -Please confirm that you would like to continue [y/N]y - - -Changed password for user apm_system -PASSWORD apm_system = iKVpVyFFTC8qEXvJILi2 - -Changed password for user kibana -PASSWORD kibana = DnDgQRRgkuyV8YqTrxbk - -Changed password for user logstash_system -PASSWORD logstash_system = aqjVEdMsG7P2CXm9sQNk +### elasticsearch 的帐号密码已经通过 server.sh 内置(`ELASTIC_PASSWORD=admin`) +### 即帐号: elastic ,密码: admin,将其填入 search/rpc/etc/search.yaml 文件中即可 -Changed password for user beats_system -PASSWORD beats_system = Oleo1gQhli6tGaWuHz96 - -Changed password for user remote_monitoring_user -PASSWORD remote_monitoring_user = rX9CsBLM2c3ow9sH6Iud - -Changed password for user elastic -PASSWORD elastic = yi4cxxdiz86pRKOoTAcm -``` -### 看到上面最后行 -``` -Changed password for user elastic -PASSWORD elastic = yi4cxxdiz86pRKOoTAcm -``` -### 得到elastic 的帐号: elastic ,密码: yi4cxxdiz86pRKOoTAcm 将这个填入search/rpc/search.yaml 文件中 接着导入 sql.sql到 mysql数据中 ,如果有工具自行导入,下面仅参考 diff --git a/common/rpc/common.go b/common/rpc/common.go deleted file mode 100644 index 5aa1ec4..0000000 --- a/common/rpc/common.go +++ /dev/null @@ -1,37 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! -// Source: common.proto - -package main - -import ( - "flag" - "fmt" - - "datacenter/common/rpc/common" - "datacenter/common/rpc/internal/config" - "datacenter/common/rpc/internal/server" - "datacenter/common/rpc/internal/svc" - - "github.com/zeromicro/go-zero/core/conf" - "github.com/zeromicro/go-zero/zrpc" - "google.golang.org/grpc" -) - -var configFile = flag.String("f", "etc/common.yaml", "the config file") - -func main() { - flag.Parse() - - var c config.Config - conf.MustLoad(*configFile, &c) - ctx := svc.NewServiceContext(c) - srv := server.NewCommonServer(ctx) - - s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { - common.RegisterCommonServer(grpcServer, srv) - }) - defer s.Stop() - - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) - s.Start() -} diff --git a/common/rpc/common/common.pb.go b/common/rpc/common/common.pb.go index f254714..cc9d5e3 100644 --- a/common/rpc/common/common.pb.go +++ b/common/rpc/common/common.pb.go @@ -155,7 +155,7 @@ func (x *BaseAppResp) GetFullwebsite() string { return "" } -//请求的api +// 请求的api type AppConfigReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -211,7 +211,7 @@ func (x *AppConfigReq) GetPtyid() int64 { return 0 } -//返回的值 +// 返回的值 type AppConfigResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache diff --git a/common/rpc/etc/rpc.example.yaml b/common/rpc/etc/rpc.example.yaml index 2e898d7..c0da653 100644 --- a/common/rpc/etc/rpc.example.yaml +++ b/common/rpc/etc/rpc.example.yaml @@ -1,7 +1,7 @@ Name: common.rpc ListenOn: 127.0.0.1:8081 Mysql: - DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8&parseTime=true&loc=Asia%2FShanghai + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai CacheRedis: - Host: 127.0.0.1:6379 Pass: admin diff --git a/datacenter.api b/datacenter.api index 79b6979..e955502 100644 --- a/datacenter.api +++ b/datacenter.api @@ -1,8 +1,10 @@ -info( - title: "中台系统"// TODO: add title - desc: "中台系统"// TODO: add description +syntax = "v1" + +info ( + title: "中台系统" // TODO: add title + desc: "中台系统" // TODO: add description author: "jackluo" - email: "net.webjoy@gmail.com" + email: "net.webjoy@gmail.com" ) import "user.api" @@ -12,30 +14,35 @@ import "questions.api" //获取 应用信息 type Beid { - Beid int64 `json:"beid"` + Beid int64 `json:"beid,optional" form:"beid,optional"` } + type Token { Token string `json:"token"` } + type WxTicket { Ticket string `json:"ticket"` } + type Application { - Sname string `json:"Sname"` //名称 - Logo string `json:"logo"` // login - Isclose int64 `json:"isclose"` //是否关闭 + Sname string `json:"Sname"` //名称 + Logo string `json:"logo"` // login + Isclose int64 `json:"isclose"` //是否关闭 Fullwebsite string `json:"fullwebsite"` // 全站名称 } + type SnsReq { Beid - Ptyid int64 `json:"ptyid"` //对应平台 + Ptyid int64 `json:"ptyid"` //对应平台 BackUrl string `json:"back_url"` //登陆返回的地址 } + type SnsResp { Beid - Ptyid int64 `json:"ptyid"` //对应平台 - Appid string `json:"appid"` //sns 平台的id - Title string `json:"title"` //名称 + Ptyid int64 `json:"ptyid"` //对应平台 + Appid string `json:"appid"` //sns 平台的id + Title string `json:"title"` //名称 LoginUrl string `json:"login_url"` //微信登陆的地址 } @@ -46,36 +53,37 @@ type WxShareResp { Signature string `json:"signature"` } -@server( +@server ( group: common ) service datacenter-api { - @doc( + @doc ( summary: "获取站点的信息" ) @handler appInfo get /common/appinfo (Beid) returns (Application) - @doc( + + @doc ( summary: "获取站点的社交属性信息" ) @handler snsInfo post /common/snsinfo (SnsReq) returns (SnsResp) - + //获取分享的 @handler wxTicket post /common/wx/ticket (SnsReq) returns (WxShareResp) - } //上传需要登陆 -@server( - jwt: Auth +@server ( + jwt: Auth group: common ) service datacenter-api { - @doc( + @doc ( summary: "七牛上传凭证" ) @handler qiuniuToken post /common/qiuniu/token (Beid) returns (Token) -} \ No newline at end of file +} + diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..688ea91 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,147 @@ +# 数据中台演进路线图(ROADMAP) + +> 结论先行:**真正的数据中台 = 全域接入 + 统一治理 + 资产沉淀 + 服务化出口 + 资产门户** 的闭环,方法论祖师是阿里 OneData(OneModel/OneID/OneService)。 +> 本项目现状是"活动业务中台"(管业务流程:登录/投票/抽奖),与数据中台(管数据本身:元数据/口径/质量/服务)的差距在**领域**而不在代码量。 +> 调研基线:2026-10;完整调研报告见本文末尾来源。 + +## 一、中台是什么(方法论速览) + +**OneData 三件套**(阿里): + +| 组件 | 解决什么 | 对应商业版模块 | +|---|---|---| +| **OneModel** 统一建模 | 指标口径混乱、重复建模 | 数据标准 / 数仓管理 / 数据指标 | +| **OneID** 实体打通 | 同一用户多 ID 孤岛 | 数据标签 / 主数据 | +| **OneService** 统一服务 | 取数靠人肉、接口重复开发 | 数据服务 | + +概念层次:`存储(数仓/湖/湖仓一体) ← 流程(编排) ← 理念(Data Fabric) ← 价值(数据中台)`。**中台 ≠ 数仓**:数仓交付报表,中台交付可复用的数据服务与资产。2025-2026 共识:湖仓一体(Iceberg 为事实标准)做底,元数据先行,以服务 API 和 AI 问数为价值出口。 + +**三云官方能力框架的交集**(阿里 DataWorks / 华为 DataArts / 腾讯 WeData)= 8 大能力域: +`集成 / 开发 / 治理(元数据·标准·质量·安全) / 资产(目录·地图) / 指标 / 服务(API) / 可视化 / 运维` + 统一门户。这就是"真正的中台"检查表。 + +## 二、运行主链路 + +```mermaid +flowchart LR + SRC[源系统: 业务库/MQ/API] --> INT[数据集成 SeaTunnel/Flink CDC/DataX] + SCH[调度 DolphinScheduler] -.编排.-> INT + INT --> ODS[ODS] --> DWD[DWD] --> DWS[DWS] --> ADS[ADS 宽表] + DWD --> METRIC[指标] + ADS --> TAG[标签/画像] + METRIC --> CAT[资产目录/地图] + TAG --> CAT + ADS --> DSVC[数据服务 API] --> GATE[网关: 鉴权/限流/审计] --> APP[业务应用] + GATE --> BI[BI/报表] + GATE --> AIQ[AI 问数] + META[元数据/血缘] -.横切.-> INT & DWD & DSVC + DQ[数据质量] -.入仓/出仓卡点.-> ODS & DWS + SEC[安全: 分级分类/脱敏] -.出口脱敏.-> DSVC +``` + +要点:纵向是"接入→分层加工→资产沉淀→服务出口"的生产链;横向治理(元数据/质量/标准/安全)横切每一跳;消费侧三类客户——人(BI/门户)、应用(API)、AI(问数)吃同一份资产目录。 + +## 三、商业版 14 模块 × 开源对照(哪些自研、哪些集成) + +| 模块 | 成熟开源 | 策略 | +|---|---|---| +| 数据集成 | SeaTunnel(Apache 顶级)、DataX、Flink CDC | **集成** | +| 调度 | DolphinScheduler(Apache 顶级) | **集成** | +| 元数据 | DataHub、OpenMetadata、Atlas、SQLFlow(列级血缘) | Go 自研管控面,大了再换存储层 | +| 数据质量 | Datavines(27 种规则)、Great Expectations、Deequ | 规则清单抄 Datavines,Go 自研执行 | +| 可视化 | DataEase(国产拖拉拽)、Superset、Metabase | **集成,不自研报表设计器** | +| 数据服务 API | 阿里 OneService 范式、magic-api(动态 SQL 出接口)、APISIX/Higress | **Go 自研(本项目最有价值组件)** | +| 数据标准 / 标签 / 生命周期 / 主数据 | 无统治级开源 | **自研(模型简单,依赖业务落地)** | +| AI 问数 | DB-GPT、Wren AI、Vanna | 参考,语义层自研 | +| OLAP 底座 | Doris(实时更新/JSON 占优)/ StarRocks(湖仓查询占优) | Phase 3 引入 | +| 运维/日志 | Prometheus + Grafana + Loki / 夜莺 | **集成** | + +**核心判断**: +1. 没有任何开源单体等于数据中台;中台 = 开源引擎 + **自研管控面/胶水层** + 治理流程。 +2. **Go 写管控面(元数据/目录/服务 API/标签/指标/AI 编排),Java 开源做执行面(调度/同步/计算)**——不自研 DolphinScheduler/SeaTunnel 等效物(十人年工程),用 Go 服务通过 OpenAPI 驱动它们。 +3. 本项目可复用资产:go-zero 网关+RPC 骨架、`beid` 多租户习惯、ES 依赖、用户体系(天然的标签/画像对象——有真实 To C 数据,比纯 B 端项目更有优势)。 + +## 四、分阶段路线 + +> 服务拆分沿用现有"网关 + rpc + model"模式;表名沿用 `dc_` 前缀 + `beid` 多租户字段。 + +### Phase 0:地基(已完成 ✅ 2026-10-10) +Go 1.26 / go-zero v1.10.3 / MySQL 8.4 / Redis 8 / etcd 3.5 / ES 7.17 全栈升级,详见 [UPGRADE.md](UPGRADE.md)。后续补:OpenTelemetry + Prometheus + Grafana + Loki(即"数据运维/系统日志"模块内核)。 + +### Phase 1:MVP 最小闭环(2-4 个月)——数据源 + 元数据 + 数据服务 API + 资产目录 +价值闭环:**登记数据源 → 采集元数据/血缘 → 沉淀资产目录 → 配置出数据 API 给第三方调用**。 + +新增服务:`datasource-rpc`、`metadata-rpc`、`datasvc-rpc`、`catalog-rpc`。 + +核心表雏形: +```sql +dc_datasource(id, name, type ENUM('mysql','redis','es','http','kafka'), host, port, + db_name, username, password_enc, params_json, status, owner, beid, ...) + +dc_meta_entity(id, entity_type ENUM('database','table','column','api','metric','topic'), + fqn VARCHAR(512) UNIQUE, name, comment, engine, rows_cnt, props_json, + classification ENUM('L1','L2','L3','L4'), owner, beid, ...) + +dc_meta_relation(id, rel_type ENUM('contains','lineage','depends'), from_id, to_id, props_json, beid) +dc_meta_version(entity_id, version, snapshot_json, published_at, operator) -- 定版/对比 + +dc_api(id, name, path UNIQUE, mode ENUM('wizard','sql','register'), datasource_id, + sql_text, script_js, params_json, cache_ttl, + status ENUM('draft','published','offline'), owner, beid) +dc_api_app(id, app_key UNIQUE, app_secret_enc, name, rate_limit_qps, ip_whitelist, beid) +dc_api_grant(api_id, app_id, expire_at, field_mask, beid) + +dc_asset(entity_id, category_id, asset_tags_json, visibility, beid) +dc_asset_category(id, parent_id, name, beid) +``` + +关键选型: +- 采集器:Go 直连 `information_schema`(MySQL)/ ES `_mapping` / Redis SCAN,一两天可出第一版; +- 血缘:先表级(集成任务/API 由本平台产生,写入时顺带登记,成本≈0);列级后置对接 SQLFlow; +- 元数据存储:MySQL + 递归 CTE 撑到几千实体没问题,勿过早引入图数据库。 + +验收:第三方 App 拿 AppKey 调配置出来的 API;目录里能看到 API→表→数据源血缘链;L4 字段出口脱敏。 + +### Phase 2:质量 + 指标 + 标签(+3-6 个月) +新增 `quality-rpc`、`metric-rpc`、`tag-rpc`。 +- 质量:规则分类抄 Datavines(行数/空值/唯一/值域/一致性/波动),go-zero job 服务执行,结果挂到资产目录"可信度"; +- 指标:原子指标 + 派生指标模型(阿里规范),指标可被数据服务引用成 API(**指标即服务**); +- 标签:`app_user`/`base_member` 天然是标签对象;规则 SQL → 结果人群表起步,量大后切 Doris bitmap 圈群。 + +### Phase 3:集成开发 + 调度 + CDC(+6-12 个月)——集成开源,不自研 +- 调度 → **DolphinScheduler**(`job-rpc` 对接其 OpenAPI 做运维视图);同步 → **SeaTunnel**(批流一体);实时 → **Flink CDC**;OLAP 底座 → **Doris**(与商业版同源)。 +- `ide-svc`:SQL IDE 与版本管理,任务物化为 DS 工作流;拖拉拽 DAG 后置。 +- 原则:本平台的元数据/血缘/质量**包住**开源引擎——每个同步任务自动登记血缘、挂入仓卡点。这是自研管控面的价值所在。 + +### Phase 4:可视化 + AI 问数(12 个月+) +- BI:集成 **DataEase**(贴合中文拖拉拽体验)或 Superset;知识图谱(Neo4j/NebulaGraph)后置。 +- **AI 问数(2026 现状)**:裸 NL2SQL 直连库准确率仅 62%-79%(腾讯实测),业界已转向**语义层路线**——LLM 只负责把自然语言映射到预定义指标/维度(NL2Metric),SQL 由指标引擎确定性生成。可行链路: + `问题 → 意图识别/改写(LLM) → 语义映射(向量检索命中指标/同义词,ES 即可起步) → 确定性生成查询 → 权限过滤+审查 → 走数据服务执行 → 结果+图表建议` + LLM 永远不直接拿生产库连接串。**成立前提正是 Phase 1/2 的元数据与指标**——AI 问数是元数据工程的复利出口。 + +### 目标架构 + +``` +datacenter-api(网关, 现有) + ├─ user/votes/questions/search(现有业务 RPC, 不动) + ├─ datasource-rpc metadata-rpc catalog-rpc ← Phase 1 + ├─ datasvc-rpc(执行器) [+ APISIX/Higress 可选前置] ← Phase 1 + ├─ quality-rpc metric-rpc tag-rpc ← Phase 2 + ├─ job-rpc(→DolphinScheduler) ide-rpc ← Phase 3 (SeaTunnel/Flink CDC/Doris) + └─ ai-rpc(→LLM 语义层) [DataEase 外挂] ← Phase 4 +基础: MySQL(元数据/管控) + Redis + etcd + ES + Prometheus/Grafana/Loki +``` + +## 五、最务实的三句话 + +1. **别从数仓开始,从"元数据 + 数据服务"开始**——Phase 1 四件套用 go-zero 两三个月可形成可演示闭环;"先建 Doris"会卡在无数据可入。 +2. **Go 写管控面,开源做执行面**——差异化价值(血缘打通、API 化、资产化、语义层)全在管控面,恰是 Go 擅长的。 +3. **AI 问数是叙事主线与复利出口**——技术上 2026 年可行(语义层路线),商业上性感,前提是 Phase 1/2 老老实实做元数据与指标。 + +## 六、来源(节选) + +- OneData 方法论:阿里云开发者社区、cnblogs.com/zsql +- 三云能力框架:help.aliyun.com(DataWorks)、huaweicloud.com(DataArts Studio)、cloud.tencent.com(WeData) +- 湖仓一体/演进:yunqi.tech、clouderacn.cn、databend.cn、aloudata.com、dtstack.com(数栈白皮书) +- 开源组件:DataHub、OpenMetadata、Apache DolphinScheduler/SeaTunnel/Atlas、Datavines、DataEase、Superset、APISIX、DB-GPT、Wren AI、Vanna(各官方仓库/官网) +- ChatBI 准确率:cloud.tencent.com《准确率是 ChatBI 的生命线》(2025-09) +- 本地现状:代码审读(14 张表全为 To C 活动业务,beid+ptyid 多租户雏形) diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md new file mode 100644 index 0000000..a980412 --- /dev/null +++ b/docs/UPGRADE.md @@ -0,0 +1,93 @@ +# 升级记录:go-zero v1.3.0 → v1.10.3(2026-10-10) + +## 版本对照 + +| 组件 | 升级前 | 升级后 | +|---|---|---| +| Go | 1.17 | **1.26.0**(go.mod)/ 工具链 1.27.2 | +| go-zero | v1.3.0 (2022) | **v1.10.3** (2026-08) | +| goctl | v1.3.0 | **v1.10.2** | +| grpc | v1.43.0 | v1.84.0 | +| protobuf | v1.27.1 | v1.36.12(protoc 36.2) | +| jwt | dgrijalva/jwt-go v3.2.0(停维护,**CVE-2020-26160**) | **golang-jwt/jwt/v4 v4.5.2**(与 go-zero 依赖对齐) | +| 七牛 SDK | qiniu/api.v7/v7 v7.8.2(仓库已归档) | **qiniu/go-sdk/v7 v7.29.0**(仅换 import,业务代码零修改) | +| elastic | olivere/elastic/v7 v7.0.28 | v7.0.32 | +| MySQL 镜像 | mysql:8.0.21 (2020) | **mysql:8.4** | +| Redis | sameersbn/redis(陈旧) | **redis:8-alpine** | +| etcd | quay.io/coreos/etcd(v3.3 时代) | **etcd v3.5.21** | +| Elasticsearch | spencezhou/elasticsearch:7.6.2(个人镜像) | **docker.elastic.co 官方 7.17.28**(密码用 `ELASTIC_PASSWORD=admin` 一键设置,不再需要手工 setup-passwords) | +| MySQL 字符集 | utf8(mb3) 与 utf8mb4 混用 | **全库 utf8mb4** | + +## 代码改动清单 + +1. **合并双 main**:删除 user/votes/common 三个 rpc 目录下与手写 `rpc.go` 重复的 goctl 生成 main(`user.go`/`votes.go`/`common.go`),`go build ./...` 恢复可用。 +2. **`redis.NewRedis` → `redis.MustNewRedis(c.CacheRedis[0].RedisConf)`**(3 处 ServiceContext,签名变更的编译级必改)。 +3. **jwt import** 换 `golang-jwt/jwt/v4`(2 处,签发代码零修改)。 +4. **`ioutil.ReadAll` → `io.ReadAll`**(shared/tools.go)。 +5. **存量 bug 修复**(vet 暴露): + - `votes/model/appvotesmodel.go:83`:Sprintf 占位符与参数不匹配,**按次数计票 SQL 一直是坏的**(`from <字段列表>` 而非 `from app_votes`); + - `search/rpc/dao/article.go:70`:`logx.Info` 误用格式化占位符 → `logx.Infof`; + - `shared/tools_test.go`:写死占位 URL 的必挂测试 → 改为 httptest 常规测试,并补充 `ToLimitOffset` 用例。 +6. **gofmt 全量格式化**(10 个历史未格式化文件)。 +7. **`.api` 文件**:全部补 `syntax = "v1"` 头(新版 goctl 硬要求);`type Xxx struct {` → `type Xxx {`(新语法)。 +8. **server.sh**:镜像全面升级(见上表),并给 stop/rm 加 `2>/dev/null` 容错。 + +## ⚠️ 最重要的破坏性变更:参数解析 tag 规则(v1.3.0 → v1.8+) + +go-zero 新版 `httpx.Parse` 逐阶段(path → form → header → json)用**各自专用 tag** 解析,且任何字段被某一阶段认领后就做 required 校验: + +- 旧版:`json` tag 可同时用于 query/form 解析; +- 新版:**query/form 只认 `form` tag**;而 GET 请求虽无 body,json 阶段仍会对 `json` tag 的 required 字段做空 map 校验 → json-only tag 的 GET 接口**必挂** `field "xxx" is not set`(官方 goctl 最新版生成的代码同样触发,属上游已知行为)。 + +**迁移格式**(本仓库已统一采用): + +```go +// GET query 参数(.api 与 types.go 保持一致) +Beid int64 `json:"beid,optional" form:"beid,optional"` +// POST JSON body 参数:保持 json-only,required 语义由 body 提供 +Mobile string `json:"mobile"` +``` + +> 禁止 `json:"xxx"` + `form:"xxx"` 双 required 组合(POST JSON 请求会挂在 form 阶段)。 +> GET 参数 required 校验由 form 阶段承担;多方法共用类型(如 `VoteReq` 同时用于 GET/POST)统一 `,optional`,必填校验下沉到业务层(voteslogic 本就有完整校验)。 + +涉及类型:`Beid`、`Actid`、`VoteReq`、`SearchReq`、`UserReq`(其余 POST-only 类型未动)。 + +## 其他行为变更(影响感知,无需改码) + +- `zrpc.RpcClientConf.NonBlock` 默认值 false → **true**:网关启动不再等待后端 RPC 就绪(服务可先起)。 +- trace 的 `Batcher: jaeger` 已移除(v1.10.0),配置 jaeger 需走 `otlpgrpc`;本项目未配置 trace,无影响。 +- MySQL 8.4:DSN 保持 `charset=utf8mb4&parseTime=true&loc=Asia/Shanghai`;如遇 `Public Key Retrieval is not allowed`,DSN 加 `allowPublicKeyRetrieval=true`(生产建议 TLS)。 + +## 验证结果(本机冒烟,Docker:MySQL 8.4 + Redis 8 + etcd v3.5.21) + +- `go build ./...` / `go vet ./...` / `go test ./...` ✅ +- GET `/common/appinfo?beid=1`:网关→etcd→common.rpc→MySQL→Redis 全链路 ✅,缓存键 `cache#BaseApp#id#1` 生成 ✅ +- POST `/common/snsinfo`:JSON body 解析 + 微信 login_url 组装 ✅ +- POST `/user/login`:user.rpc → `FindOneByMobile` 链路 ✅(返回业务态"用户未注册") +- 参数校验语义保持:GET 缺参仍返回 `field "beid" is not set` ✅ + +## 遗留事项 + +| 事项 | 说明 | +|---|---| +| `chanxuehong/wechat` 僵尸依赖 | 编译可用但 5 年未更新。迁移方案:`silenceper/wechat/v2 v2.1.14`(OAuth:`officialaccount/oauth`;JS-SDK:`officialaccount/js`),涉及 `wxticketlogic.go` + `snsloginlogic.go` 约百行胶水重写,**需真实公众号凭据回归**,故本次未动 | +| pb 未重新生成 | 旧 pb.go(protoc-gen-go v1.25)与新 grpc v1.84 兼容,全链路验证通过;待需要新字段时用 `goctl rpc protoc` 整套重生成(勿新旧混放) | +| vet "lock by value" 24 处 | 网关 logic/handler 以值传递 pb 结构的历史模式,随 pb 重生成一并改为指针传递 | +| 鉴权中间件被注释 | `usercheckmiddleware.go` 校验逻辑整段注释(升级前即如此),生产前必须恢复 | +| 全量 regen | goctl 不会覆盖已存在文件;如需 regen 须先备份删除旧生成物再 diff 合入 | + +## 工具链安装(本机,用户空间) + +```bash +# Go 1.27.2 +curl -o /tmp/go.tar.gz https://go.dev/dl/go1.27.2.linux-amd64.tar.gz +mkdir -p ~/.local && tar -C ~/.local -xzf /tmp/go.tar.gz +export PATH=$HOME/.local/go/bin:$HOME/go/bin:$PATH + +# goctl + protoc 工具链 +go install github.com/zeromicro/go-zero/tools/goctl@latest +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest +# protoc 36.2 位于 ~/.local/protoc/bin +``` diff --git a/go.mod b/go.mod index e50b8d2..0b12961 100644 --- a/go.mod +++ b/go.mod @@ -1,85 +1,114 @@ module datacenter -go 1.17 +go 1.26.0 require ( github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd - github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/golang/protobuf v1.5.2 - github.com/olivere/elastic/v7 v7.0.28 - github.com/qiniu/api.v7/v7 v7.8.2 - golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 - google.golang.org/grpc v1.43.0 - google.golang.org/protobuf v1.27.1 + github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/golang/protobuf v1.5.4 + github.com/olivere/elastic/v7 v7.0.32 + github.com/qiniu/go-sdk/v7 v7.29.0 + golang.org/x/time v0.16.0 + google.golang.org/grpc v1.84.0 + google.golang.org/protobuf v1.36.12 ) require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang-jwt/jwt/v4 v4.2.0 // indirect - github.com/openzipkin/zipkin-go v0.4.0 // indirect - go.opentelemetry.io/otel/exporters/zipkin v1.3.0 // indirect - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/grafana/pyroscope-go v1.3.0 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.21.0 // indirect + github.com/titanous/json5 v1.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/mock v0.6.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/term v0.45.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + modernc.org/fileutil v1.0.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973 // indirect github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13 // indirect - github.com/coreos/go-semver v0.3.0 // indirect - github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.2.2 // indirect - github.com/go-redis/redis v6.15.9+incompatible // indirect - github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/google/go-cmp v0.5.6 // indirect - github.com/google/gofuzz v1.1.0 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/gnostic v0.4.1 // indirect - github.com/gookit/color v1.3.6 // indirect - github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.11 // indirect - github.com/justinas/alice v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.11.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.26.0 // indirect - github.com/prometheus/procfs v0.6.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/zeromicro/go-zero v1.3.0 - go.etcd.io/etcd/api/v3 v3.5.1 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect - go.etcd.io/etcd/client/v3 v3.5.1 // indirect - go.opentelemetry.io/otel v1.3.0 // indirect - go.opentelemetry.io/otel/exporters/jaeger v1.3.0 // indirect - go.opentelemetry.io/otel/sdk v1.3.0 // indirect - go.opentelemetry.io/otel/trace v1.3.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/automaxprocs v1.4.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.17.0 // indirect - golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 // indirect - golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/appengine v1.6.5 // indirect - google.golang.org/genproto v0.0.0-20220112215332-a9c7c0acf9f2 // indirect + github.com/zeromicro/go-zero v1.10.3 + go.etcd.io/etcd/api/v3 v3.5.21 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect + go.etcd.io/etcd/client/v3 v3.5.21 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/api v0.20.12 // indirect - k8s.io/apimachinery v0.20.12 // indirect - k8s.io/client-go v0.20.12 // indirect - k8s.io/klog/v2 v2.40.1 // indirect - k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect - sigs.k8s.io/yaml v1.2.0 // indirect + k8s.io/api v0.34.3 // indirect + k8s.io/apimachinery v0.34.3 // indirect + k8s.io/client-go v0.34.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index e43fe75..dd57003 100644 --- a/go.sum +++ b/go.sum @@ -1,749 +1,320 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/clickhouse-go v1.5.1/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs= -github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.17.0 h1:EwLdrIS50uczw71Jc7iVSxZluTKj5nfSP8n7ARRnJy0= -github.com/alicebob/miniredis/v2 v2.17.0/go.mod h1:gquAfGbzn92jvtrSC69+6zZnwSODVXVpYDRaGhWaL6I= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.40.32/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw= +github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973 h1:Js/7nHtkpvUW62passc9FOflXyLQDjSoYclkFeDdTEM= github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973/go.mod h1:9+sJ9zvvkXC5sPjPEZM3Jpb9n2Q2VtcrGZly0UHYF5I= github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13 h1:c1vUDbnwvu5d2ucfzXvMzBWzeu5IxPvtESOFPl3CieA= github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13/go.mod h1:XEYt99iTxMqkv+gW85JX/DdUINHUe43Sbe5AtqSaDAQ= github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd h1:TM3wjEWel4U31J72dlhnwCBqPC0+FA0Ejm2NCbn5a5U= github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd/go.mod h1:/dvhOIRCjjiZu6NV0QTTiMcc5XwoORbxfDSsRY2IfaM= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= +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-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gookit/color v1.3.6 h1:Rgbazd4JO5AgSTVGS3o0nvaSdwdrS8bzvIXwtK6OiMk= -github.com/gookit/color v1.3.6/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8= +github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY= +github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= -github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/olivere/elastic/v7 v7.0.28 h1:KAP4EuaEcvPJknRNkAAso1xeu0C1+/CeDQsxj9Cw9Fg= -github.com/olivere/elastic/v7 v7.0.28/go.mod h1:DzHQoqd6YqSuvF1lk/fR4cW4FNUNzSD5/F5MBm3GRMo= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ= -github.com/openzipkin/zipkin-go v0.4.0 h1:CtfRrOVZtbDj8rt1WXjklw0kqqJQwICrCKmlfUuBUUw= -github.com/openzipkin/zipkin-go v0.4.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= +github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/qiniu/api.v7/v7 v7.8.2 h1:f08kI0MmsJNzK4sUS8bG3HDH67ktwd/ji23Gkiy2ra4= -github.com/qiniu/api.v7/v7 v7.8.2/go.mod h1:FPsIqxh1Ym3X01sANE5ZwXfLZSWoCUp5+jNI8cLo3l0= -github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= -github.com/smartystreets/gunit v1.4.2/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/qiniu/go-sdk/v7 v7.29.0 h1:W6bnl2IEtpkKH/MJtYC3HKGwcEViu7hqZ2OWc0Dg78Q= +github.com/qiniu/go-sdk/v7 v7.29.0/go.mod h1:pTwVR1B+8SXcPLhDzBUasiKFTD9F7jRglRDR553BW3k= +github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E= +github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= +github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= +github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s= +github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg= -github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= -github.com/zeromicro/go-zero v1.3.0 h1:Eyn36yBtR043sm4YKmxR6eS3UA/GtZDktQ+UqIJ3Lm0= -github.com/zeromicro/go-zero v1.3.0/go.mod h1:Hy4o1VFAt32lXaQMbaBhoFeZjA/rJqJ4PTGNdGsURcc= -go.etcd.io/etcd/api/v3 v3.5.1 h1:v28cktvBq+7vGyJXF8G+rWJmj+1XUmMtqcLnH8hDocM= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.1 h1:XIQcHCFSG53bJETYeRJtIxdLv2EWRGxcfzR8lSnTH4E= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v3 v3.5.1 h1:oImGuV5LGKjCqXdjkMHCyWa5OO1gYKCnC/1sgdfj1Uk= -go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46OtKyd3Q= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/otel v1.3.0 h1:APxLf0eiBwLl+SOXiJJCVYzA1OOJNyAoV8C5RNRyy7Y= -go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= -go.opentelemetry.io/otel/exporters/jaeger v1.3.0 h1:HfydzioALdtcB26H5WHc4K47iTETJCdloL7VN579/L0= -go.opentelemetry.io/otel/exporters/jaeger v1.3.0/go.mod h1:KoYHi1BtkUPncGSRtCe/eh1ijsnePhSkxwzz07vU0Fc= -go.opentelemetry.io/otel/exporters/zipkin v1.3.0 h1:uOD28dZ7yIKITTcUS6MeAGNHYy3uhP7DTkhcJM6onlQ= -go.opentelemetry.io/otel/exporters/zipkin v1.3.0/go.mod h1:LxGGfHIYbvsFnrJtBcazb0yG24xHdDGrT/H6RB9r3+8= -go.opentelemetry.io/otel/sdk v1.3.0 h1:3278edCoH89MEJ0Ky8WQXVmDQv3FX4ZJ3Pp+9fJreAI= -go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= -go.opentelemetry.io/otel/trace v1.3.0 h1:doy8Hzb1RJ+I3yFhtDmwNc7tIyw1tNMOIsyPzp1NOGY= -go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0= -go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +github.com/zeromicro/go-zero v1.10.3 h1:fm4+jUuUF77IWtFeAyf2xVoBRcgEpF1NZJUqTvZ3dw0= +github.com/zeromicro/go-zero v1.10.3/go.mod h1:Gnac2bT/JGb9Ja79wchssVeYtJxuWWzL98DuLH11kds= +go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= +go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= +go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= +go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= +go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= +go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= +go.opentelemetry.io/otel/exporters/zipkin v1.40.0 h1:zu+I4j+FdO6xIxBVPeuncQVbjxUM4LiMgv6GwGe9REE= +go.opentelemetry.io/otel/exporters/zipkin v1.40.0/go.mod h1:zS6cC4nFBYXbu18e7aLfMzubBjOiN7ZcROu477qtMf8= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 h1:kETrAMYZq6WVGPa8IIixL0CaEcIUNi+1WX7grUoi3y8= -golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs= -golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= -golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20220112215332-a9c7c0acf9f2 h1:z+R4M/SuyaRsj1zu3WC+nIQyfSrSIpuDcY01/R3uCtg= -google.golang.org/genproto v0.0.0-20220112215332-a9c7c0acf9f2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= -google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4= +google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.12 h1:LfRpmRkJLwPP8eaYehsVVmIIfg1yCBIIUHaSsdqCgHA= -k8s.io/api v0.20.12/go.mod h1:A2brwyEkVLM3wQGNnzoAa5JsQRzHK0uoOQ+bsnv7V68= -k8s.io/apimachinery v0.20.12 h1:2c0LIVNMvB8k2Ozstmhl2zGeCEcPazznuLYEwxFdNjM= -k8s.io/apimachinery v0.20.12/go.mod h1:uM7hCI0NyBymUwgshMgZyte475lxhr+QH6h3cvdnzEc= -k8s.io/client-go v0.20.12 h1:U75SxTC31BHT9i7CbX/hL4v+U1Wkzy/E1vt5ClDPp3I= -k8s.io/client-go v0.20.12/go.mod h1:NBJj6Evp73Xy/4v/O/RDRaH0+3JoxNfjRxkyRgrdbsA= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.40.1 h1:P4RRucWk/lFOlDdkAr3mc7iWFkgKrZY9qZMAgek06S4= -k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/logic/common/qiuniutokenlogic.go b/internal/logic/common/qiuniutokenlogic.go index 4ca78bc..a321f15 100644 --- a/internal/logic/common/qiuniutokenlogic.go +++ b/internal/logic/common/qiuniutokenlogic.go @@ -8,8 +8,8 @@ import ( "datacenter/internal/types" "datacenter/shared" - "github.com/qiniu/api.v7/v7/auth" - "github.com/qiniu/api.v7/v7/storage" + "github.com/qiniu/go-sdk/v7/auth" + "github.com/qiniu/go-sdk/v7/storage" "github.com/zeromicro/go-zero/core/logx" ) diff --git a/internal/logic/datacenterlogic.go b/internal/logic/datacenterlogic.go index 3436f0a..85891da 100644 --- a/internal/logic/datacenterlogic.go +++ b/internal/logic/datacenterlogic.go @@ -7,7 +7,7 @@ import ( "datacenter/internal/svc" "datacenter/internal/types" - "github.com/dgrijalva/jwt-go" + "github.com/golang-jwt/jwt/v4" "github.com/zeromicro/go-zero/core/logx" ) diff --git a/internal/svc/servicecontext.go b/internal/svc/servicecontext.go index cd9035f..c3fa99c 100644 --- a/internal/svc/servicecontext.go +++ b/internal/svc/servicecontext.go @@ -57,7 +57,7 @@ func NewServiceContext(c config.Config) *ServiceContext { qr := questionsclient.NewQuestions(zrpc.MustNewClient(c.QuestionsRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) //缓存 ca := cache.New(c.CacheRedis, syncx.NewSingleFlight(), cache.NewStat("dc"), shared.ErrNotFound) - rcon := redis.NewRedis(c.CacheRedis[0].Host, c.CacheRedis[0].Type, c.CacheRedis[0].Pass) + rcon := redis.MustNewRedis(c.CacheRedis[0].RedisConf) return &ServiceContext{ Config: c, GreetMiddleware1: greetMiddleware1, diff --git a/internal/types/types.go b/internal/types/types.go index 0ab2222..dffe84b 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2,7 +2,7 @@ package types type Beid struct { - Beid int64 `json:"beid"` + Beid int64 `json:"beid,optional" form:"beid,optional"` } type Token struct { @@ -100,10 +100,10 @@ type JwtToken struct { } type UserReq struct { - Auid int64 `json:"auid"` - Uid int64 `json:"uid"` - Beid int64 `json:"beid"` //应用id - Ptyid int64 `json:"ptyid"` //对应平台 + Auid int64 `json:"auid,optional" form:"auid,optional"` + Uid int64 `json:"uid,optional" form:"uid,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` //应用id + Ptyid int64 `json:"ptyid,optional" form:"ptyid,optional"` } type Request struct { @@ -115,11 +115,11 @@ type Response struct { } type Actid struct { - Actid int64 `json:"actid"` //活动id + Actid int64 `json:"actid,optional" form:"actid,optional"` //活动id } type VoteReq struct { - Aeid int64 `json:"aeid"` // 作品id + Aeid int64 `json:"aeid,optional" form:"aeid,optional"` // 作品id Actid } @@ -162,9 +162,9 @@ type EnrollResp struct { } type SearchReq struct { - Keyword string `json:"keyword"` - Page string `json:"page"` - Size string `json:"size"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + Page string `json:"page,optional" form:"page,optional"` + Size string `json:"size,optional" form:"size,optional"` } type SearchResp struct { diff --git a/questions.api b/questions.api index db1db20..46705f5 100644 --- a/questions.api +++ b/questions.api @@ -1,3 +1,4 @@ +syntax = "v1" // 问答 抽奖 开始 @server( group: questions @@ -18,20 +19,20 @@ service datacenter-api { get /questions/award/list (Actid) returns (ActivityResp) } -type AnswerReq struct { +type AnswerReq { ActivityId int64 `json:"actid"` Answers string `json:"answers"` Score string `json:"score"` } -type QuestionsAwardReq struct { +type QuestionsAwardReq { ActivityId int64 `json:"actid"` AnswerId int64 `json:"answerid"` } -type AnswerResp struct { +type AnswerResp { Answers string `json:"answers"` Score string `json:"score"` } -type AwardConvertReq struct { +type AwardConvertReq { UserName string `json:"username"` Phone string `json:"phone"` LotteryId int64 `json:"lotteryid"` diff --git a/questions/model/appquestionsawardsmodel.go b/questions/model/appquestionsawardsmodel.go index 1cdf9ff..2480f4c 100755 --- a/questions/model/appquestionsawardsmodel.go +++ b/questions/model/appquestionsawardsmodel.go @@ -71,7 +71,7 @@ func (m *defaultAppQuestionsAwardsModel) Insert(data AppQuestionsAwards) (sql.Re return ret, err } -//获取所有的 +// 获取所有的 func (m *defaultAppQuestionsAwardsModel) Find(id int64) ([]AppQuestionsAwards, error) { //缓存 后面来加吧 var resp []AppQuestionsAwards diff --git a/questions/rpc/internal/logic/postquestionschangelogic.go b/questions/rpc/internal/logic/postquestionschangelogic.go index e0dee34..99fafc7 100644 --- a/questions/rpc/internal/logic/postquestionschangelogic.go +++ b/questions/rpc/internal/logic/postquestionschangelogic.go @@ -24,7 +24,7 @@ func NewPostQuestionsChangeLogic(ctx context.Context, svcCtx *svc.ServiceContext } } -// 提交 答案 +// 提交 答案 func (l *PostQuestionsChangeLogic) PostQuestionsChange(in *questions.QuestionsAnswerReq) (*questions.QuestionsAnswerResp, error) { logx.Info("in=", in) result, err := l.svcCtx.QuestionsAnswersModel.Insert(model.AppQuestionsAnswers{ diff --git a/questions/rpc/internal/logic/postturntablelogic.go b/questions/rpc/internal/logic/postturntablelogic.go index af88bee..e8753eb 100644 --- a/questions/rpc/internal/logic/postturntablelogic.go +++ b/questions/rpc/internal/logic/postturntablelogic.go @@ -90,7 +90,7 @@ doDefault: }, nil } -//检查 奖品信息 +// 检查 奖品信息 func (l *PostTurnTableLogic) prize(actid, prizeCode int64) (*model.AppQuestionsAwards, error) { var prizeGift *model.AppQuestionsAwards diff --git a/questions/rpc/internal/server/questionsserver.go b/questions/rpc/internal/server/questionsserver.go index 02519ed..faa6d17 100644 --- a/questions/rpc/internal/server/questionsserver.go +++ b/questions/rpc/internal/server/questionsserver.go @@ -45,7 +45,7 @@ func (s *QuestionsServer) GetQuestionsList(ctx context.Context, in *questions.Ac return l.GetQuestionsList(in) } -// 提交 答案 +// 提交 答案 func (s *QuestionsServer) PostQuestionsChange(ctx context.Context, in *questions.QuestionsAnswerReq) (*questions.QuestionsAnswerResp, error) { l := logic.NewPostQuestionsChangeLogic(ctx, s.svcCtx) return l.PostQuestionsChange(in) diff --git a/questions/rpc/internal/svc/servicecontext.go b/questions/rpc/internal/svc/servicecontext.go index 6e96a96..f984023 100644 --- a/questions/rpc/internal/svc/servicecontext.go +++ b/questions/rpc/internal/svc/servicecontext.go @@ -20,7 +20,7 @@ type ServiceContext struct { } func NewServiceContext(c config.Config) *ServiceContext { - rconn := redis.NewRedis(c.CacheRedis[0].Host, c.CacheRedis[0].Type, c.CacheRedis[0].Pass) + rconn := redis.MustNewRedis(c.CacheRedis[0].RedisConf) mconn := sqlx.NewMysql(c.Mysql.DataSource) qam := model.NewAppQuestionsActivitiesModel(mconn, c.CacheRedis) qawardsm := model.NewAppQuestionsAwardsModel(mconn, c.CacheRedis) diff --git a/questions/rpc/questions/questions.pb.go b/questions/rpc/questions/questions.pb.go index 0ae34a6..0b8e995 100644 --- a/questions/rpc/questions/questions.pb.go +++ b/questions/rpc/questions/questions.pb.go @@ -170,7 +170,7 @@ func (x *ActivitiesReq) GetActid() int64 { return 0 } -//返回问答抽奖活动信息 +// 返回问答抽奖活动信息 type ActInfoResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -638,7 +638,7 @@ func (x *QuestionsListResp) GetData() []*QuestionsResp { return nil } -//答案提交 +// 答案提交 type QuestionsAnswerReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -734,7 +734,7 @@ func (x *QuestionsAnswerReq) GetAuid() int64 { return 0 } -//问答返回 +// 问答返回 type QuestionsAnswerResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -790,7 +790,7 @@ func (x *QuestionsAnswerResp) GetScore() string { return "" } -//得分 +// 得分 type GradeReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -854,7 +854,7 @@ func (x *GradeReq) GetAuid() int64 { return 0 } -//抽奖 +// 抽奖 type TurnTableReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -942,7 +942,7 @@ func (x *TurnTableReq) GetAnswerId() int64 { return 0 } -//兑奖 +// 兑奖 type ConvertReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache diff --git a/questions/rpc/questionsclient/questions.go b/questions/rpc/questionsclient/questions.go index 2b22c04..d1adf4c 100644 --- a/questions/rpc/questionsclient/questions.go +++ b/questions/rpc/questionsclient/questions.go @@ -83,7 +83,7 @@ func (m *defaultQuestions) GetQuestionsList(ctx context.Context, in *ActivitiesR return client.GetQuestionsList(ctx, in) } -// 提交 答案 +// 提交 答案 func (m *defaultQuestions) PostQuestionsChange(ctx context.Context, in *QuestionsAnswerReq) (*QuestionsAnswerResp, error) { client := questions.NewQuestionsClient(m.cli.Conn()) return client.PostQuestionsChange(ctx, in) diff --git a/search.api b/search.api index c68d422..a73f44e 100644 --- a/search.api +++ b/search.api @@ -1,17 +1,18 @@ +syntax = "v1" -type SearchReq struct { - Keyword string `json:"keyword"` - Page string `json:"page"` - Size string `json:"size"` +type SearchReq { + Keyword string `json:"keyword,optional" form:"keyword,optional"` + Page string `json:"page,optional" form:"page,optional"` + Size string `json:"size,optional" form:"size,optional"` } -type SearchResp struct { +type SearchResp { Data []ArticleReq `json:"data"` } -type ArticleReq struct{ +type ArticleReq { NewsId string `json:"NewsId"` NewsTitle string `json:"NewsTitle"` ImageUrl string `json:"ImageUrl"` diff --git a/search/rpc/dao/article.go b/search/rpc/dao/article.go index e29472e..01c593f 100644 --- a/search/rpc/dao/article.go +++ b/search/rpc/dao/article.go @@ -67,14 +67,14 @@ func NewArticelES(client *elastic.Client) (*ArticelES, error) { if !exists { _, err := articelEs.client.CreateIndex(articelEs.index).Body(articelEs.mapping).Do(context.Background()) if err != nil { - logx.Info("userEs init failed err is %s\n", err) + logx.Infof("userEs init failed err is %s\n", err) return nil, err } } return articelEs, nil } -//索引 +// 索引 func (aes *ArticelES) Index(art *searchclient.ArticleReq) (string, error) { res, err := aes.client.Index().Index(aes.index).Id(art.NewsId).BodyJson(art).Do(context.Background()) if err != nil { diff --git a/search/rpc/internal/logic/articleinitlogic.go b/search/rpc/internal/logic/articleinitlogic.go index bc88cdb..709f397 100644 --- a/search/rpc/internal/logic/articleinitlogic.go +++ b/search/rpc/internal/logic/articleinitlogic.go @@ -13,8 +13,8 @@ import ( "github.com/zeromicro/go-zero/core/logx" ) -//数据初始化模板事例 -//一些数据结构 +// 数据初始化模板事例 +// 一些数据结构 type PageData struct { ResultMsg string Data Data @@ -43,7 +43,7 @@ func NewArticleInitLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Artic } } -//这个是个递归 +// 这个是个递归 func (l *ArticleInitLogic) ArticleRecursion(gourl string, form url.Values, once bool) error { body, err := shared.HttpPostForm(gourl, form) if err != nil { diff --git a/server.sh b/server.sh index a692be9..461d352 100644 --- a/server.sh +++ b/server.sh @@ -3,31 +3,29 @@ ## Email net.webjoy@gmail.com ## wechat 13228191831 ##### 启动mysql 服务 -docker kill mysql -docker rm mysql -docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=admin -v $(pwd)/mysql8:/var/lib/mysql -d mysql:8.0.21 +docker kill mysql 2>/dev/null +docker rm mysql 2>/dev/null +docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=admin -v $(pwd)/mysql8:/var/lib/mysql -d mysql:8.4 -##### 启动redis 服务 -docker stop redis -docker rm redis +##### 启动redis 服务(如宿主机 6379 被占用,请自行调整映射端口) +docker stop redis 2>/dev/null +docker rm redis 2>/dev/null echo "Start Redis Service..." docker run --name redis -d \ --publish 6379:6379 \ - --env 'REDIS_PASSWORD=admin' \ - --volume $(pwd)/redis:/var/lib/redis \ - sameersbn/redis:latest + redis:8-alpine \ + redis-server --requirepass admin ### 启动 etcd 服务 -#!/bin/bash rm -rf $(pwd)/etcd && mkdir -p $(pwd)/etcd && \ - docker stop etcd && \ - docker rm etcd || true && \ + docker stop etcd 2>/dev/null + docker rm etcd 2>/dev/null || true && \ docker run -d \ -p 2379:2379 \ -p 2380:2380 \ --mount type=bind,source=$(pwd)/etcd,destination=/etcd-data \ --name etcd \ - quay.io/coreos/etcd \ + quay.io/coreos/etcd:v3.5.21 \ /usr/local/bin/etcd \ --name s1 \ --data-dir /etcd-data \ @@ -41,11 +39,13 @@ rm -rf $(pwd)/etcd && mkdir -p $(pwd)/etcd && \ #### 启动elasticsearch 服务 -docker stop es -docker rm es +docker stop es 2>/dev/null +docker rm es 2>/dev/null docker run -d --name es \ -p 9200:9200 -p 9300:9300 \ -e "discovery.type=single-node" \ + -e "xpack.security.enabled=true" \ + -e "ELASTIC_PASSWORD=admin" \ -v $(pwd)/elasticsearch:/usr/share/elasticsearch/data \ - spencezhou/elasticsearch:7.6.2 \ No newline at end of file + docker.elastic.co/elasticsearch/elasticsearch:7.17.28 diff --git a/shared/tools.go b/shared/tools.go index 84661b1..ba6b682 100644 --- a/shared/tools.go +++ b/shared/tools.go @@ -2,7 +2,7 @@ package shared import ( "fmt" - "io/ioutil" + "io" "math/rand" "net" "net/http" @@ -91,7 +91,7 @@ func HttpPostForm(gourl string, form url.Values) ([]byte, error) { return []byte{}, err } defer resp.Body.Close() - return ioutil.ReadAll(resp.Body) + return io.ReadAll(resp.Body) } // 得到一个随机数 diff --git a/shared/tools_test.go b/shared/tools_test.go index 57b443b..94ba4ab 100644 --- a/shared/tools_test.go +++ b/shared/tools_test.go @@ -1,14 +1,41 @@ package shared import ( + "net/http" + "net/http/httptest" "net/url" "testing" ) func TestHttpPostForm(t *testing.T) { - body, err := HttpPostForm("http://xxxxxxxxxx", url.Values{}) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + t.Errorf("解析表单失败: %v", err) + } + if got := r.FormValue("name"); got != "jackluo" { + t.Errorf("表单字段 name = %q, 期望 %q", got, "jackluo") + } + w.Write([]byte("ok")) + })) + defer srv.Close() + + body, err := HttpPostForm(srv.URL, url.Values{"name": {"jackluo"}}) if err != nil { - t.Error(err) + t.Fatal(err) + } + if string(body) != "ok" { + t.Errorf("响应体 = %q, 期望 %q", string(body), "ok") + } +} + +func TestToLimitOffset(t *testing.T) { + offset, limit := ToLimitOffset("3", "20") + if offset != 40 || limit != 20 { + t.Errorf("ToLimitOffset(3,20) = (%d, %d), 期望 (40, 20)", offset, limit) + } + //默认值:page<=0 归 1,limit=0 归 10 + offset, limit = ToLimitOffset("0", "") + if offset != 0 || limit != 10 { + t.Errorf("ToLimitOffset(0,\"\") = (%d, %d), 期望 (0, 10)", offset, limit) } - t.Error(string(body)) } diff --git a/sql.sql b/sql.sql index 449b0bd..6036fb2 100644 --- a/sql.sql +++ b/sql.sql @@ -34,7 +34,7 @@ CREATE TABLE `app_config` ( `updated_at` datetime(3) DEFAULT NULL, `deleted_at` datetime(3) DEFAULT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='中台APP社交属性表'; +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='中台APP社交属性表'; -- ---------------------------- -- Records of app_config @@ -105,13 +105,13 @@ CREATE TABLE `app_votes` ( `uid` bigint NOT NULL DEFAULT '0' COMMENT '中台表用户的id', `auid` bigint NOT NULL DEFAULT '0' COMMENT '中台表appuser的id', `actid` bigint NOT NULL DEFAULT '0' COMMENT '投票活动的id', - `ip` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '投票人IP', + `ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '投票人IP', `aeid` bigint unsigned NOT NULL COMMENT '投票的id', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `deleted_at` datetime(3) DEFAULT NULL, PRIMARY KEY (`avid`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='投票记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COMMENT='投票记录表'; -- ---------------------------- -- Table structure for app_votes_activity @@ -138,7 +138,7 @@ CREATE TABLE `app_votes_activity` ( `num` int DEFAULT NULL COMMENT '单位', `deleted_at` datetime(3) DEFAULT NULL, PRIMARY KEY (`actid`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='投票活动表'; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='投票活动表'; -- ---------------------------- -- Records of app_votes_activity @@ -164,7 +164,7 @@ CREATE TABLE `base_app` ( `updated_at` datetime(3) DEFAULT NULL, `deleted_at` datetime(3) DEFAULT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='中台APP表'; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='中台APP表'; -- ---------------------------- -- Records of base_app @@ -191,10 +191,10 @@ CREATE TABLE `base_member` ( `deleted_at` datetime(3) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `mobile_index` (`mobile`) -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='中台用户表'; +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='中台用户表'; ---- 加入问答抽奖 +-- 加入问答抽奖 -- ---------------------------- @@ -206,15 +206,15 @@ CREATE TABLE `app_questions` ( `beid` int NOT NULL DEFAULT '0' COMMENT '对应的平台', `ptyid` int NOT NULL COMMENT '平台id: 1.微信公众号,2.微信小程序,3.支付宝', `activity_id` int NOT NULL DEFAULT '0' COMMENT '活动的id', - `question` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '问题', - `options` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '选项', - `corrent` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT 'A' COMMENT '正确选项(ABCD)', + `question` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '问题', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '选项', + `corrent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'A' COMMENT '正确选项(ABCD)', `status` int NOT NULL DEFAULT '0' COMMENT '状态(01)', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `questions_question_unique` (`question`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; -- ---------------------------- -- Records of app_questions @@ -238,21 +238,21 @@ CREATE TABLE `app_questions_activities` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `beid` int NOT NULL DEFAULT '0' COMMENT '对应的平台', `ptyid` int NOT NULL COMMENT '平台id: 1.微信公众号,2.微信小程序,3.支付宝', - `title` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '活动主题', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '活动主题', `start_date` int NOT NULL DEFAULT '0' COMMENT '投票活动开始时间', `end_date` int NOT NULL DEFAULT '0' COMMENT '投票活动结束时间', `get_score` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '及格分数', - `activity_week` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '活动周期(可多选),分隔', - `header` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '分享标题', - `des` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '分享文字', - `image` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '分享图片', - `rule` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '抽奖规则', + `activity_week` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '活动周期(可多选),分隔', + `header` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分享标题', + `des` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分享文字', + `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分享图片', + `rule` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '抽奖规则', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `activities_title_unique` (`title`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; -- ---------------------------- -- Records of app_questions_activities @@ -273,15 +273,15 @@ CREATE TABLE `app_questions_answers` ( `auid` bigint DEFAULT NULL, `test_id` int unsigned NOT NULL, `activity_id` int unsigned NOT NULL, - `answers` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '用户答题对错', - `score` varchar(10) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '0.00' COMMENT '得分', + `answers` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户答题对错', + `score` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0.00' COMMENT '得分', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) USING BTREE, KEY `answers_uid_foreign` (`uid`) USING BTREE, KEY `answers_test_id_foreign` (`test_id`) USING BTREE, KEY `answers_activity_id_foreign` (`activity_id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; -- ---------------------------- -- Table structure for app_questions_awards @@ -291,20 +291,20 @@ CREATE TABLE `app_questions_awards` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `beid` int NOT NULL DEFAULT '0' COMMENT '对应的平台', `ptyid` int NOT NULL COMMENT '平台id: 1.微信公众号,2.微信小程序,3.支付宝', - `title` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '奖项名', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '奖项名', `activity_id` int unsigned NOT NULL, `start_probability` int NOT NULL DEFAULT '0' COMMENT '开始概率', `end_probability` int NOT NULL DEFAULT '0' COMMENT '结束概率', `number` int unsigned NOT NULL DEFAULT '0' COMMENT '中奖个数', `is_lottery` tinyint NOT NULL DEFAULT '0' COMMENT '是否属于中奖', - `header` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '分享标题', - `des` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '分享文本', - `image` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '分享图片', + `header` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分享标题', + `des` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分享文本', + `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '分享图片', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `awards_title_unique` (`title`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; -- ---------------------------- -- Records of app_questions_awards @@ -329,13 +329,13 @@ CREATE TABLE `app_questions_converts` ( `uid` bigint NOT NULL DEFAULT '0' COMMENT '中台表用户的id', `auid` bigint NOT NULL DEFAULT '0' COMMENT '中台表用户的id', `lottery_id` int unsigned NOT NULL, - `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '获奖名', - `phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '手机号', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '获奖名', + `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号', `status` int NOT NULL DEFAULT '0' COMMENT '处理(0/1)', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; -- ---------------------------- -- Table structure for app_questions_lotteries @@ -359,7 +359,7 @@ CREATE TABLE `app_questions_lotteries` ( KEY `lotteries_activity_id_foreign` (`activity_id`) USING BTREE, KEY `lotteries_answer_id_foreign` (`answer_id`) USING BTREE, KEY `lotteries_award_id_foreign` (`award_id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; -- ---------------------------- -- Table structure for app_questions_tests @@ -369,14 +369,14 @@ CREATE TABLE `app_questions_tests` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `beid` int NOT NULL DEFAULT '0' COMMENT '对应的平台', `ptyid` int NOT NULL COMMENT '平台id: 1.微信公众号,2.微信小程序,3.支付宝', - `title` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '题库类名', - `question_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '题库题编号', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '题库类名', + `question_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '题库题编号', `status` int NOT NULL DEFAULT '0' COMMENT '状态(0/1)', `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `tests_title_unique` (`title`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; diff --git a/user.api b/user.api index 80380c6..822010c 100644 --- a/user.api +++ b/user.api @@ -1,26 +1,27 @@ +syntax = "v1" //注册请求 -type RegisterReq struct { +type RegisterReq { // TODO: add members here and delete this comment Mobile string `json:"mobile"` //基本一个手机号码就完事 Password string `json:"password"` Smscode string `json:"smscode"` //短信码 } //登陆请求 -type LoginReq struct{ +type LoginReq { Mobile string `json:"mobile"` Type int64 `json:"type"` //1.密码登陆,2.短信登陆 Password string `json:"password"` } //微信登陆 -type WxLoginReq struct { +type WxLoginReq { Beid int64 `json:"beid"` //应用id Code string `json:"code"` //微信登陆密钥 Ptyid int64 `json:"ptyid"` //对应平台 } //返回用户信息 -type UserReply struct { +type UserReply { Auid int64 `json:"auid"` Uid int64 `json:"uid"` Beid int64 `json:"beid"` //应用id @@ -33,7 +34,7 @@ type UserReply struct { JwtToken } //返回APPUser -type AppUser struct{ +type AppUser { Uid int64 `json:"uid"` Auid int64 `json:"auid"` Beid int64 `json:"beid"` //应用id @@ -43,7 +44,7 @@ type AppUser struct{ Avator string `json:"avator"` } -type LoginAppUser struct{ +type LoginAppUser { Uid int64 `json:"uid"` Auid int64 `json:"auid"` Beid int64 `json:"beid"` //应用id @@ -54,17 +55,17 @@ type LoginAppUser struct{ JwtToken } -type JwtToken struct { +type JwtToken { AccessToken string `json:"access_token,omitempty"` AccessExpire int64 `json:"access_expire,omitempty"` RefreshAfter int64 `json:"refresh_after,omitempty"` } -type UserReq struct{ - Auid int64 `json:"auid"` - Uid int64 `json:"uid"` - Beid int64 `json:"beid"` //应用id - Ptyid int64 `json:"ptyid"` //对应平台 +type UserReq { + Auid int64 `json:"auid,optional" form:"auid,optional"` + Uid int64 `json:"uid,optional" form:"uid,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` //应用id + Ptyid int64 `json:"ptyid,optional" form:"ptyid,optional"` //对应平台 } type Request { diff --git a/user/api/internal/logic/loginlogic.go b/user/api/internal/logic/loginlogic.go index ada59f4..4292166 100644 --- a/user/api/internal/logic/loginlogic.go +++ b/user/api/internal/logic/loginlogic.go @@ -8,7 +8,7 @@ import ( "datacenter/user/api/internal/types" "datacenter/user/model" - "github.com/dgrijalva/jwt-go" + "github.com/golang-jwt/jwt/v4" "github.com/zeromicro/go-zero/core/logx" ) diff --git a/user/api/user.api b/user/api/user.api index 201f05f..3cb5659 100644 --- a/user/api/user.api +++ b/user/api/user.api @@ -1,3 +1,4 @@ +syntax = "v1" info( title: "user api"// TODO: add title desc: "用户系统"// TODO: add description @@ -5,7 +6,7 @@ info( email: "net.webjoy@gmail.com" ) //注册请求 -type RegisterReq struct { +type RegisterReq { // TODO: add members here and delete this comment Username string `json:"username"` Mobile string `json:"mobile"` //基本一个手机号码就完事 @@ -13,14 +14,14 @@ type RegisterReq struct { Smscode string `json:"smscode"` //短信码 } //登陆请求 -type LoginReq struct{ +type LoginReq { Username string `json:"username"` Mobile string `json:"mobile"` Password string `json:"password"` Smscode string `json:"smscode"` //短信码 } //返回用户信息 -type UserReply struct { +type UserReply { auid int64 `json:"auid"` Uid int64 `json:"uid"` Beid int64 `json:"beid"` //应用id @@ -34,7 +35,7 @@ type UserReply struct { JwtToken } -type JwtToken struct { +type JwtToken { AccessToken string `json:"accessToken,omitempty"` AccessExpire int64 `json:"accessExpire,omitempty"` RefreshAfter int64 `json:"refreshAfter,omitempty"` diff --git a/user/rpc/rpc.go b/user/rpc/rpc.go index 509eac8..13f423f 100644 --- a/user/rpc/rpc.go +++ b/user/rpc/rpc.go @@ -40,7 +40,7 @@ func main() { s.Start() } -//拦截验证器 +// 拦截验证器 func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { if !limiter.Allow() { diff --git a/user/rpc/user.go b/user/rpc/user.go deleted file mode 100644 index ec87d72..0000000 --- a/user/rpc/user.go +++ /dev/null @@ -1,37 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! -// Source: user.proto - -package main - -import ( - "flag" - "fmt" - - "datacenter/user/rpc/internal/config" - "datacenter/user/rpc/internal/server" - "datacenter/user/rpc/internal/svc" - "datacenter/user/rpc/user" - - "github.com/zeromicro/go-zero/core/conf" - "github.com/zeromicro/go-zero/zrpc" - "google.golang.org/grpc" -) - -var configFile = flag.String("f", "etc/user.yaml", "the config file") - -func main() { - flag.Parse() - - var c config.Config - conf.MustLoad(*configFile, &c) - ctx := svc.NewServiceContext(c) - srv := server.NewUserServer(ctx) - - s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { - user.RegisterUserServer(grpcServer, srv) - }) - defer s.Stop() - - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) - s.Start() -} diff --git a/user/rpc/user/user.pb.go b/user/rpc/user/user.pb.go index a5618e0..fda4169 100644 --- a/user/rpc/user/user.pb.go +++ b/user/rpc/user/user.pb.go @@ -76,7 +76,7 @@ func (x *Request) GetPing() string { return "" } -//注册请求 +// 注册请求 type RegisterReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -140,7 +140,7 @@ func (x *RegisterReq) GetSmscode() string { return "" } -//登陆请求 +// 登陆请求 type LoginReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -204,7 +204,7 @@ func (x *LoginReq) GetPassword() string { return "" } -//返回用户信息 +// 返回用户信息 type UserReply struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -324,7 +324,7 @@ func (x *UserReply) GetJwttoken() *JwtToken { return nil } -//返回用户信息 +// 返回用户信息 type UserReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -459,7 +459,7 @@ func (x *JwtToken) GetRefreshAfter() int64 { return 0 } -//请求的api +// 请求的api type AppConfigReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -523,7 +523,7 @@ func (x *AppConfigReq) GetCode() string { return "" } -//返回用户的信息 +// 返回用户的信息 type AppUserResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -778,7 +778,7 @@ func (x *AppUserLoginResp) GetJwttoken() *JwtToken { return nil } -//返回的值 +// 返回的值 type AppConfigResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache diff --git a/votes.api b/votes.api index 4b16d53..9b3c2e3 100644 --- a/votes.api +++ b/votes.api @@ -1,17 +1,18 @@ +syntax = "v1" // 投票活动api -type Actid struct { - Actid int64 `json:"actid"` //活动id +type Actid { + Actid int64 `json:"actid,optional" form:"actid,optional"` //活动id } -type VoteReq struct { - Aeid int64 `json:"aeid"` // 作品id +type VoteReq { + Aeid int64 `json:"aeid,optional" form:"aeid,optional"` // 作品id Actid } -type VoteResp struct { +type VoteResp { VoteReq Votecount int64 `json:"votecount"` //投票票数 Viewcount int64 `json:"viewcount"` //浏览数 @@ -20,7 +21,7 @@ type VoteResp struct { // 活动返回的参数 -type ActivityResp struct { +type ActivityResp { Actid int64 `json:"actid"` Title string `json:"title"` //活动名称 Descr string `json:"descr"` //活动描述 @@ -35,7 +36,7 @@ type ActivityResp struct { //报名 -type EnrollReq struct { +type EnrollReq { Actid Name string `json:"name"` // 名称 Address string `json:"address"` //地址 @@ -44,7 +45,7 @@ type EnrollReq struct { } // 作品返回 -type EnrollResp struct { +type EnrollResp { Actid Aeid int64 `json:"aeid"` // 作品id Name string `json:"name"` // 名称 diff --git a/votes/model/appenrollmodel.go b/votes/model/appenrollmodel.go index 3e95a46..f7a6eb7 100755 --- a/votes/model/appenrollmodel.go +++ b/votes/model/appenrollmodel.go @@ -144,7 +144,7 @@ func (m *defaultAppEnrollModel) IncrVotes(aeid int64) error { return err } -//浏览量 +1 +// 浏览量 +1 func (m *defaultAppEnrollModel) IncrView(aeid int64) error { appEnrollAeidKey := fmt.Sprintf("%s%v", cacheAppEnrollAeidPrefix, aeid) _, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) { diff --git a/votes/model/appvotesactivitymodel.go b/votes/model/appvotesactivitymodel.go index f4de3ee..295160f 100755 --- a/votes/model/appvotesactivitymodel.go +++ b/votes/model/appvotesactivitymodel.go @@ -74,7 +74,7 @@ func (m *defaultAppVotesActivityModel) IncrVotes(actid int64) error { return err } -//浏览量 +1 +// 浏览量 +1 func (m *defaultAppVotesActivityModel) IncrView(actid int64) error { appVotesActivityActidKey := fmt.Sprintf("%s%v", cacheAppVotesActivityActidPrefix, actid) _, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) { @@ -84,7 +84,7 @@ func (m *defaultAppVotesActivityModel) IncrView(actid int64) error { return err } -//报名数 +1 +// 报名数 +1 func (m *defaultAppVotesActivityModel) IncrEnroll(actid int64) error { appVotesActivityActidKey := fmt.Sprintf("%s%v", cacheAppVotesActivityActidPrefix, actid) _, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) { diff --git a/votes/model/appvotesmodel.go b/votes/model/appvotesmodel.go index d96a718..3f14f2c 100755 --- a/votes/model/appvotesmodel.go +++ b/votes/model/appvotesmodel.go @@ -80,7 +80,7 @@ func (m *defaultAppVotesModel) FindByDaysWithVotes(actid, uid, auid int64, date * auid snsid */ func (m *defaultAppVotesModel) FindByNumWithVotes(actid, uid, auid int64) (int64, error) { - query := fmt.Sprintf("select count(*) as count from %s where actid = ? AND uid = ? AND auid = ? limit 1", appVotesRows, m.table) + query := fmt.Sprintf("select count(*) as count from %s where actid = ? AND uid = ? AND auid = ? limit 1", m.table) var count int64 err := m.conn.QueryRowPartial(&count, query, actid, uid, auid) if err != nil { diff --git a/votes/rpc/internal/svc/servicecontext.go b/votes/rpc/internal/svc/servicecontext.go index ae6e371..aec4d5d 100644 --- a/votes/rpc/internal/svc/servicecontext.go +++ b/votes/rpc/internal/svc/servicecontext.go @@ -21,7 +21,7 @@ func NewServiceContext(c config.Config) *ServiceContext { aem := model.NewAppEnrollModel(conn, c.CacheRedis) avm := model.NewAppVotesModel(conn) avam := model.NewAppVotesActivityModel(conn, c.CacheRedis) - rconn := redis.NewRedis(c.CacheRedis[0].Host, c.CacheRedis[0].Type, c.CacheRedis[0].Pass) + rconn := redis.MustNewRedis(c.CacheRedis[0].RedisConf) return &ServiceContext{ c: c, AppEnrollModel: aem, diff --git a/votes/rpc/votes.go b/votes/rpc/votes.go deleted file mode 100644 index ce54a43..0000000 --- a/votes/rpc/votes.go +++ /dev/null @@ -1,37 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! -// Source: votes.proto - -package main - -import ( - "flag" - "fmt" - - "datacenter/votes/rpc/internal/config" - "datacenter/votes/rpc/internal/server" - "datacenter/votes/rpc/internal/svc" - "datacenter/votes/rpc/votes" - - "github.com/zeromicro/go-zero/core/conf" - "github.com/zeromicro/go-zero/zrpc" - "google.golang.org/grpc" -) - -var configFile = flag.String("f", "etc/votes.yaml", "the config file") - -func main() { - flag.Parse() - - var c config.Config - conf.MustLoad(*configFile, &c) - ctx := svc.NewServiceContext(c) - srv := server.NewVotesServer(ctx) - - s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { - votes.RegisterVotesServer(grpcServer, srv) - }) - defer s.Stop() - - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) - s.Start() -} diff --git a/votes/rpc/votes/votes.pb.go b/votes/rpc/votes/votes.pb.go index 9ac3f8c..67f7c57 100644 --- a/votes/rpc/votes/votes.pb.go +++ b/votes/rpc/votes/votes.pb.go @@ -249,7 +249,7 @@ func (x *EnrollInfoReq) GetActid() int64 { return 0 } -//报名 请求 +// 报名 请求 type EnrollReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -361,7 +361,7 @@ func (x *EnrollReq) GetActid() int64 { return 0 } -//报名返回 +// 报名返回 type EnrollResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -497,7 +497,7 @@ func (x *EnrollResp) GetActid() int64 { return 0 } -//返回列表 +// 返回列表 type EnrollListResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -545,7 +545,7 @@ func (x *EnrollListResp) GetData() []*EnrollResp { return nil } -//投票 +// 投票 type VotesReq struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -641,7 +641,7 @@ func (x *VotesReq) GetIp() string { return "" } -//返回票数 +// 返回票数 type VotesResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -745,7 +745,7 @@ func (x *VotesResp) GetViewcount() int64 { return 0 } -//返回活动信息 +// 返回活动信息 type ActInfoResp struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache From 15408ad72f430cffa4ed15bfdf7cd0baff1109a8 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 11:38:11 +0800 Subject: [PATCH 02/12] =?UTF-8?q?Phase=201=20=E9=AA=A8=E6=9E=B6=EF=BC=9Ada?= =?UTF-8?q?tasource/metadata/datasvc/catalog=20=E5=9B=9B=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 4 个 proto 契约定稿(数据源/元数据/数据服务API/资产目录) - goctl 1.10.2 生成 rpc 骨架(pb+_grpc 分离,config/example yaml 就绪) - docs/schema/phase1.sql:9 张 dc_ 前缀表 DDL(utf8mb4,beid 多租户) - docs/PHASE1.md:并行开发契约(目录/端口/架构约定/交付清单) --- .gitignore | 4 + catalog/rpc/catalog.go | 39 + catalog/rpc/catalog.proto | 102 ++ catalog/rpc/catalog/catalog.pb.go | 1202 +++++++++++++++ catalog/rpc/catalog/catalog_grpc.pb.go | 363 +++++ catalog/rpc/catalogClient/catalog.go | 101 ++ catalog/rpc/etc/catalog.example.yaml | 8 + catalog/rpc/internal/config/config.go | 10 + catalog/rpc/internal/logic/applyAssetLogic.go | 31 + .../rpc/internal/logic/createCategoryLogic.go | 31 + .../rpc/internal/logic/listAppliesLogic.go | 31 + catalog/rpc/internal/logic/listAssetsLogic.go | 31 + .../rpc/internal/logic/listCategoriesLogic.go | 31 + .../rpc/internal/logic/reviewApplyLogic.go | 31 + catalog/rpc/internal/logic/tagAssetLogic.go | 31 + catalog/rpc/internal/server/catalogServer.go | 66 + catalog/rpc/internal/svc/serviceContext.go | 13 + datasource/rpc/dataSourceClient/dataSource.go | 88 ++ datasource/rpc/datasource.go | 39 + datasource/rpc/datasource.proto | 91 ++ datasource/rpc/datasource/datasource.pb.go | 968 ++++++++++++ .../rpc/datasource/datasource_grpc.pb.go | 323 ++++ datasource/rpc/etc/datasource.example.yaml | 9 + datasource/rpc/internal/config/config.go | 15 + .../internal/logic/createDataSourceLogic.go | 31 + .../internal/logic/deleteDataSourceLogic.go | 31 + .../rpc/internal/logic/getDataSourceLogic.go | 31 + .../internal/logic/listDataSourcesLogic.go | 31 + .../rpc/internal/logic/testConnectionLogic.go | 31 + .../internal/logic/updateDataSourceLogic.go | 31 + .../rpc/internal/server/dataSourceServer.go | 60 + datasource/rpc/internal/svc/serviceContext.go | 13 + datasvc/rpc/dataSvcClient/dataSvc.go | 123 ++ datasvc/rpc/datasvc.go | 39 + datasvc/rpc/datasvc.proto | 119 ++ datasvc/rpc/datasvc/datasvc.pb.go | 1313 ++++++++++++++++ datasvc/rpc/datasvc/datasvc_grpc.pb.go | 481 ++++++ datasvc/rpc/etc/datasvc.example.yaml | 13 + datasvc/rpc/internal/config/config.go | 15 + datasvc/rpc/internal/logic/createApiLogic.go | 31 + datasvc/rpc/internal/logic/deleteApiLogic.go | 31 + datasvc/rpc/internal/logic/executeApiLogic.go | 31 + datasvc/rpc/internal/logic/getApiLogic.go | 31 + datasvc/rpc/internal/logic/grantApiLogic.go | 31 + datasvc/rpc/internal/logic/listApisLogic.go | 31 + datasvc/rpc/internal/logic/offlineApiLogic.go | 30 + datasvc/rpc/internal/logic/publishApiLogic.go | 31 + .../rpc/internal/logic/registerAppLogic.go | 31 + datasvc/rpc/internal/logic/updateApiLogic.go | 31 + datasvc/rpc/internal/server/dataSvcServer.go | 83 ++ datasvc/rpc/internal/svc/serviceContext.go | 13 + docs/PHASE1.md | 36 + docs/schema/phase1.sql | 166 +++ metadata/rpc/etc/metadata.example.yaml | 8 + metadata/rpc/internal/config/config.go | 10 + .../rpc/internal/logic/addLineageLogic.go | 31 + .../internal/logic/collectDatabaseLogic.go | 31 + .../internal/logic/compareVersionsLogic.go | 31 + .../rpc/internal/logic/createVersionLogic.go | 31 + metadata/rpc/internal/logic/getEntityLogic.go | 31 + .../rpc/internal/logic/getLineageLogic.go | 31 + .../rpc/internal/logic/listEntitiesLogic.go | 31 + .../rpc/internal/logic/listVersionsLogic.go | 31 + .../rpc/internal/server/metadataServer.go | 72 + metadata/rpc/internal/svc/serviceContext.go | 13 + metadata/rpc/metadata.go | 39 + metadata/rpc/metadata.proto | 118 ++ metadata/rpc/metadata/metadata.pb.go | 1325 +++++++++++++++++ metadata/rpc/metadata/metadata_grpc.pb.go | 403 +++++ metadata/rpc/metadataClient/metadata.go | 112 ++ 70 files changed, 8975 insertions(+) create mode 100644 catalog/rpc/catalog.go create mode 100644 catalog/rpc/catalog.proto create mode 100644 catalog/rpc/catalog/catalog.pb.go create mode 100644 catalog/rpc/catalog/catalog_grpc.pb.go create mode 100644 catalog/rpc/catalogClient/catalog.go create mode 100644 catalog/rpc/etc/catalog.example.yaml create mode 100755 catalog/rpc/internal/config/config.go create mode 100644 catalog/rpc/internal/logic/applyAssetLogic.go create mode 100644 catalog/rpc/internal/logic/createCategoryLogic.go create mode 100644 catalog/rpc/internal/logic/listAppliesLogic.go create mode 100644 catalog/rpc/internal/logic/listAssetsLogic.go create mode 100644 catalog/rpc/internal/logic/listCategoriesLogic.go create mode 100644 catalog/rpc/internal/logic/reviewApplyLogic.go create mode 100644 catalog/rpc/internal/logic/tagAssetLogic.go create mode 100644 catalog/rpc/internal/server/catalogServer.go create mode 100644 catalog/rpc/internal/svc/serviceContext.go create mode 100644 datasource/rpc/dataSourceClient/dataSource.go create mode 100644 datasource/rpc/datasource.go create mode 100644 datasource/rpc/datasource.proto create mode 100644 datasource/rpc/datasource/datasource.pb.go create mode 100644 datasource/rpc/datasource/datasource_grpc.pb.go create mode 100644 datasource/rpc/etc/datasource.example.yaml create mode 100755 datasource/rpc/internal/config/config.go create mode 100644 datasource/rpc/internal/logic/createDataSourceLogic.go create mode 100644 datasource/rpc/internal/logic/deleteDataSourceLogic.go create mode 100644 datasource/rpc/internal/logic/getDataSourceLogic.go create mode 100644 datasource/rpc/internal/logic/listDataSourcesLogic.go create mode 100644 datasource/rpc/internal/logic/testConnectionLogic.go create mode 100644 datasource/rpc/internal/logic/updateDataSourceLogic.go create mode 100644 datasource/rpc/internal/server/dataSourceServer.go create mode 100644 datasource/rpc/internal/svc/serviceContext.go create mode 100644 datasvc/rpc/dataSvcClient/dataSvc.go create mode 100644 datasvc/rpc/datasvc.go create mode 100644 datasvc/rpc/datasvc.proto create mode 100644 datasvc/rpc/datasvc/datasvc.pb.go create mode 100644 datasvc/rpc/datasvc/datasvc_grpc.pb.go create mode 100644 datasvc/rpc/etc/datasvc.example.yaml create mode 100755 datasvc/rpc/internal/config/config.go create mode 100644 datasvc/rpc/internal/logic/createApiLogic.go create mode 100644 datasvc/rpc/internal/logic/deleteApiLogic.go create mode 100644 datasvc/rpc/internal/logic/executeApiLogic.go create mode 100644 datasvc/rpc/internal/logic/getApiLogic.go create mode 100644 datasvc/rpc/internal/logic/grantApiLogic.go create mode 100644 datasvc/rpc/internal/logic/listApisLogic.go create mode 100644 datasvc/rpc/internal/logic/offlineApiLogic.go create mode 100644 datasvc/rpc/internal/logic/publishApiLogic.go create mode 100644 datasvc/rpc/internal/logic/registerAppLogic.go create mode 100644 datasvc/rpc/internal/logic/updateApiLogic.go create mode 100644 datasvc/rpc/internal/server/dataSvcServer.go create mode 100644 datasvc/rpc/internal/svc/serviceContext.go create mode 100644 docs/PHASE1.md create mode 100644 docs/schema/phase1.sql create mode 100644 metadata/rpc/etc/metadata.example.yaml create mode 100755 metadata/rpc/internal/config/config.go create mode 100644 metadata/rpc/internal/logic/addLineageLogic.go create mode 100644 metadata/rpc/internal/logic/collectDatabaseLogic.go create mode 100644 metadata/rpc/internal/logic/compareVersionsLogic.go create mode 100644 metadata/rpc/internal/logic/createVersionLogic.go create mode 100644 metadata/rpc/internal/logic/getEntityLogic.go create mode 100644 metadata/rpc/internal/logic/getLineageLogic.go create mode 100644 metadata/rpc/internal/logic/listEntitiesLogic.go create mode 100644 metadata/rpc/internal/logic/listVersionsLogic.go create mode 100644 metadata/rpc/internal/server/metadataServer.go create mode 100644 metadata/rpc/internal/svc/serviceContext.go create mode 100644 metadata/rpc/metadata.go create mode 100644 metadata/rpc/metadata.proto create mode 100644 metadata/rpc/metadata/metadata.pb.go create mode 100644 metadata/rpc/metadata/metadata_grpc.pb.go create mode 100644 metadata/rpc/metadataClient/metadata.go diff --git a/.gitignore b/.gitignore index 7aca9ee..b214368 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ common/rpc/etc/rpc.yaml votes/rpc/etc/rpc.yaml search/rpc/etc/search.yaml questions/rpc/etc/questions.yaml +datasource/rpc/etc/datasource.yaml +metadata/rpc/etc/metadata.yaml +datasvc/rpc/etc/datasvc.yaml +catalog/rpc/etc/catalog.yaml diff --git a/catalog/rpc/catalog.go b/catalog/rpc/catalog.go new file mode 100644 index 0000000..c843440 --- /dev/null +++ b/catalog/rpc/catalog.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/config" + "datacenter/catalog/rpc/internal/server" + "datacenter/catalog/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/catalog.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + catalog.RegisterCatalogServer(grpcServer, server.NewCatalogServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/catalog/rpc/catalog.proto b/catalog/rpc/catalog.proto new file mode 100644 index 0000000..e0f953d --- /dev/null +++ b/catalog/rpc/catalog.proto @@ -0,0 +1,102 @@ +syntax = "proto3"; + +package catalog; + +option go_package = "./catalog"; + +message AssetCategory { + int64 id = 1; + int64 parent_id = 2; + string name = 3; + int64 beid = 4; +} +message CategoryReply { AssetCategory category = 1; } +message CategoryListReply { repeated AssetCategory list = 1; } + +message Asset { + int64 id = 1; + int64 entity_id = 2; //元数据实体id + int64 category_id = 3; + string name = 4; //冗余快照,避免跨服务联查 + string entity_type = 5; + string comment = 6; + string classification = 7; + string asset_tags_json = 8; + string visibility = 9; //public/private + int64 beid = 10; +} +message AssetReply { Asset asset = 1; } +message AssetListReply { + repeated Asset list = 1; + int64 total = 2; +} + +message TagAssetReq { + int64 entity_id = 1; + int64 category_id = 2; + string name = 3; + string entity_type = 4; + string comment = 5; + string classification = 6; + string asset_tags_json = 7; + string visibility = 8; + int64 beid = 9; +} +message AssetListReq { + int64 beid = 1; + int64 category_id = 2; + string keyword = 3; + int64 page = 4; + int64 size = 5; +} + +message AssetApply { + int64 id = 1; + int64 asset_id = 2; + string reason = 3; + string applicant = 4; + string status = 5; //pending/approved/rejected + string reviewer = 6; + int64 beid = 7; + int64 created_at = 8; +} +message ApplyReply { AssetApply apply = 1; } +message ApplyListReply { repeated AssetApply list = 1; } + +message ApplyAssetReq { + int64 asset_id = 1; + string reason = 2; + string applicant = 3; + int64 beid = 4; +} +message ReviewApplyReq { + int64 id = 1; + bool approve = 2; + string reviewer = 3; + int64 beid = 4; +} +message ApplyListReq { + int64 beid = 1; + string status = 2; + int64 page = 3; + int64 size = 4; +} + +service Catalog { + //创建目录分类 + rpc CreateCategory(AssetCategory) returns (CategoryReply); + //分类树 + rpc ListCategories(IdReq) returns (CategoryListReply); + //登记资产(元数据实体入目录) + rpc TagAsset(TagAssetReq) returns (AssetReply); + //资产列表 + rpc ListAssets(AssetListReq) returns (AssetListReply); + //申请使用 + rpc ApplyAsset(ApplyAssetReq) returns (ApplyReply); + //审批 + rpc ReviewApply(ReviewApplyReq) returns (ApplyReply); + //申请列表 + rpc ListApplies(ApplyListReq) returns (ApplyListReply); +} + +message IdReq { int64 id = 1; int64 beid = 2; } diff --git a/catalog/rpc/catalog/catalog.pb.go b/catalog/rpc/catalog/catalog.pb.go new file mode 100644 index 0000000..d37c9ef --- /dev/null +++ b/catalog/rpc/catalog/catalog.pb.go @@ -0,0 +1,1202 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: catalog.proto + +package catalog + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AssetCategory struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + ParentId int64 `protobuf:"varint,2,opt,name=parent_id,json=parentId,proto3" json:"parent_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Beid int64 `protobuf:"varint,4,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssetCategory) Reset() { + *x = AssetCategory{} + mi := &file_catalog_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssetCategory) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetCategory) ProtoMessage() {} + +func (x *AssetCategory) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssetCategory.ProtoReflect.Descriptor instead. +func (*AssetCategory) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{0} +} + +func (x *AssetCategory) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *AssetCategory) GetParentId() int64 { + if x != nil { + return x.ParentId + } + return 0 +} + +func (x *AssetCategory) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AssetCategory) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type CategoryReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Category *AssetCategory `protobuf:"bytes,1,opt,name=category,proto3" json:"category,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CategoryReply) Reset() { + *x = CategoryReply{} + mi := &file_catalog_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CategoryReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CategoryReply) ProtoMessage() {} + +func (x *CategoryReply) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CategoryReply.ProtoReflect.Descriptor instead. +func (*CategoryReply) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{1} +} + +func (x *CategoryReply) GetCategory() *AssetCategory { + if x != nil { + return x.Category + } + return nil +} + +type CategoryListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*AssetCategory `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CategoryListReply) Reset() { + *x = CategoryListReply{} + mi := &file_catalog_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CategoryListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CategoryListReply) ProtoMessage() {} + +func (x *CategoryListReply) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CategoryListReply.ProtoReflect.Descriptor instead. +func (*CategoryListReply) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{2} +} + +func (x *CategoryListReply) GetList() []*AssetCategory { + if x != nil { + return x.List + } + return nil +} + +type Asset struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + EntityId int64 `protobuf:"varint,2,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` //元数据实体id + CategoryId int64 `protobuf:"varint,3,opt,name=category_id,json=categoryId,proto3" json:"category_id,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` //冗余快照,避免跨服务联查 + EntityType string `protobuf:"bytes,5,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"` + Comment string `protobuf:"bytes,6,opt,name=comment,proto3" json:"comment,omitempty"` + Classification string `protobuf:"bytes,7,opt,name=classification,proto3" json:"classification,omitempty"` + AssetTagsJson string `protobuf:"bytes,8,opt,name=asset_tags_json,json=assetTagsJson,proto3" json:"asset_tags_json,omitempty"` + Visibility string `protobuf:"bytes,9,opt,name=visibility,proto3" json:"visibility,omitempty"` //public/private + Beid int64 `protobuf:"varint,10,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Asset) Reset() { + *x = Asset{} + mi := &file_catalog_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Asset) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Asset) ProtoMessage() {} + +func (x *Asset) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Asset.ProtoReflect.Descriptor instead. +func (*Asset) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{3} +} + +func (x *Asset) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Asset) GetEntityId() int64 { + if x != nil { + return x.EntityId + } + return 0 +} + +func (x *Asset) GetCategoryId() int64 { + if x != nil { + return x.CategoryId + } + return 0 +} + +func (x *Asset) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Asset) GetEntityType() string { + if x != nil { + return x.EntityType + } + return "" +} + +func (x *Asset) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +func (x *Asset) GetClassification() string { + if x != nil { + return x.Classification + } + return "" +} + +func (x *Asset) GetAssetTagsJson() string { + if x != nil { + return x.AssetTagsJson + } + return "" +} + +func (x *Asset) GetVisibility() string { + if x != nil { + return x.Visibility + } + return "" +} + +func (x *Asset) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type AssetReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Asset *Asset `protobuf:"bytes,1,opt,name=asset,proto3" json:"asset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssetReply) Reset() { + *x = AssetReply{} + mi := &file_catalog_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssetReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetReply) ProtoMessage() {} + +func (x *AssetReply) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssetReply.ProtoReflect.Descriptor instead. +func (*AssetReply) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{4} +} + +func (x *AssetReply) GetAsset() *Asset { + if x != nil { + return x.Asset + } + return nil +} + +type AssetListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*Asset `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssetListReply) Reset() { + *x = AssetListReply{} + mi := &file_catalog_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssetListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetListReply) ProtoMessage() {} + +func (x *AssetListReply) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssetListReply.ProtoReflect.Descriptor instead. +func (*AssetListReply) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{5} +} + +func (x *AssetListReply) GetList() []*Asset { + if x != nil { + return x.List + } + return nil +} + +func (x *AssetListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type TagAssetReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId int64 `protobuf:"varint,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + CategoryId int64 `protobuf:"varint,2,opt,name=category_id,json=categoryId,proto3" json:"category_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + EntityType string `protobuf:"bytes,4,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"` + Comment string `protobuf:"bytes,5,opt,name=comment,proto3" json:"comment,omitempty"` + Classification string `protobuf:"bytes,6,opt,name=classification,proto3" json:"classification,omitempty"` + AssetTagsJson string `protobuf:"bytes,7,opt,name=asset_tags_json,json=assetTagsJson,proto3" json:"asset_tags_json,omitempty"` + Visibility string `protobuf:"bytes,8,opt,name=visibility,proto3" json:"visibility,omitempty"` + Beid int64 `protobuf:"varint,9,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagAssetReq) Reset() { + *x = TagAssetReq{} + mi := &file_catalog_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagAssetReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagAssetReq) ProtoMessage() {} + +func (x *TagAssetReq) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagAssetReq.ProtoReflect.Descriptor instead. +func (*TagAssetReq) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{6} +} + +func (x *TagAssetReq) GetEntityId() int64 { + if x != nil { + return x.EntityId + } + return 0 +} + +func (x *TagAssetReq) GetCategoryId() int64 { + if x != nil { + return x.CategoryId + } + return 0 +} + +func (x *TagAssetReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TagAssetReq) GetEntityType() string { + if x != nil { + return x.EntityType + } + return "" +} + +func (x *TagAssetReq) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +func (x *TagAssetReq) GetClassification() string { + if x != nil { + return x.Classification + } + return "" +} + +func (x *TagAssetReq) GetAssetTagsJson() string { + if x != nil { + return x.AssetTagsJson + } + return "" +} + +func (x *TagAssetReq) GetVisibility() string { + if x != nil { + return x.Visibility + } + return "" +} + +func (x *TagAssetReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type AssetListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + CategoryId int64 `protobuf:"varint,2,opt,name=category_id,json=categoryId,proto3" json:"category_id,omitempty"` + Keyword string `protobuf:"bytes,3,opt,name=keyword,proto3" json:"keyword,omitempty"` + Page int64 `protobuf:"varint,4,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssetListReq) Reset() { + *x = AssetListReq{} + mi := &file_catalog_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssetListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetListReq) ProtoMessage() {} + +func (x *AssetListReq) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssetListReq.ProtoReflect.Descriptor instead. +func (*AssetListReq) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{7} +} + +func (x *AssetListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *AssetListReq) GetCategoryId() int64 { + if x != nil { + return x.CategoryId + } + return 0 +} + +func (x *AssetListReq) GetKeyword() string { + if x != nil { + return x.Keyword + } + return "" +} + +func (x *AssetListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *AssetListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type AssetApply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + AssetId int64 `protobuf:"varint,2,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` + Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + Applicant string `protobuf:"bytes,4,opt,name=applicant,proto3" json:"applicant,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` //pending/approved/rejected + Reviewer string `protobuf:"bytes,6,opt,name=reviewer,proto3" json:"reviewer,omitempty"` + Beid int64 `protobuf:"varint,7,opt,name=beid,proto3" json:"beid,omitempty"` + CreatedAt int64 `protobuf:"varint,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssetApply) Reset() { + *x = AssetApply{} + mi := &file_catalog_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssetApply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetApply) ProtoMessage() {} + +func (x *AssetApply) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssetApply.ProtoReflect.Descriptor instead. +func (*AssetApply) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{8} +} + +func (x *AssetApply) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *AssetApply) GetAssetId() int64 { + if x != nil { + return x.AssetId + } + return 0 +} + +func (x *AssetApply) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *AssetApply) GetApplicant() string { + if x != nil { + return x.Applicant + } + return "" +} + +func (x *AssetApply) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *AssetApply) GetReviewer() string { + if x != nil { + return x.Reviewer + } + return "" +} + +func (x *AssetApply) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *AssetApply) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +type ApplyReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Apply *AssetApply `protobuf:"bytes,1,opt,name=apply,proto3" json:"apply,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyReply) Reset() { + *x = ApplyReply{} + mi := &file_catalog_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyReply) ProtoMessage() {} + +func (x *ApplyReply) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyReply.ProtoReflect.Descriptor instead. +func (*ApplyReply) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{9} +} + +func (x *ApplyReply) GetApply() *AssetApply { + if x != nil { + return x.Apply + } + return nil +} + +type ApplyListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*AssetApply `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyListReply) Reset() { + *x = ApplyListReply{} + mi := &file_catalog_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyListReply) ProtoMessage() {} + +func (x *ApplyListReply) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyListReply.ProtoReflect.Descriptor instead. +func (*ApplyListReply) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{10} +} + +func (x *ApplyListReply) GetList() []*AssetApply { + if x != nil { + return x.List + } + return nil +} + +type ApplyAssetReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + Applicant string `protobuf:"bytes,3,opt,name=applicant,proto3" json:"applicant,omitempty"` + Beid int64 `protobuf:"varint,4,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyAssetReq) Reset() { + *x = ApplyAssetReq{} + mi := &file_catalog_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyAssetReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyAssetReq) ProtoMessage() {} + +func (x *ApplyAssetReq) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyAssetReq.ProtoReflect.Descriptor instead. +func (*ApplyAssetReq) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{11} +} + +func (x *ApplyAssetReq) GetAssetId() int64 { + if x != nil { + return x.AssetId + } + return 0 +} + +func (x *ApplyAssetReq) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *ApplyAssetReq) GetApplicant() string { + if x != nil { + return x.Applicant + } + return "" +} + +func (x *ApplyAssetReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ReviewApplyReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Approve bool `protobuf:"varint,2,opt,name=approve,proto3" json:"approve,omitempty"` + Reviewer string `protobuf:"bytes,3,opt,name=reviewer,proto3" json:"reviewer,omitempty"` + Beid int64 `protobuf:"varint,4,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReviewApplyReq) Reset() { + *x = ReviewApplyReq{} + mi := &file_catalog_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReviewApplyReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReviewApplyReq) ProtoMessage() {} + +func (x *ReviewApplyReq) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReviewApplyReq.ProtoReflect.Descriptor instead. +func (*ReviewApplyReq) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{12} +} + +func (x *ReviewApplyReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ReviewApplyReq) GetApprove() bool { + if x != nil { + return x.Approve + } + return false +} + +func (x *ReviewApplyReq) GetReviewer() string { + if x != nil { + return x.Reviewer + } + return "" +} + +func (x *ReviewApplyReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ApplyListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyListReq) Reset() { + *x = ApplyListReq{} + mi := &file_catalog_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyListReq) ProtoMessage() {} + +func (x *ApplyListReq) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyListReq.ProtoReflect.Descriptor instead. +func (*ApplyListReq) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{13} +} + +func (x *ApplyListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *ApplyListReq) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ApplyListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ApplyListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_catalog_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_catalog_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_catalog_proto_rawDescGZIP(), []int{14} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +var File_catalog_proto protoreflect.FileDescriptor + +const file_catalog_proto_rawDesc = "" + + "\n" + + "\rcatalog.proto\x12\acatalog\"d\n" + + "\rAssetCategory\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1b\n" + + "\tparent_id\x18\x02 \x01(\x03R\bparentId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" + + "\x04beid\x18\x04 \x01(\x03R\x04beid\"C\n" + + "\rCategoryReply\x122\n" + + "\bcategory\x18\x01 \x01(\v2\x16.catalog.AssetCategoryR\bcategory\"?\n" + + "\x11CategoryListReply\x12*\n" + + "\x04list\x18\x01 \x03(\v2\x16.catalog.AssetCategoryR\x04list\"\xa8\x02\n" + + "\x05Asset\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1b\n" + + "\tentity_id\x18\x02 \x01(\x03R\bentityId\x12\x1f\n" + + "\vcategory_id\x18\x03 \x01(\x03R\n" + + "categoryId\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12\x1f\n" + + "\ventity_type\x18\x05 \x01(\tR\n" + + "entityType\x12\x18\n" + + "\acomment\x18\x06 \x01(\tR\acomment\x12&\n" + + "\x0eclassification\x18\a \x01(\tR\x0eclassification\x12&\n" + + "\x0fasset_tags_json\x18\b \x01(\tR\rassetTagsJson\x12\x1e\n" + + "\n" + + "visibility\x18\t \x01(\tR\n" + + "visibility\x12\x12\n" + + "\x04beid\x18\n" + + " \x01(\x03R\x04beid\"2\n" + + "\n" + + "AssetReply\x12$\n" + + "\x05asset\x18\x01 \x01(\v2\x0e.catalog.AssetR\x05asset\"J\n" + + "\x0eAssetListReply\x12\"\n" + + "\x04list\x18\x01 \x03(\v2\x0e.catalog.AssetR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\x9e\x02\n" + + "\vTagAssetReq\x12\x1b\n" + + "\tentity_id\x18\x01 \x01(\x03R\bentityId\x12\x1f\n" + + "\vcategory_id\x18\x02 \x01(\x03R\n" + + "categoryId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x1f\n" + + "\ventity_type\x18\x04 \x01(\tR\n" + + "entityType\x12\x18\n" + + "\acomment\x18\x05 \x01(\tR\acomment\x12&\n" + + "\x0eclassification\x18\x06 \x01(\tR\x0eclassification\x12&\n" + + "\x0fasset_tags_json\x18\a \x01(\tR\rassetTagsJson\x12\x1e\n" + + "\n" + + "visibility\x18\b \x01(\tR\n" + + "visibility\x12\x12\n" + + "\x04beid\x18\t \x01(\x03R\x04beid\"\x85\x01\n" + + "\fAssetListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x1f\n" + + "\vcategory_id\x18\x02 \x01(\x03R\n" + + "categoryId\x12\x18\n" + + "\akeyword\x18\x03 \x01(\tR\akeyword\x12\x12\n" + + "\x04page\x18\x04 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x05 \x01(\x03R\x04size\"\xd4\x01\n" + + "\n" + + "AssetApply\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x19\n" + + "\basset_id\x18\x02 \x01(\x03R\aassetId\x12\x16\n" + + "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x1c\n" + + "\tapplicant\x18\x04 \x01(\tR\tapplicant\x12\x16\n" + + "\x06status\x18\x05 \x01(\tR\x06status\x12\x1a\n" + + "\breviewer\x18\x06 \x01(\tR\breviewer\x12\x12\n" + + "\x04beid\x18\a \x01(\x03R\x04beid\x12\x1d\n" + + "\n" + + "created_at\x18\b \x01(\x03R\tcreatedAt\"7\n" + + "\n" + + "ApplyReply\x12)\n" + + "\x05apply\x18\x01 \x01(\v2\x13.catalog.AssetApplyR\x05apply\"9\n" + + "\x0eApplyListReply\x12'\n" + + "\x04list\x18\x01 \x03(\v2\x13.catalog.AssetApplyR\x04list\"t\n" + + "\rApplyAssetReq\x12\x19\n" + + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12\x1c\n" + + "\tapplicant\x18\x03 \x01(\tR\tapplicant\x12\x12\n" + + "\x04beid\x18\x04 \x01(\x03R\x04beid\"j\n" + + "\x0eReviewApplyReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x18\n" + + "\aapprove\x18\x02 \x01(\bR\aapprove\x12\x1a\n" + + "\breviewer\x18\x03 \x01(\tR\breviewer\x12\x12\n" + + "\x04beid\x18\x04 \x01(\x03R\x04beid\"b\n" + + "\fApplyListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid2\xb5\x03\n" + + "\aCatalog\x12@\n" + + "\x0eCreateCategory\x12\x16.catalog.AssetCategory\x1a\x16.catalog.CategoryReply\x12<\n" + + "\x0eListCategories\x12\x0e.catalog.IdReq\x1a\x1a.catalog.CategoryListReply\x125\n" + + "\bTagAsset\x12\x14.catalog.TagAssetReq\x1a\x13.catalog.AssetReply\x12<\n" + + "\n" + + "ListAssets\x12\x15.catalog.AssetListReq\x1a\x17.catalog.AssetListReply\x129\n" + + "\n" + + "ApplyAsset\x12\x16.catalog.ApplyAssetReq\x1a\x13.catalog.ApplyReply\x12;\n" + + "\vReviewApply\x12\x17.catalog.ReviewApplyReq\x1a\x13.catalog.ApplyReply\x12=\n" + + "\vListApplies\x12\x15.catalog.ApplyListReq\x1a\x17.catalog.ApplyListReplyB\vZ\t./catalogb\x06proto3" + +var ( + file_catalog_proto_rawDescOnce sync.Once + file_catalog_proto_rawDescData []byte +) + +func file_catalog_proto_rawDescGZIP() []byte { + file_catalog_proto_rawDescOnce.Do(func() { + file_catalog_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_catalog_proto_rawDesc), len(file_catalog_proto_rawDesc))) + }) + return file_catalog_proto_rawDescData +} + +var file_catalog_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_catalog_proto_goTypes = []any{ + (*AssetCategory)(nil), // 0: catalog.AssetCategory + (*CategoryReply)(nil), // 1: catalog.CategoryReply + (*CategoryListReply)(nil), // 2: catalog.CategoryListReply + (*Asset)(nil), // 3: catalog.Asset + (*AssetReply)(nil), // 4: catalog.AssetReply + (*AssetListReply)(nil), // 5: catalog.AssetListReply + (*TagAssetReq)(nil), // 6: catalog.TagAssetReq + (*AssetListReq)(nil), // 7: catalog.AssetListReq + (*AssetApply)(nil), // 8: catalog.AssetApply + (*ApplyReply)(nil), // 9: catalog.ApplyReply + (*ApplyListReply)(nil), // 10: catalog.ApplyListReply + (*ApplyAssetReq)(nil), // 11: catalog.ApplyAssetReq + (*ReviewApplyReq)(nil), // 12: catalog.ReviewApplyReq + (*ApplyListReq)(nil), // 13: catalog.ApplyListReq + (*IdReq)(nil), // 14: catalog.IdReq +} +var file_catalog_proto_depIdxs = []int32{ + 0, // 0: catalog.CategoryReply.category:type_name -> catalog.AssetCategory + 0, // 1: catalog.CategoryListReply.list:type_name -> catalog.AssetCategory + 3, // 2: catalog.AssetReply.asset:type_name -> catalog.Asset + 3, // 3: catalog.AssetListReply.list:type_name -> catalog.Asset + 8, // 4: catalog.ApplyReply.apply:type_name -> catalog.AssetApply + 8, // 5: catalog.ApplyListReply.list:type_name -> catalog.AssetApply + 0, // 6: catalog.Catalog.CreateCategory:input_type -> catalog.AssetCategory + 14, // 7: catalog.Catalog.ListCategories:input_type -> catalog.IdReq + 6, // 8: catalog.Catalog.TagAsset:input_type -> catalog.TagAssetReq + 7, // 9: catalog.Catalog.ListAssets:input_type -> catalog.AssetListReq + 11, // 10: catalog.Catalog.ApplyAsset:input_type -> catalog.ApplyAssetReq + 12, // 11: catalog.Catalog.ReviewApply:input_type -> catalog.ReviewApplyReq + 13, // 12: catalog.Catalog.ListApplies:input_type -> catalog.ApplyListReq + 1, // 13: catalog.Catalog.CreateCategory:output_type -> catalog.CategoryReply + 2, // 14: catalog.Catalog.ListCategories:output_type -> catalog.CategoryListReply + 4, // 15: catalog.Catalog.TagAsset:output_type -> catalog.AssetReply + 5, // 16: catalog.Catalog.ListAssets:output_type -> catalog.AssetListReply + 9, // 17: catalog.Catalog.ApplyAsset:output_type -> catalog.ApplyReply + 9, // 18: catalog.Catalog.ReviewApply:output_type -> catalog.ApplyReply + 10, // 19: catalog.Catalog.ListApplies:output_type -> catalog.ApplyListReply + 13, // [13:20] is the sub-list for method output_type + 6, // [6:13] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_catalog_proto_init() } +func file_catalog_proto_init() { + if File_catalog_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_catalog_proto_rawDesc), len(file_catalog_proto_rawDesc)), + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_catalog_proto_goTypes, + DependencyIndexes: file_catalog_proto_depIdxs, + MessageInfos: file_catalog_proto_msgTypes, + }.Build() + File_catalog_proto = out.File + file_catalog_proto_goTypes = nil + file_catalog_proto_depIdxs = nil +} diff --git a/catalog/rpc/catalog/catalog_grpc.pb.go b/catalog/rpc/catalog/catalog_grpc.pb.go new file mode 100644 index 0000000..11076be --- /dev/null +++ b/catalog/rpc/catalog/catalog_grpc.pb.go @@ -0,0 +1,363 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: catalog.proto + +package catalog + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Catalog_CreateCategory_FullMethodName = "/catalog.Catalog/CreateCategory" + Catalog_ListCategories_FullMethodName = "/catalog.Catalog/ListCategories" + Catalog_TagAsset_FullMethodName = "/catalog.Catalog/TagAsset" + Catalog_ListAssets_FullMethodName = "/catalog.Catalog/ListAssets" + Catalog_ApplyAsset_FullMethodName = "/catalog.Catalog/ApplyAsset" + Catalog_ReviewApply_FullMethodName = "/catalog.Catalog/ReviewApply" + Catalog_ListApplies_FullMethodName = "/catalog.Catalog/ListApplies" +) + +// CatalogClient is the client API for Catalog service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type CatalogClient interface { + //创建目录分类 + CreateCategory(ctx context.Context, in *AssetCategory, opts ...grpc.CallOption) (*CategoryReply, error) + //分类树 + ListCategories(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*CategoryListReply, error) + //登记资产(元数据实体入目录) + TagAsset(ctx context.Context, in *TagAssetReq, opts ...grpc.CallOption) (*AssetReply, error) + //资产列表 + ListAssets(ctx context.Context, in *AssetListReq, opts ...grpc.CallOption) (*AssetListReply, error) + //申请使用 + ApplyAsset(ctx context.Context, in *ApplyAssetReq, opts ...grpc.CallOption) (*ApplyReply, error) + //审批 + ReviewApply(ctx context.Context, in *ReviewApplyReq, opts ...grpc.CallOption) (*ApplyReply, error) + //申请列表 + ListApplies(ctx context.Context, in *ApplyListReq, opts ...grpc.CallOption) (*ApplyListReply, error) +} + +type catalogClient struct { + cc grpc.ClientConnInterface +} + +func NewCatalogClient(cc grpc.ClientConnInterface) CatalogClient { + return &catalogClient{cc} +} + +func (c *catalogClient) CreateCategory(ctx context.Context, in *AssetCategory, opts ...grpc.CallOption) (*CategoryReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CategoryReply) + err := c.cc.Invoke(ctx, Catalog_CreateCategory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *catalogClient) ListCategories(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*CategoryListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CategoryListReply) + err := c.cc.Invoke(ctx, Catalog_ListCategories_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *catalogClient) TagAsset(ctx context.Context, in *TagAssetReq, opts ...grpc.CallOption) (*AssetReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AssetReply) + err := c.cc.Invoke(ctx, Catalog_TagAsset_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *catalogClient) ListAssets(ctx context.Context, in *AssetListReq, opts ...grpc.CallOption) (*AssetListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AssetListReply) + err := c.cc.Invoke(ctx, Catalog_ListAssets_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *catalogClient) ApplyAsset(ctx context.Context, in *ApplyAssetReq, opts ...grpc.CallOption) (*ApplyReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApplyReply) + err := c.cc.Invoke(ctx, Catalog_ApplyAsset_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *catalogClient) ReviewApply(ctx context.Context, in *ReviewApplyReq, opts ...grpc.CallOption) (*ApplyReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApplyReply) + err := c.cc.Invoke(ctx, Catalog_ReviewApply_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *catalogClient) ListApplies(ctx context.Context, in *ApplyListReq, opts ...grpc.CallOption) (*ApplyListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApplyListReply) + err := c.cc.Invoke(ctx, Catalog_ListApplies_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CatalogServer is the server API for Catalog service. +// All implementations must embed UnimplementedCatalogServer +// for forward compatibility. +type CatalogServer interface { + //创建目录分类 + CreateCategory(context.Context, *AssetCategory) (*CategoryReply, error) + //分类树 + ListCategories(context.Context, *IdReq) (*CategoryListReply, error) + //登记资产(元数据实体入目录) + TagAsset(context.Context, *TagAssetReq) (*AssetReply, error) + //资产列表 + ListAssets(context.Context, *AssetListReq) (*AssetListReply, error) + //申请使用 + ApplyAsset(context.Context, *ApplyAssetReq) (*ApplyReply, error) + //审批 + ReviewApply(context.Context, *ReviewApplyReq) (*ApplyReply, error) + //申请列表 + ListApplies(context.Context, *ApplyListReq) (*ApplyListReply, error) + mustEmbedUnimplementedCatalogServer() +} + +// UnimplementedCatalogServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedCatalogServer struct{} + +func (UnimplementedCatalogServer) CreateCategory(context.Context, *AssetCategory) (*CategoryReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateCategory not implemented") +} +func (UnimplementedCatalogServer) ListCategories(context.Context, *IdReq) (*CategoryListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListCategories not implemented") +} +func (UnimplementedCatalogServer) TagAsset(context.Context, *TagAssetReq) (*AssetReply, error) { + return nil, status.Error(codes.Unimplemented, "method TagAsset not implemented") +} +func (UnimplementedCatalogServer) ListAssets(context.Context, *AssetListReq) (*AssetListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListAssets not implemented") +} +func (UnimplementedCatalogServer) ApplyAsset(context.Context, *ApplyAssetReq) (*ApplyReply, error) { + return nil, status.Error(codes.Unimplemented, "method ApplyAsset not implemented") +} +func (UnimplementedCatalogServer) ReviewApply(context.Context, *ReviewApplyReq) (*ApplyReply, error) { + return nil, status.Error(codes.Unimplemented, "method ReviewApply not implemented") +} +func (UnimplementedCatalogServer) ListApplies(context.Context, *ApplyListReq) (*ApplyListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListApplies not implemented") +} +func (UnimplementedCatalogServer) mustEmbedUnimplementedCatalogServer() {} +func (UnimplementedCatalogServer) testEmbeddedByValue() {} + +// UnsafeCatalogServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CatalogServer will +// result in compilation errors. +type UnsafeCatalogServer interface { + mustEmbedUnimplementedCatalogServer() +} + +func RegisterCatalogServer(s grpc.ServiceRegistrar, srv CatalogServer) { + // If the following call panics, it indicates UnimplementedCatalogServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Catalog_ServiceDesc, srv) +} + +func _Catalog_CreateCategory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AssetCategory) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CatalogServer).CreateCategory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Catalog_CreateCategory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CatalogServer).CreateCategory(ctx, req.(*AssetCategory)) + } + return interceptor(ctx, in, info, handler) +} + +func _Catalog_ListCategories_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CatalogServer).ListCategories(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Catalog_ListCategories_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CatalogServer).ListCategories(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Catalog_TagAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TagAssetReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CatalogServer).TagAsset(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Catalog_TagAsset_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CatalogServer).TagAsset(ctx, req.(*TagAssetReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Catalog_ListAssets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AssetListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CatalogServer).ListAssets(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Catalog_ListAssets_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CatalogServer).ListAssets(ctx, req.(*AssetListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Catalog_ApplyAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApplyAssetReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CatalogServer).ApplyAsset(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Catalog_ApplyAsset_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CatalogServer).ApplyAsset(ctx, req.(*ApplyAssetReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Catalog_ReviewApply_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReviewApplyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CatalogServer).ReviewApply(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Catalog_ReviewApply_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CatalogServer).ReviewApply(ctx, req.(*ReviewApplyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Catalog_ListApplies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApplyListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CatalogServer).ListApplies(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Catalog_ListApplies_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CatalogServer).ListApplies(ctx, req.(*ApplyListReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Catalog_ServiceDesc is the grpc.ServiceDesc for Catalog service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Catalog_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "catalog.Catalog", + HandlerType: (*CatalogServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateCategory", + Handler: _Catalog_CreateCategory_Handler, + }, + { + MethodName: "ListCategories", + Handler: _Catalog_ListCategories_Handler, + }, + { + MethodName: "TagAsset", + Handler: _Catalog_TagAsset_Handler, + }, + { + MethodName: "ListAssets", + Handler: _Catalog_ListAssets_Handler, + }, + { + MethodName: "ApplyAsset", + Handler: _Catalog_ApplyAsset_Handler, + }, + { + MethodName: "ReviewApply", + Handler: _Catalog_ReviewApply_Handler, + }, + { + MethodName: "ListApplies", + Handler: _Catalog_ListApplies_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "catalog.proto", +} diff --git a/catalog/rpc/catalogClient/catalog.go b/catalog/rpc/catalogClient/catalog.go new file mode 100644 index 0000000..e00c1d7 --- /dev/null +++ b/catalog/rpc/catalogClient/catalog.go @@ -0,0 +1,101 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: catalog.proto + +package catalogClient + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + ApplyAssetReq = catalog.ApplyAssetReq + ApplyListReply = catalog.ApplyListReply + ApplyListReq = catalog.ApplyListReq + ApplyReply = catalog.ApplyReply + Asset = catalog.Asset + AssetApply = catalog.AssetApply + AssetCategory = catalog.AssetCategory + AssetListReply = catalog.AssetListReply + AssetListReq = catalog.AssetListReq + AssetReply = catalog.AssetReply + CategoryListReply = catalog.CategoryListReply + CategoryReply = catalog.CategoryReply + IdReq = catalog.IdReq + ReviewApplyReq = catalog.ReviewApplyReq + TagAssetReq = catalog.TagAssetReq + + Catalog interface { + // 创建目录分类 + CreateCategory(ctx context.Context, in *AssetCategory, opts ...grpc.CallOption) (*CategoryReply, error) + // 分类树 + ListCategories(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*CategoryListReply, error) + // 登记资产(元数据实体入目录) + TagAsset(ctx context.Context, in *TagAssetReq, opts ...grpc.CallOption) (*AssetReply, error) + // 资产列表 + ListAssets(ctx context.Context, in *AssetListReq, opts ...grpc.CallOption) (*AssetListReply, error) + // 申请使用 + ApplyAsset(ctx context.Context, in *ApplyAssetReq, opts ...grpc.CallOption) (*ApplyReply, error) + // 审批 + ReviewApply(ctx context.Context, in *ReviewApplyReq, opts ...grpc.CallOption) (*ApplyReply, error) + // 申请列表 + ListApplies(ctx context.Context, in *ApplyListReq, opts ...grpc.CallOption) (*ApplyListReply, error) + } + + defaultCatalog struct { + cli zrpc.Client + } +) + +func NewCatalog(cli zrpc.Client) Catalog { + return &defaultCatalog{ + cli: cli, + } +} + +// 创建目录分类 +func (m *defaultCatalog) CreateCategory(ctx context.Context, in *AssetCategory, opts ...grpc.CallOption) (*CategoryReply, error) { + client := catalog.NewCatalogClient(m.cli.Conn()) + return client.CreateCategory(ctx, in, opts...) +} + +// 分类树 +func (m *defaultCatalog) ListCategories(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*CategoryListReply, error) { + client := catalog.NewCatalogClient(m.cli.Conn()) + return client.ListCategories(ctx, in, opts...) +} + +// 登记资产(元数据实体入目录) +func (m *defaultCatalog) TagAsset(ctx context.Context, in *TagAssetReq, opts ...grpc.CallOption) (*AssetReply, error) { + client := catalog.NewCatalogClient(m.cli.Conn()) + return client.TagAsset(ctx, in, opts...) +} + +// 资产列表 +func (m *defaultCatalog) ListAssets(ctx context.Context, in *AssetListReq, opts ...grpc.CallOption) (*AssetListReply, error) { + client := catalog.NewCatalogClient(m.cli.Conn()) + return client.ListAssets(ctx, in, opts...) +} + +// 申请使用 +func (m *defaultCatalog) ApplyAsset(ctx context.Context, in *ApplyAssetReq, opts ...grpc.CallOption) (*ApplyReply, error) { + client := catalog.NewCatalogClient(m.cli.Conn()) + return client.ApplyAsset(ctx, in, opts...) +} + +// 审批 +func (m *defaultCatalog) ReviewApply(ctx context.Context, in *ReviewApplyReq, opts ...grpc.CallOption) (*ApplyReply, error) { + client := catalog.NewCatalogClient(m.cli.Conn()) + return client.ReviewApply(ctx, in, opts...) +} + +// 申请列表 +func (m *defaultCatalog) ListApplies(ctx context.Context, in *ApplyListReq, opts ...grpc.CallOption) (*ApplyListReply, error) { + client := catalog.NewCatalogClient(m.cli.Conn()) + return client.ListApplies(ctx, in, opts...) +} diff --git a/catalog/rpc/etc/catalog.example.yaml b/catalog/rpc/etc/catalog.example.yaml new file mode 100644 index 0000000..32628f9 --- /dev/null +++ b/catalog/rpc/etc/catalog.example.yaml @@ -0,0 +1,8 @@ +Name: catalog.rpc +ListenOn: 0.0.0.0:8088 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: catalog.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/catalog/rpc/internal/config/config.go b/catalog/rpc/internal/config/config.go new file mode 100755 index 0000000..96863e9 --- /dev/null +++ b/catalog/rpc/internal/config/config.go @@ -0,0 +1,10 @@ +package config + +import "github.com/zeromicro/go-zero/zrpc" + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } +} diff --git a/catalog/rpc/internal/logic/applyAssetLogic.go b/catalog/rpc/internal/logic/applyAssetLogic.go new file mode 100644 index 0000000..e8c0fad --- /dev/null +++ b/catalog/rpc/internal/logic/applyAssetLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ApplyAssetLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewApplyAssetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApplyAssetLogic { + return &ApplyAssetLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 申请使用 +func (l *ApplyAssetLogic) ApplyAsset(in *catalog.ApplyAssetReq) (*catalog.ApplyReply, error) { + // todo: add your logic here and delete this line + + return &catalog.ApplyReply{}, nil +} diff --git a/catalog/rpc/internal/logic/createCategoryLogic.go b/catalog/rpc/internal/logic/createCategoryLogic.go new file mode 100644 index 0000000..8773974 --- /dev/null +++ b/catalog/rpc/internal/logic/createCategoryLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateCategoryLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateCategoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateCategoryLogic { + return &CreateCategoryLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建目录分类 +func (l *CreateCategoryLogic) CreateCategory(in *catalog.AssetCategory) (*catalog.CategoryReply, error) { + // todo: add your logic here and delete this line + + return &catalog.CategoryReply{}, nil +} diff --git a/catalog/rpc/internal/logic/listAppliesLogic.go b/catalog/rpc/internal/logic/listAppliesLogic.go new file mode 100644 index 0000000..4872a32 --- /dev/null +++ b/catalog/rpc/internal/logic/listAppliesLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListAppliesLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListAppliesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListAppliesLogic { + return &ListAppliesLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 申请列表 +func (l *ListAppliesLogic) ListApplies(in *catalog.ApplyListReq) (*catalog.ApplyListReply, error) { + // todo: add your logic here and delete this line + + return &catalog.ApplyListReply{}, nil +} diff --git a/catalog/rpc/internal/logic/listAssetsLogic.go b/catalog/rpc/internal/logic/listAssetsLogic.go new file mode 100644 index 0000000..40aaf86 --- /dev/null +++ b/catalog/rpc/internal/logic/listAssetsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListAssetsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListAssetsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListAssetsLogic { + return &ListAssetsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 资产列表 +func (l *ListAssetsLogic) ListAssets(in *catalog.AssetListReq) (*catalog.AssetListReply, error) { + // todo: add your logic here and delete this line + + return &catalog.AssetListReply{}, nil +} diff --git a/catalog/rpc/internal/logic/listCategoriesLogic.go b/catalog/rpc/internal/logic/listCategoriesLogic.go new file mode 100644 index 0000000..c4b25a0 --- /dev/null +++ b/catalog/rpc/internal/logic/listCategoriesLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListCategoriesLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListCategoriesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListCategoriesLogic { + return &ListCategoriesLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 分类树 +func (l *ListCategoriesLogic) ListCategories(in *catalog.IdReq) (*catalog.CategoryListReply, error) { + // todo: add your logic here and delete this line + + return &catalog.CategoryListReply{}, nil +} diff --git a/catalog/rpc/internal/logic/reviewApplyLogic.go b/catalog/rpc/internal/logic/reviewApplyLogic.go new file mode 100644 index 0000000..37b59a5 --- /dev/null +++ b/catalog/rpc/internal/logic/reviewApplyLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ReviewApplyLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewReviewApplyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ReviewApplyLogic { + return &ReviewApplyLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 审批 +func (l *ReviewApplyLogic) ReviewApply(in *catalog.ReviewApplyReq) (*catalog.ApplyReply, error) { + // todo: add your logic here and delete this line + + return &catalog.ApplyReply{}, nil +} diff --git a/catalog/rpc/internal/logic/tagAssetLogic.go b/catalog/rpc/internal/logic/tagAssetLogic.go new file mode 100644 index 0000000..96e53ba --- /dev/null +++ b/catalog/rpc/internal/logic/tagAssetLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TagAssetLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewTagAssetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TagAssetLogic { + return &TagAssetLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 登记资产(元数据实体入目录) +func (l *TagAssetLogic) TagAsset(in *catalog.TagAssetReq) (*catalog.AssetReply, error) { + // todo: add your logic here and delete this line + + return &catalog.AssetReply{}, nil +} diff --git a/catalog/rpc/internal/server/catalogServer.go b/catalog/rpc/internal/server/catalogServer.go new file mode 100644 index 0000000..f9c29a1 --- /dev/null +++ b/catalog/rpc/internal/server/catalogServer.go @@ -0,0 +1,66 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: catalog.proto + +package server + +import ( + "context" + + "datacenter/catalog/rpc/catalog" + "datacenter/catalog/rpc/internal/logic" + "datacenter/catalog/rpc/internal/svc" +) + +type CatalogServer struct { + svcCtx *svc.ServiceContext + catalog.UnimplementedCatalogServer +} + +func NewCatalogServer(svcCtx *svc.ServiceContext) *CatalogServer { + return &CatalogServer{ + svcCtx: svcCtx, + } +} + +// 创建目录分类 +func (s *CatalogServer) CreateCategory(ctx context.Context, in *catalog.AssetCategory) (*catalog.CategoryReply, error) { + l := logic.NewCreateCategoryLogic(ctx, s.svcCtx) + return l.CreateCategory(in) +} + +// 分类树 +func (s *CatalogServer) ListCategories(ctx context.Context, in *catalog.IdReq) (*catalog.CategoryListReply, error) { + l := logic.NewListCategoriesLogic(ctx, s.svcCtx) + return l.ListCategories(in) +} + +// 登记资产(元数据实体入目录) +func (s *CatalogServer) TagAsset(ctx context.Context, in *catalog.TagAssetReq) (*catalog.AssetReply, error) { + l := logic.NewTagAssetLogic(ctx, s.svcCtx) + return l.TagAsset(in) +} + +// 资产列表 +func (s *CatalogServer) ListAssets(ctx context.Context, in *catalog.AssetListReq) (*catalog.AssetListReply, error) { + l := logic.NewListAssetsLogic(ctx, s.svcCtx) + return l.ListAssets(in) +} + +// 申请使用 +func (s *CatalogServer) ApplyAsset(ctx context.Context, in *catalog.ApplyAssetReq) (*catalog.ApplyReply, error) { + l := logic.NewApplyAssetLogic(ctx, s.svcCtx) + return l.ApplyAsset(in) +} + +// 审批 +func (s *CatalogServer) ReviewApply(ctx context.Context, in *catalog.ReviewApplyReq) (*catalog.ApplyReply, error) { + l := logic.NewReviewApplyLogic(ctx, s.svcCtx) + return l.ReviewApply(in) +} + +// 申请列表 +func (s *CatalogServer) ListApplies(ctx context.Context, in *catalog.ApplyListReq) (*catalog.ApplyListReply, error) { + l := logic.NewListAppliesLogic(ctx, s.svcCtx) + return l.ListApplies(in) +} diff --git a/catalog/rpc/internal/svc/serviceContext.go b/catalog/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..5fab8a2 --- /dev/null +++ b/catalog/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/catalog/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/datasource/rpc/dataSourceClient/dataSource.go b/datasource/rpc/dataSourceClient/dataSource.go new file mode 100644 index 0000000..8bda5de --- /dev/null +++ b/datasource/rpc/dataSourceClient/dataSource.go @@ -0,0 +1,88 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: datasource.proto + +package dataSourceClient + +import ( + "context" + + "datacenter/datasource/rpc/datasource" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + CreateDataSourceReq = datasource.CreateDataSourceReq + DataSourceInfo = datasource.DataSourceInfo + DataSourceListReply = datasource.DataSourceListReply + DataSourceReply = datasource.DataSourceReply + GetDataSourceReq = datasource.GetDataSourceReq + IdReq = datasource.IdReq + ListReq = datasource.ListReq + OkReply = datasource.OkReply + TestConnReply = datasource.TestConnReply + UpdateDataSourceReq = datasource.UpdateDataSourceReq + + DataSource interface { + // 创建数据源 + CreateDataSource(ctx context.Context, in *CreateDataSourceReq, opts ...grpc.CallOption) (*DataSourceReply, error) + // 更新数据源 + UpdateDataSource(ctx context.Context, in *UpdateDataSourceReq, opts ...grpc.CallOption) (*OkReply, error) + // 删除数据源 + DeleteDataSource(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) + // 数据源详情 + GetDataSource(ctx context.Context, in *GetDataSourceReq, opts ...grpc.CallOption) (*DataSourceReply, error) + // 数据源列表 + ListDataSources(ctx context.Context, in *ListReq, opts ...grpc.CallOption) (*DataSourceListReply, error) + // 测试连接 + TestConnection(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*TestConnReply, error) + } + + defaultDataSource struct { + cli zrpc.Client + } +) + +func NewDataSource(cli zrpc.Client) DataSource { + return &defaultDataSource{ + cli: cli, + } +} + +// 创建数据源 +func (m *defaultDataSource) CreateDataSource(ctx context.Context, in *CreateDataSourceReq, opts ...grpc.CallOption) (*DataSourceReply, error) { + client := datasource.NewDataSourceClient(m.cli.Conn()) + return client.CreateDataSource(ctx, in, opts...) +} + +// 更新数据源 +func (m *defaultDataSource) UpdateDataSource(ctx context.Context, in *UpdateDataSourceReq, opts ...grpc.CallOption) (*OkReply, error) { + client := datasource.NewDataSourceClient(m.cli.Conn()) + return client.UpdateDataSource(ctx, in, opts...) +} + +// 删除数据源 +func (m *defaultDataSource) DeleteDataSource(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) { + client := datasource.NewDataSourceClient(m.cli.Conn()) + return client.DeleteDataSource(ctx, in, opts...) +} + +// 数据源详情 +func (m *defaultDataSource) GetDataSource(ctx context.Context, in *GetDataSourceReq, opts ...grpc.CallOption) (*DataSourceReply, error) { + client := datasource.NewDataSourceClient(m.cli.Conn()) + return client.GetDataSource(ctx, in, opts...) +} + +// 数据源列表 +func (m *defaultDataSource) ListDataSources(ctx context.Context, in *ListReq, opts ...grpc.CallOption) (*DataSourceListReply, error) { + client := datasource.NewDataSourceClient(m.cli.Conn()) + return client.ListDataSources(ctx, in, opts...) +} + +// 测试连接 +func (m *defaultDataSource) TestConnection(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*TestConnReply, error) { + client := datasource.NewDataSourceClient(m.cli.Conn()) + return client.TestConnection(ctx, in, opts...) +} diff --git a/datasource/rpc/datasource.go b/datasource/rpc/datasource.go new file mode 100644 index 0000000..aedfe97 --- /dev/null +++ b/datasource/rpc/datasource.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/datasource/rpc/datasource" + "datacenter/datasource/rpc/internal/config" + "datacenter/datasource/rpc/internal/server" + "datacenter/datasource/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/datasource.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + datasource.RegisterDataSourceServer(grpcServer, server.NewDataSourceServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/datasource/rpc/datasource.proto b/datasource/rpc/datasource.proto new file mode 100644 index 0000000..2cedd6b --- /dev/null +++ b/datasource/rpc/datasource.proto @@ -0,0 +1,91 @@ +syntax = "proto3"; + +package datasource; + +option go_package = "./datasource"; + +//数据源 +message DataSourceInfo { + int64 id = 1; //数据源id + string name = 2; //名称 + string type = 3; //类型: mysql/redis/es/http/kafka + string host = 4; + int32 port = 5; + string db_name = 6; //库名 + string username = 7; + string password_enc = 8; //AES-GCM 加密后的密码 + string params_json = 9; //扩展参数(json) + int32 status = 10; //1.启用 0.停用 + string owner = 11; //负责人 + int64 beid = 12; //平台id + int64 created_at = 13; + int64 updated_at = 14; +} + +message DataSourceReply { + DataSourceInfo datasource = 1; + string dsn = 2; //with_secret=true 时返回拼好的连接串 +} + +message OkReply { bool ok = 1; } +message IdReq { int64 id = 1; int64 beid = 2; } +message ListReq { int64 beid = 1; string type = 2; int64 page = 3; int64 size = 4; } +message DataSourceListReply { + repeated DataSourceInfo list = 1; + int64 total = 2; +} + +message CreateDataSourceReq { + string name = 1; + string type = 2; + string host = 3; + int32 port = 4; + string db_name = 5; + string username = 6; + string password = 7; //明文入参,服务端加密存储 + string params_json = 8; + string owner = 9; + int64 beid = 10; +} + +message UpdateDataSourceReq { + int64 id = 1; + int64 beid = 2; + string name = 3; + string type = 4; + string host = 5; + int32 port = 6; + string db_name = 7; + string username = 8; + string password = 9; //为空则不更新密码 + string params_json = 10; + int32 status = 11; + string owner = 12; +} + +message GetDataSourceReq { + int64 id = 1; + int64 beid = 2; + bool with_secret = 3; //true 时返回解密密码与 dsn(仅内部服务调用) +} + +message TestConnReply { + bool ok = 1; + int64 latency_ms = 2; + string err_msg = 3; +} + +service DataSource { + //创建数据源 + rpc CreateDataSource(CreateDataSourceReq) returns (DataSourceReply); + //更新数据源 + rpc UpdateDataSource(UpdateDataSourceReq) returns (OkReply); + //删除数据源 + rpc DeleteDataSource(IdReq) returns (OkReply); + //数据源详情 + rpc GetDataSource(GetDataSourceReq) returns (DataSourceReply); + //数据源列表 + rpc ListDataSources(ListReq) returns (DataSourceListReply); + //测试连接 + rpc TestConnection(IdReq) returns (TestConnReply); +} diff --git a/datasource/rpc/datasource/datasource.pb.go b/datasource/rpc/datasource/datasource.pb.go new file mode 100644 index 0000000..2da3eff --- /dev/null +++ b/datasource/rpc/datasource/datasource.pb.go @@ -0,0 +1,968 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: datasource.proto + +package datasource + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// 数据源 +type DataSourceInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` //数据源id + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` //名称 + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` //类型: mysql/redis/es/http/kafka + Host string `protobuf:"bytes,4,opt,name=host,proto3" json:"host,omitempty"` + Port int32 `protobuf:"varint,5,opt,name=port,proto3" json:"port,omitempty"` + DbName string `protobuf:"bytes,6,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` //库名 + Username string `protobuf:"bytes,7,opt,name=username,proto3" json:"username,omitempty"` + PasswordEnc string `protobuf:"bytes,8,opt,name=password_enc,json=passwordEnc,proto3" json:"password_enc,omitempty"` //AES-GCM 加密后的密码 + ParamsJson string `protobuf:"bytes,9,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` //扩展参数(json) + Status int32 `protobuf:"varint,10,opt,name=status,proto3" json:"status,omitempty"` //1.启用 0.停用 + Owner string `protobuf:"bytes,11,opt,name=owner,proto3" json:"owner,omitempty"` //负责人 + Beid int64 `protobuf:"varint,12,opt,name=beid,proto3" json:"beid,omitempty"` //平台id + CreatedAt int64 `protobuf:"varint,13,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt int64 `protobuf:"varint,14,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DataSourceInfo) Reset() { + *x = DataSourceInfo{} + mi := &file_datasource_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DataSourceInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataSourceInfo) ProtoMessage() {} + +func (x *DataSourceInfo) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataSourceInfo.ProtoReflect.Descriptor instead. +func (*DataSourceInfo) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{0} +} + +func (x *DataSourceInfo) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *DataSourceInfo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DataSourceInfo) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *DataSourceInfo) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *DataSourceInfo) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *DataSourceInfo) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *DataSourceInfo) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *DataSourceInfo) GetPasswordEnc() string { + if x != nil { + return x.PasswordEnc + } + return "" +} + +func (x *DataSourceInfo) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *DataSourceInfo) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *DataSourceInfo) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *DataSourceInfo) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *DataSourceInfo) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *DataSourceInfo) GetUpdatedAt() int64 { + if x != nil { + return x.UpdatedAt + } + return 0 +} + +type DataSourceReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Datasource *DataSourceInfo `protobuf:"bytes,1,opt,name=datasource,proto3" json:"datasource,omitempty"` + Dsn string `protobuf:"bytes,2,opt,name=dsn,proto3" json:"dsn,omitempty"` //with_secret=true 时返回拼好的连接串 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DataSourceReply) Reset() { + *x = DataSourceReply{} + mi := &file_datasource_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DataSourceReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataSourceReply) ProtoMessage() {} + +func (x *DataSourceReply) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataSourceReply.ProtoReflect.Descriptor instead. +func (*DataSourceReply) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{1} +} + +func (x *DataSourceReply) GetDatasource() *DataSourceInfo { + if x != nil { + return x.Datasource + } + return nil +} + +func (x *DataSourceReply) GetDsn() string { + if x != nil { + return x.Dsn + } + return "" +} + +type OkReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OkReply) Reset() { + *x = OkReply{} + mi := &file_datasource_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OkReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OkReply) ProtoMessage() {} + +func (x *OkReply) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OkReply.ProtoReflect.Descriptor instead. +func (*OkReply) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{2} +} + +func (x *OkReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_datasource_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{3} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListReq) Reset() { + *x = ListReq{} + mi := &file_datasource_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListReq) ProtoMessage() {} + +func (x *ListReq) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListReq.ProtoReflect.Descriptor instead. +func (*ListReq) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{4} +} + +func (x *ListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *ListReq) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *ListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type DataSourceListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*DataSourceInfo `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DataSourceListReply) Reset() { + *x = DataSourceListReply{} + mi := &file_datasource_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DataSourceListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataSourceListReply) ProtoMessage() {} + +func (x *DataSourceListReply) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataSourceListReply.ProtoReflect.Descriptor instead. +func (*DataSourceListReply) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{5} +} + +func (x *DataSourceListReply) GetList() []*DataSourceInfo { + if x != nil { + return x.List + } + return nil +} + +func (x *DataSourceListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type CreateDataSourceReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"` + Port int32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"` + DbName string `protobuf:"bytes,5,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + Username string `protobuf:"bytes,6,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,7,opt,name=password,proto3" json:"password,omitempty"` //明文入参,服务端加密存储 + ParamsJson string `protobuf:"bytes,8,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` + Owner string `protobuf:"bytes,9,opt,name=owner,proto3" json:"owner,omitempty"` + Beid int64 `protobuf:"varint,10,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateDataSourceReq) Reset() { + *x = CreateDataSourceReq{} + mi := &file_datasource_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateDataSourceReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateDataSourceReq) ProtoMessage() {} + +func (x *CreateDataSourceReq) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateDataSourceReq.ProtoReflect.Descriptor instead. +func (*CreateDataSourceReq) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateDataSourceReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateDataSourceReq) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *CreateDataSourceReq) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *CreateDataSourceReq) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *CreateDataSourceReq) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *CreateDataSourceReq) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CreateDataSourceReq) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *CreateDataSourceReq) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *CreateDataSourceReq) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *CreateDataSourceReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type UpdateDataSourceReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + Host string `protobuf:"bytes,5,opt,name=host,proto3" json:"host,omitempty"` + Port int32 `protobuf:"varint,6,opt,name=port,proto3" json:"port,omitempty"` + DbName string `protobuf:"bytes,7,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + Username string `protobuf:"bytes,8,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,9,opt,name=password,proto3" json:"password,omitempty"` //为空则不更新密码 + ParamsJson string `protobuf:"bytes,10,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` + Status int32 `protobuf:"varint,11,opt,name=status,proto3" json:"status,omitempty"` + Owner string `protobuf:"bytes,12,opt,name=owner,proto3" json:"owner,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateDataSourceReq) Reset() { + *x = UpdateDataSourceReq{} + mi := &file_datasource_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateDataSourceReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateDataSourceReq) ProtoMessage() {} + +func (x *UpdateDataSourceReq) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateDataSourceReq.ProtoReflect.Descriptor instead. +func (*UpdateDataSourceReq) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateDataSourceReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UpdateDataSourceReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *UpdateDataSourceReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateDataSourceReq) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *UpdateDataSourceReq) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *UpdateDataSourceReq) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *UpdateDataSourceReq) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *UpdateDataSourceReq) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UpdateDataSourceReq) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *UpdateDataSourceReq) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *UpdateDataSourceReq) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *UpdateDataSourceReq) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +type GetDataSourceReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + WithSecret bool `protobuf:"varint,3,opt,name=with_secret,json=withSecret,proto3" json:"with_secret,omitempty"` //true 时返回解密密码与 dsn(仅内部服务调用) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDataSourceReq) Reset() { + *x = GetDataSourceReq{} + mi := &file_datasource_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDataSourceReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDataSourceReq) ProtoMessage() {} + +func (x *GetDataSourceReq) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDataSourceReq.ProtoReflect.Descriptor instead. +func (*GetDataSourceReq) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{8} +} + +func (x *GetDataSourceReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *GetDataSourceReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *GetDataSourceReq) GetWithSecret() bool { + if x != nil { + return x.WithSecret + } + return false +} + +type TestConnReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + LatencyMs int64 `protobuf:"varint,2,opt,name=latency_ms,json=latencyMs,proto3" json:"latency_ms,omitempty"` + ErrMsg string `protobuf:"bytes,3,opt,name=err_msg,json=errMsg,proto3" json:"err_msg,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TestConnReply) Reset() { + *x = TestConnReply{} + mi := &file_datasource_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TestConnReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestConnReply) ProtoMessage() {} + +func (x *TestConnReply) ProtoReflect() protoreflect.Message { + mi := &file_datasource_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestConnReply.ProtoReflect.Descriptor instead. +func (*TestConnReply) Descriptor() ([]byte, []int) { + return file_datasource_proto_rawDescGZIP(), []int{9} +} + +func (x *TestConnReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *TestConnReply) GetLatencyMs() int64 { + if x != nil { + return x.LatencyMs + } + return 0 +} + +func (x *TestConnReply) GetErrMsg() string { + if x != nil { + return x.ErrMsg + } + return "" +} + +var File_datasource_proto protoreflect.FileDescriptor + +const file_datasource_proto_rawDesc = "" + + "\n" + + "\x10datasource.proto\x12\n" + + "datasource\"\xe9\x02\n" + + "\x0eDataSourceInfo\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\x12\x12\n" + + "\x04host\x18\x04 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x05 \x01(\x05R\x04port\x12\x17\n" + + "\adb_name\x18\x06 \x01(\tR\x06dbName\x12\x1a\n" + + "\busername\x18\a \x01(\tR\busername\x12!\n" + + "\fpassword_enc\x18\b \x01(\tR\vpasswordEnc\x12\x1f\n" + + "\vparams_json\x18\t \x01(\tR\n" + + "paramsJson\x12\x16\n" + + "\x06status\x18\n" + + " \x01(\x05R\x06status\x12\x14\n" + + "\x05owner\x18\v \x01(\tR\x05owner\x12\x12\n" + + "\x04beid\x18\f \x01(\x03R\x04beid\x12\x1d\n" + + "\n" + + "created_at\x18\r \x01(\x03R\tcreatedAt\x12\x1d\n" + + "\n" + + "updated_at\x18\x0e \x01(\x03R\tupdatedAt\"_\n" + + "\x0fDataSourceReply\x12:\n" + + "\n" + + "datasource\x18\x01 \x01(\v2\x1a.datasource.DataSourceInfoR\n" + + "datasource\x12\x10\n" + + "\x03dsn\x18\x02 \x01(\tR\x03dsn\"\x19\n" + + "\aOkReply\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"Y\n" + + "\aListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"[\n" + + "\x13DataSourceListReply\x12.\n" + + "\x04list\x18\x01 \x03(\v2\x1a.datasource.DataSourceInfoR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\x81\x02\n" + + "\x13CreateDataSourceReq\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x12\n" + + "\x04host\x18\x03 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x04 \x01(\x05R\x04port\x12\x17\n" + + "\adb_name\x18\x05 \x01(\tR\x06dbName\x12\x1a\n" + + "\busername\x18\x06 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\a \x01(\tR\bpassword\x12\x1f\n" + + "\vparams_json\x18\b \x01(\tR\n" + + "paramsJson\x12\x14\n" + + "\x05owner\x18\t \x01(\tR\x05owner\x12\x12\n" + + "\x04beid\x18\n" + + " \x01(\x03R\x04beid\"\xa9\x02\n" + + "\x13UpdateDataSourceReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" + + "\x04type\x18\x04 \x01(\tR\x04type\x12\x12\n" + + "\x04host\x18\x05 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x06 \x01(\x05R\x04port\x12\x17\n" + + "\adb_name\x18\a \x01(\tR\x06dbName\x12\x1a\n" + + "\busername\x18\b \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\t \x01(\tR\bpassword\x12\x1f\n" + + "\vparams_json\x18\n" + + " \x01(\tR\n" + + "paramsJson\x12\x16\n" + + "\x06status\x18\v \x01(\x05R\x06status\x12\x14\n" + + "\x05owner\x18\f \x01(\tR\x05owner\"W\n" + + "\x10GetDataSourceReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\x12\x1f\n" + + "\vwith_secret\x18\x03 \x01(\bR\n" + + "withSecret\"W\n" + + "\rTestConnReply\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1d\n" + + "\n" + + "latency_ms\x18\x02 \x01(\x03R\tlatencyMs\x12\x17\n" + + "\aerr_msg\x18\x03 \x01(\tR\x06errMsg2\xb9\x03\n" + + "\n" + + "DataSource\x12P\n" + + "\x10CreateDataSource\x12\x1f.datasource.CreateDataSourceReq\x1a\x1b.datasource.DataSourceReply\x12H\n" + + "\x10UpdateDataSource\x12\x1f.datasource.UpdateDataSourceReq\x1a\x13.datasource.OkReply\x12:\n" + + "\x10DeleteDataSource\x12\x11.datasource.IdReq\x1a\x13.datasource.OkReply\x12J\n" + + "\rGetDataSource\x12\x1c.datasource.GetDataSourceReq\x1a\x1b.datasource.DataSourceReply\x12G\n" + + "\x0fListDataSources\x12\x13.datasource.ListReq\x1a\x1f.datasource.DataSourceListReply\x12>\n" + + "\x0eTestConnection\x12\x11.datasource.IdReq\x1a\x19.datasource.TestConnReplyB\x0eZ\f./datasourceb\x06proto3" + +var ( + file_datasource_proto_rawDescOnce sync.Once + file_datasource_proto_rawDescData []byte +) + +func file_datasource_proto_rawDescGZIP() []byte { + file_datasource_proto_rawDescOnce.Do(func() { + file_datasource_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_datasource_proto_rawDesc), len(file_datasource_proto_rawDesc))) + }) + return file_datasource_proto_rawDescData +} + +var file_datasource_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_datasource_proto_goTypes = []any{ + (*DataSourceInfo)(nil), // 0: datasource.DataSourceInfo + (*DataSourceReply)(nil), // 1: datasource.DataSourceReply + (*OkReply)(nil), // 2: datasource.OkReply + (*IdReq)(nil), // 3: datasource.IdReq + (*ListReq)(nil), // 4: datasource.ListReq + (*DataSourceListReply)(nil), // 5: datasource.DataSourceListReply + (*CreateDataSourceReq)(nil), // 6: datasource.CreateDataSourceReq + (*UpdateDataSourceReq)(nil), // 7: datasource.UpdateDataSourceReq + (*GetDataSourceReq)(nil), // 8: datasource.GetDataSourceReq + (*TestConnReply)(nil), // 9: datasource.TestConnReply +} +var file_datasource_proto_depIdxs = []int32{ + 0, // 0: datasource.DataSourceReply.datasource:type_name -> datasource.DataSourceInfo + 0, // 1: datasource.DataSourceListReply.list:type_name -> datasource.DataSourceInfo + 6, // 2: datasource.DataSource.CreateDataSource:input_type -> datasource.CreateDataSourceReq + 7, // 3: datasource.DataSource.UpdateDataSource:input_type -> datasource.UpdateDataSourceReq + 3, // 4: datasource.DataSource.DeleteDataSource:input_type -> datasource.IdReq + 8, // 5: datasource.DataSource.GetDataSource:input_type -> datasource.GetDataSourceReq + 4, // 6: datasource.DataSource.ListDataSources:input_type -> datasource.ListReq + 3, // 7: datasource.DataSource.TestConnection:input_type -> datasource.IdReq + 1, // 8: datasource.DataSource.CreateDataSource:output_type -> datasource.DataSourceReply + 2, // 9: datasource.DataSource.UpdateDataSource:output_type -> datasource.OkReply + 2, // 10: datasource.DataSource.DeleteDataSource:output_type -> datasource.OkReply + 1, // 11: datasource.DataSource.GetDataSource:output_type -> datasource.DataSourceReply + 5, // 12: datasource.DataSource.ListDataSources:output_type -> datasource.DataSourceListReply + 9, // 13: datasource.DataSource.TestConnection:output_type -> datasource.TestConnReply + 8, // [8:14] is the sub-list for method output_type + 2, // [2:8] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_datasource_proto_init() } +func file_datasource_proto_init() { + if File_datasource_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_datasource_proto_rawDesc), len(file_datasource_proto_rawDesc)), + NumEnums: 0, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_datasource_proto_goTypes, + DependencyIndexes: file_datasource_proto_depIdxs, + MessageInfos: file_datasource_proto_msgTypes, + }.Build() + File_datasource_proto = out.File + file_datasource_proto_goTypes = nil + file_datasource_proto_depIdxs = nil +} diff --git a/datasource/rpc/datasource/datasource_grpc.pb.go b/datasource/rpc/datasource/datasource_grpc.pb.go new file mode 100644 index 0000000..eeab6ed --- /dev/null +++ b/datasource/rpc/datasource/datasource_grpc.pb.go @@ -0,0 +1,323 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: datasource.proto + +package datasource + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DataSource_CreateDataSource_FullMethodName = "/datasource.DataSource/CreateDataSource" + DataSource_UpdateDataSource_FullMethodName = "/datasource.DataSource/UpdateDataSource" + DataSource_DeleteDataSource_FullMethodName = "/datasource.DataSource/DeleteDataSource" + DataSource_GetDataSource_FullMethodName = "/datasource.DataSource/GetDataSource" + DataSource_ListDataSources_FullMethodName = "/datasource.DataSource/ListDataSources" + DataSource_TestConnection_FullMethodName = "/datasource.DataSource/TestConnection" +) + +// DataSourceClient is the client API for DataSource service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DataSourceClient interface { + //创建数据源 + CreateDataSource(ctx context.Context, in *CreateDataSourceReq, opts ...grpc.CallOption) (*DataSourceReply, error) + //更新数据源 + UpdateDataSource(ctx context.Context, in *UpdateDataSourceReq, opts ...grpc.CallOption) (*OkReply, error) + //删除数据源 + DeleteDataSource(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) + //数据源详情 + GetDataSource(ctx context.Context, in *GetDataSourceReq, opts ...grpc.CallOption) (*DataSourceReply, error) + //数据源列表 + ListDataSources(ctx context.Context, in *ListReq, opts ...grpc.CallOption) (*DataSourceListReply, error) + //测试连接 + TestConnection(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*TestConnReply, error) +} + +type dataSourceClient struct { + cc grpc.ClientConnInterface +} + +func NewDataSourceClient(cc grpc.ClientConnInterface) DataSourceClient { + return &dataSourceClient{cc} +} + +func (c *dataSourceClient) CreateDataSource(ctx context.Context, in *CreateDataSourceReq, opts ...grpc.CallOption) (*DataSourceReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DataSourceReply) + err := c.cc.Invoke(ctx, DataSource_CreateDataSource_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSourceClient) UpdateDataSource(ctx context.Context, in *UpdateDataSourceReq, opts ...grpc.CallOption) (*OkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OkReply) + err := c.cc.Invoke(ctx, DataSource_UpdateDataSource_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSourceClient) DeleteDataSource(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OkReply) + err := c.cc.Invoke(ctx, DataSource_DeleteDataSource_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSourceClient) GetDataSource(ctx context.Context, in *GetDataSourceReq, opts ...grpc.CallOption) (*DataSourceReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DataSourceReply) + err := c.cc.Invoke(ctx, DataSource_GetDataSource_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSourceClient) ListDataSources(ctx context.Context, in *ListReq, opts ...grpc.CallOption) (*DataSourceListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DataSourceListReply) + err := c.cc.Invoke(ctx, DataSource_ListDataSources_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSourceClient) TestConnection(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*TestConnReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TestConnReply) + err := c.cc.Invoke(ctx, DataSource_TestConnection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DataSourceServer is the server API for DataSource service. +// All implementations must embed UnimplementedDataSourceServer +// for forward compatibility. +type DataSourceServer interface { + //创建数据源 + CreateDataSource(context.Context, *CreateDataSourceReq) (*DataSourceReply, error) + //更新数据源 + UpdateDataSource(context.Context, *UpdateDataSourceReq) (*OkReply, error) + //删除数据源 + DeleteDataSource(context.Context, *IdReq) (*OkReply, error) + //数据源详情 + GetDataSource(context.Context, *GetDataSourceReq) (*DataSourceReply, error) + //数据源列表 + ListDataSources(context.Context, *ListReq) (*DataSourceListReply, error) + //测试连接 + TestConnection(context.Context, *IdReq) (*TestConnReply, error) + mustEmbedUnimplementedDataSourceServer() +} + +// UnimplementedDataSourceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDataSourceServer struct{} + +func (UnimplementedDataSourceServer) CreateDataSource(context.Context, *CreateDataSourceReq) (*DataSourceReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateDataSource not implemented") +} +func (UnimplementedDataSourceServer) UpdateDataSource(context.Context, *UpdateDataSourceReq) (*OkReply, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateDataSource not implemented") +} +func (UnimplementedDataSourceServer) DeleteDataSource(context.Context, *IdReq) (*OkReply, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteDataSource not implemented") +} +func (UnimplementedDataSourceServer) GetDataSource(context.Context, *GetDataSourceReq) (*DataSourceReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetDataSource not implemented") +} +func (UnimplementedDataSourceServer) ListDataSources(context.Context, *ListReq) (*DataSourceListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListDataSources not implemented") +} +func (UnimplementedDataSourceServer) TestConnection(context.Context, *IdReq) (*TestConnReply, error) { + return nil, status.Error(codes.Unimplemented, "method TestConnection not implemented") +} +func (UnimplementedDataSourceServer) mustEmbedUnimplementedDataSourceServer() {} +func (UnimplementedDataSourceServer) testEmbeddedByValue() {} + +// UnsafeDataSourceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DataSourceServer will +// result in compilation errors. +type UnsafeDataSourceServer interface { + mustEmbedUnimplementedDataSourceServer() +} + +func RegisterDataSourceServer(s grpc.ServiceRegistrar, srv DataSourceServer) { + // If the following call panics, it indicates UnimplementedDataSourceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DataSource_ServiceDesc, srv) +} + +func _DataSource_CreateDataSource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateDataSourceReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSourceServer).CreateDataSource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSource_CreateDataSource_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSourceServer).CreateDataSource(ctx, req.(*CreateDataSourceReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSource_UpdateDataSource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateDataSourceReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSourceServer).UpdateDataSource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSource_UpdateDataSource_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSourceServer).UpdateDataSource(ctx, req.(*UpdateDataSourceReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSource_DeleteDataSource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSourceServer).DeleteDataSource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSource_DeleteDataSource_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSourceServer).DeleteDataSource(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSource_GetDataSource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDataSourceReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSourceServer).GetDataSource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSource_GetDataSource_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSourceServer).GetDataSource(ctx, req.(*GetDataSourceReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSource_ListDataSources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSourceServer).ListDataSources(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSource_ListDataSources_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSourceServer).ListDataSources(ctx, req.(*ListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSource_TestConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSourceServer).TestConnection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSource_TestConnection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSourceServer).TestConnection(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +// DataSource_ServiceDesc is the grpc.ServiceDesc for DataSource service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DataSource_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "datasource.DataSource", + HandlerType: (*DataSourceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateDataSource", + Handler: _DataSource_CreateDataSource_Handler, + }, + { + MethodName: "UpdateDataSource", + Handler: _DataSource_UpdateDataSource_Handler, + }, + { + MethodName: "DeleteDataSource", + Handler: _DataSource_DeleteDataSource_Handler, + }, + { + MethodName: "GetDataSource", + Handler: _DataSource_GetDataSource_Handler, + }, + { + MethodName: "ListDataSources", + Handler: _DataSource_ListDataSources_Handler, + }, + { + MethodName: "TestConnection", + Handler: _DataSource_TestConnection_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "datasource.proto", +} diff --git a/datasource/rpc/etc/datasource.example.yaml b/datasource/rpc/etc/datasource.example.yaml new file mode 100644 index 0000000..49f5b0e --- /dev/null +++ b/datasource/rpc/etc/datasource.example.yaml @@ -0,0 +1,9 @@ +Name: datasource.rpc +ListenOn: 0.0.0.0:8085 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: datasource.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +EncryptKey: 0123456789abcdef0123456789abcdef diff --git a/datasource/rpc/internal/config/config.go b/datasource/rpc/internal/config/config.go new file mode 100755 index 0000000..d7111de --- /dev/null +++ b/datasource/rpc/internal/config/config.go @@ -0,0 +1,15 @@ +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } + EncryptKey string `json:",optional"` //AES-GCM 密钥(32字节) + CacheRedis cache.ClusterConf `json:",optional"` +} diff --git a/datasource/rpc/internal/logic/createDataSourceLogic.go b/datasource/rpc/internal/logic/createDataSourceLogic.go new file mode 100644 index 0000000..4e518e2 --- /dev/null +++ b/datasource/rpc/internal/logic/createDataSourceLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasource/rpc/datasource" + "datacenter/datasource/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateDataSourceLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateDataSourceLogic { + return &CreateDataSourceLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建数据源 +func (l *CreateDataSourceLogic) CreateDataSource(in *datasource.CreateDataSourceReq) (*datasource.DataSourceReply, error) { + // todo: add your logic here and delete this line + + return &datasource.DataSourceReply{}, nil +} diff --git a/datasource/rpc/internal/logic/deleteDataSourceLogic.go b/datasource/rpc/internal/logic/deleteDataSourceLogic.go new file mode 100644 index 0000000..3f1148e --- /dev/null +++ b/datasource/rpc/internal/logic/deleteDataSourceLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasource/rpc/datasource" + "datacenter/datasource/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteDataSourceLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeleteDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteDataSourceLogic { + return &DeleteDataSourceLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 删除数据源 +func (l *DeleteDataSourceLogic) DeleteDataSource(in *datasource.IdReq) (*datasource.OkReply, error) { + // todo: add your logic here and delete this line + + return &datasource.OkReply{}, nil +} diff --git a/datasource/rpc/internal/logic/getDataSourceLogic.go b/datasource/rpc/internal/logic/getDataSourceLogic.go new file mode 100644 index 0000000..1b5e067 --- /dev/null +++ b/datasource/rpc/internal/logic/getDataSourceLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasource/rpc/datasource" + "datacenter/datasource/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetDataSourceLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDataSourceLogic { + return &GetDataSourceLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 数据源详情 +func (l *GetDataSourceLogic) GetDataSource(in *datasource.GetDataSourceReq) (*datasource.DataSourceReply, error) { + // todo: add your logic here and delete this line + + return &datasource.DataSourceReply{}, nil +} diff --git a/datasource/rpc/internal/logic/listDataSourcesLogic.go b/datasource/rpc/internal/logic/listDataSourcesLogic.go new file mode 100644 index 0000000..ea0bcc1 --- /dev/null +++ b/datasource/rpc/internal/logic/listDataSourcesLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasource/rpc/datasource" + "datacenter/datasource/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListDataSourcesLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListDataSourcesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListDataSourcesLogic { + return &ListDataSourcesLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 数据源列表 +func (l *ListDataSourcesLogic) ListDataSources(in *datasource.ListReq) (*datasource.DataSourceListReply, error) { + // todo: add your logic here and delete this line + + return &datasource.DataSourceListReply{}, nil +} diff --git a/datasource/rpc/internal/logic/testConnectionLogic.go b/datasource/rpc/internal/logic/testConnectionLogic.go new file mode 100644 index 0000000..bc1affc --- /dev/null +++ b/datasource/rpc/internal/logic/testConnectionLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasource/rpc/datasource" + "datacenter/datasource/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TestConnectionLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewTestConnectionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TestConnectionLogic { + return &TestConnectionLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 测试连接 +func (l *TestConnectionLogic) TestConnection(in *datasource.IdReq) (*datasource.TestConnReply, error) { + // todo: add your logic here and delete this line + + return &datasource.TestConnReply{}, nil +} diff --git a/datasource/rpc/internal/logic/updateDataSourceLogic.go b/datasource/rpc/internal/logic/updateDataSourceLogic.go new file mode 100644 index 0000000..978eb7c --- /dev/null +++ b/datasource/rpc/internal/logic/updateDataSourceLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasource/rpc/datasource" + "datacenter/datasource/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateDataSourceLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewUpdateDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateDataSourceLogic { + return &UpdateDataSourceLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 更新数据源 +func (l *UpdateDataSourceLogic) UpdateDataSource(in *datasource.UpdateDataSourceReq) (*datasource.OkReply, error) { + // todo: add your logic here and delete this line + + return &datasource.OkReply{}, nil +} diff --git a/datasource/rpc/internal/server/dataSourceServer.go b/datasource/rpc/internal/server/dataSourceServer.go new file mode 100644 index 0000000..a4fe0ea --- /dev/null +++ b/datasource/rpc/internal/server/dataSourceServer.go @@ -0,0 +1,60 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: datasource.proto + +package server + +import ( + "context" + + "datacenter/datasource/rpc/datasource" + "datacenter/datasource/rpc/internal/logic" + "datacenter/datasource/rpc/internal/svc" +) + +type DataSourceServer struct { + svcCtx *svc.ServiceContext + datasource.UnimplementedDataSourceServer +} + +func NewDataSourceServer(svcCtx *svc.ServiceContext) *DataSourceServer { + return &DataSourceServer{ + svcCtx: svcCtx, + } +} + +// 创建数据源 +func (s *DataSourceServer) CreateDataSource(ctx context.Context, in *datasource.CreateDataSourceReq) (*datasource.DataSourceReply, error) { + l := logic.NewCreateDataSourceLogic(ctx, s.svcCtx) + return l.CreateDataSource(in) +} + +// 更新数据源 +func (s *DataSourceServer) UpdateDataSource(ctx context.Context, in *datasource.UpdateDataSourceReq) (*datasource.OkReply, error) { + l := logic.NewUpdateDataSourceLogic(ctx, s.svcCtx) + return l.UpdateDataSource(in) +} + +// 删除数据源 +func (s *DataSourceServer) DeleteDataSource(ctx context.Context, in *datasource.IdReq) (*datasource.OkReply, error) { + l := logic.NewDeleteDataSourceLogic(ctx, s.svcCtx) + return l.DeleteDataSource(in) +} + +// 数据源详情 +func (s *DataSourceServer) GetDataSource(ctx context.Context, in *datasource.GetDataSourceReq) (*datasource.DataSourceReply, error) { + l := logic.NewGetDataSourceLogic(ctx, s.svcCtx) + return l.GetDataSource(in) +} + +// 数据源列表 +func (s *DataSourceServer) ListDataSources(ctx context.Context, in *datasource.ListReq) (*datasource.DataSourceListReply, error) { + l := logic.NewListDataSourcesLogic(ctx, s.svcCtx) + return l.ListDataSources(in) +} + +// 测试连接 +func (s *DataSourceServer) TestConnection(ctx context.Context, in *datasource.IdReq) (*datasource.TestConnReply, error) { + l := logic.NewTestConnectionLogic(ctx, s.svcCtx) + return l.TestConnection(in) +} diff --git a/datasource/rpc/internal/svc/serviceContext.go b/datasource/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..0dbf332 --- /dev/null +++ b/datasource/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/datasource/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/datasvc/rpc/dataSvcClient/dataSvc.go b/datasvc/rpc/dataSvcClient/dataSvc.go new file mode 100644 index 0000000..301e9b5 --- /dev/null +++ b/datasvc/rpc/dataSvcClient/dataSvc.go @@ -0,0 +1,123 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: datasvc.proto + +package dataSvcClient + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + Api = datasvc.Api + ApiApp = datasvc.ApiApp + ApiListReply = datasvc.ApiListReply + ApiListReq = datasvc.ApiListReq + ApiReply = datasvc.ApiReply + AppReply = datasvc.AppReply + CreateApiReq = datasvc.CreateApiReq + ExecuteReply = datasvc.ExecuteReply + ExecuteReq = datasvc.ExecuteReq + GrantApiReq = datasvc.GrantApiReq + IdReq = datasvc.IdReq + OkReply = datasvc.OkReply + PathReq = datasvc.PathReq + RegisterAppReq = datasvc.RegisterAppReq + UpdateApiReq = datasvc.UpdateApiReq + + DataSvc interface { + // 创建API + CreateApi(ctx context.Context, in *CreateApiReq, opts ...grpc.CallOption) (*ApiReply, error) + // 更新API + UpdateApi(ctx context.Context, in *UpdateApiReq, opts ...grpc.CallOption) (*OkReply, error) + // 删除API + DeleteApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) + // API详情 + GetApi(ctx context.Context, in *PathReq, opts ...grpc.CallOption) (*ApiReply, error) + // API列表 + ListApis(ctx context.Context, in *ApiListReq, opts ...grpc.CallOption) (*ApiListReply, error) + // 发布/下线 + PublishApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) + OfflineApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) + // 注册应用(返回 app_key/app_secret) + RegisterApp(ctx context.Context, in *RegisterAppReq, opts ...grpc.CallOption) (*AppReply, error) + // 授权 API 给应用 + GrantApi(ctx context.Context, in *GrantApiReq, opts ...grpc.CallOption) (*OkReply, error) + // 执行 API(核心:鉴权→限流→缓存→执行→脱敏) + ExecuteApi(ctx context.Context, in *ExecuteReq, opts ...grpc.CallOption) (*ExecuteReply, error) + } + + defaultDataSvc struct { + cli zrpc.Client + } +) + +func NewDataSvc(cli zrpc.Client) DataSvc { + return &defaultDataSvc{ + cli: cli, + } +} + +// 创建API +func (m *defaultDataSvc) CreateApi(ctx context.Context, in *CreateApiReq, opts ...grpc.CallOption) (*ApiReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.CreateApi(ctx, in, opts...) +} + +// 更新API +func (m *defaultDataSvc) UpdateApi(ctx context.Context, in *UpdateApiReq, opts ...grpc.CallOption) (*OkReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.UpdateApi(ctx, in, opts...) +} + +// 删除API +func (m *defaultDataSvc) DeleteApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.DeleteApi(ctx, in, opts...) +} + +// API详情 +func (m *defaultDataSvc) GetApi(ctx context.Context, in *PathReq, opts ...grpc.CallOption) (*ApiReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.GetApi(ctx, in, opts...) +} + +// API列表 +func (m *defaultDataSvc) ListApis(ctx context.Context, in *ApiListReq, opts ...grpc.CallOption) (*ApiListReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.ListApis(ctx, in, opts...) +} + +// 发布/下线 +func (m *defaultDataSvc) PublishApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.PublishApi(ctx, in, opts...) +} + +func (m *defaultDataSvc) OfflineApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.OfflineApi(ctx, in, opts...) +} + +// 注册应用(返回 app_key/app_secret) +func (m *defaultDataSvc) RegisterApp(ctx context.Context, in *RegisterAppReq, opts ...grpc.CallOption) (*AppReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.RegisterApp(ctx, in, opts...) +} + +// 授权 API 给应用 +func (m *defaultDataSvc) GrantApi(ctx context.Context, in *GrantApiReq, opts ...grpc.CallOption) (*OkReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.GrantApi(ctx, in, opts...) +} + +// 执行 API(核心:鉴权→限流→缓存→执行→脱敏) +func (m *defaultDataSvc) ExecuteApi(ctx context.Context, in *ExecuteReq, opts ...grpc.CallOption) (*ExecuteReply, error) { + client := datasvc.NewDataSvcClient(m.cli.Conn()) + return client.ExecuteApi(ctx, in, opts...) +} diff --git a/datasvc/rpc/datasvc.go b/datasvc/rpc/datasvc.go new file mode 100644 index 0000000..61b9b78 --- /dev/null +++ b/datasvc/rpc/datasvc.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/config" + "datacenter/datasvc/rpc/internal/server" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/datasvc.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + datasvc.RegisterDataSvcServer(grpcServer, server.NewDataSvcServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/datasvc/rpc/datasvc.proto b/datasvc/rpc/datasvc.proto new file mode 100644 index 0000000..b3d8067 --- /dev/null +++ b/datasvc/rpc/datasvc.proto @@ -0,0 +1,119 @@ +syntax = "proto3"; + +package datasvc; + +option go_package = "./datasvc"; + +message Api { + int64 id = 1; + string name = 2; + string path = 3; //执行路径 /dsvc/{path} + string mode = 4; //wizard/sql/register + int64 datasource_id = 5; + string sql_text = 6; //含 :param 占位符 + string params_json = 7; //[{name,type,required,default}] + int64 cache_ttl = 8; //秒,0 不缓存 + string status = 9; //draft/published/offline + string resp_sample_json = 10; + string owner = 11; + int64 beid = 12; +} +message ApiReply { Api api = 1; } +message ApiListReply { + repeated Api list = 1; + int64 total = 2; +} + +message CreateApiReq { + string name = 1; + string path = 2; + string mode = 3; + int64 datasource_id = 4; + string sql_text = 5; + string params_json = 6; + int64 cache_ttl = 7; + string resp_sample_json = 8; + string owner = 9; + int64 beid = 10; +} +message UpdateApiReq { + int64 id = 1; + int64 beid = 2; + string name = 3; + string sql_text = 4; + string params_json = 5; + int64 cache_ttl = 6; + string resp_sample_json = 7; +} +message ApiListReq { + int64 beid = 1; + string status = 2; + int64 page = 3; + int64 size = 4; +} +message PathReq { string path = 1; int64 beid = 2; } + +message ApiApp { + int64 id = 1; + string app_key = 2; + string name = 3; + int64 rate_limit_qps = 4; + string ip_whitelist = 5; //逗号分隔,空为不限 + int64 beid = 6; +} +message RegisterAppReq { + string name = 1; + int64 rate_limit_qps = 2; + string ip_whitelist = 3; + int64 beid = 4; +} +message AppReply { ApiApp app = 1; string app_secret = 5; } //secret 仅注册时返回一次 + +message GrantApiReq { + int64 api_id = 1; + int64 app_id = 2; + int64 expire_at = 3; //0 永久 + string field_mask = 4; //脱敏字段逗号分隔 + int64 beid = 5; +} + +message ExecuteReq { + string app_key = 1; + string sign = 2; //hmac-sha256(app_key+ts, app_secret) + string ts = 3; + string path = 4; + string params_json = 5; //业务参数 + string ip = 6; + int64 beid = 7; +} +message ExecuteReply { + string rows_json = 1; //查询结果(json数组) + int64 cost_ms = 2; + bool cache_hit = 3; + int64 affected = 4; //写语句受影响行数 +} + +service DataSvc { + //创建API + rpc CreateApi(CreateApiReq) returns (ApiReply); + //更新API + rpc UpdateApi(UpdateApiReq) returns (OkReply); + //删除API + rpc DeleteApi(IdReq) returns (OkReply); + //API详情 + rpc GetApi(PathReq) returns (ApiReply); + //API列表 + rpc ListApis(ApiListReq) returns (ApiListReply); + //发布/下线 + rpc PublishApi(IdReq) returns (OkReply); + rpc OfflineApi(IdReq) returns (OkReply); + //注册应用(返回 app_key/app_secret) + rpc RegisterApp(RegisterAppReq) returns (AppReply); + //授权 API 给应用 + rpc GrantApi(GrantApiReq) returns (OkReply); + //执行 API(核心:鉴权→限流→缓存→执行→脱敏) + rpc ExecuteApi(ExecuteReq) returns (ExecuteReply); +} + +message IdReq { int64 id = 1; int64 beid = 2; } +message OkReply { bool ok = 1; } diff --git a/datasvc/rpc/datasvc/datasvc.pb.go b/datasvc/rpc/datasvc/datasvc.pb.go new file mode 100644 index 0000000..9c462fc --- /dev/null +++ b/datasvc/rpc/datasvc/datasvc.pb.go @@ -0,0 +1,1313 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: datasvc.proto + +package datasvc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Api struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` //执行路径 /dsvc/{path} + Mode string `protobuf:"bytes,4,opt,name=mode,proto3" json:"mode,omitempty"` //wizard/sql/register + DatasourceId int64 `protobuf:"varint,5,opt,name=datasource_id,json=datasourceId,proto3" json:"datasource_id,omitempty"` + SqlText string `protobuf:"bytes,6,opt,name=sql_text,json=sqlText,proto3" json:"sql_text,omitempty"` //含 :param 占位符 + ParamsJson string `protobuf:"bytes,7,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` //[{name,type,required,default}] + CacheTtl int64 `protobuf:"varint,8,opt,name=cache_ttl,json=cacheTtl,proto3" json:"cache_ttl,omitempty"` //秒,0 不缓存 + Status string `protobuf:"bytes,9,opt,name=status,proto3" json:"status,omitempty"` //draft/published/offline + RespSampleJson string `protobuf:"bytes,10,opt,name=resp_sample_json,json=respSampleJson,proto3" json:"resp_sample_json,omitempty"` + Owner string `protobuf:"bytes,11,opt,name=owner,proto3" json:"owner,omitempty"` + Beid int64 `protobuf:"varint,12,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Api) Reset() { + *x = Api{} + mi := &file_datasvc_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Api) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Api) ProtoMessage() {} + +func (x *Api) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Api.ProtoReflect.Descriptor instead. +func (*Api) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{0} +} + +func (x *Api) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Api) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Api) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Api) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *Api) GetDatasourceId() int64 { + if x != nil { + return x.DatasourceId + } + return 0 +} + +func (x *Api) GetSqlText() string { + if x != nil { + return x.SqlText + } + return "" +} + +func (x *Api) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *Api) GetCacheTtl() int64 { + if x != nil { + return x.CacheTtl + } + return 0 +} + +func (x *Api) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *Api) GetRespSampleJson() string { + if x != nil { + return x.RespSampleJson + } + return "" +} + +func (x *Api) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *Api) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ApiReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Api *Api `protobuf:"bytes,1,opt,name=api,proto3" json:"api,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApiReply) Reset() { + *x = ApiReply{} + mi := &file_datasvc_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApiReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiReply) ProtoMessage() {} + +func (x *ApiReply) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApiReply.ProtoReflect.Descriptor instead. +func (*ApiReply) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{1} +} + +func (x *ApiReply) GetApi() *Api { + if x != nil { + return x.Api + } + return nil +} + +type ApiListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*Api `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApiListReply) Reset() { + *x = ApiListReply{} + mi := &file_datasvc_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApiListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiListReply) ProtoMessage() {} + +func (x *ApiListReply) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApiListReply.ProtoReflect.Descriptor instead. +func (*ApiListReply) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{2} +} + +func (x *ApiListReply) GetList() []*Api { + if x != nil { + return x.List + } + return nil +} + +func (x *ApiListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type CreateApiReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` + DatasourceId int64 `protobuf:"varint,4,opt,name=datasource_id,json=datasourceId,proto3" json:"datasource_id,omitempty"` + SqlText string `protobuf:"bytes,5,opt,name=sql_text,json=sqlText,proto3" json:"sql_text,omitempty"` + ParamsJson string `protobuf:"bytes,6,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` + CacheTtl int64 `protobuf:"varint,7,opt,name=cache_ttl,json=cacheTtl,proto3" json:"cache_ttl,omitempty"` + RespSampleJson string `protobuf:"bytes,8,opt,name=resp_sample_json,json=respSampleJson,proto3" json:"resp_sample_json,omitempty"` + Owner string `protobuf:"bytes,9,opt,name=owner,proto3" json:"owner,omitempty"` + Beid int64 `protobuf:"varint,10,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateApiReq) Reset() { + *x = CreateApiReq{} + mi := &file_datasvc_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateApiReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateApiReq) ProtoMessage() {} + +func (x *CreateApiReq) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateApiReq.ProtoReflect.Descriptor instead. +func (*CreateApiReq) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateApiReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateApiReq) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *CreateApiReq) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *CreateApiReq) GetDatasourceId() int64 { + if x != nil { + return x.DatasourceId + } + return 0 +} + +func (x *CreateApiReq) GetSqlText() string { + if x != nil { + return x.SqlText + } + return "" +} + +func (x *CreateApiReq) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *CreateApiReq) GetCacheTtl() int64 { + if x != nil { + return x.CacheTtl + } + return 0 +} + +func (x *CreateApiReq) GetRespSampleJson() string { + if x != nil { + return x.RespSampleJson + } + return "" +} + +func (x *CreateApiReq) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *CreateApiReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type UpdateApiReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + SqlText string `protobuf:"bytes,4,opt,name=sql_text,json=sqlText,proto3" json:"sql_text,omitempty"` + ParamsJson string `protobuf:"bytes,5,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` + CacheTtl int64 `protobuf:"varint,6,opt,name=cache_ttl,json=cacheTtl,proto3" json:"cache_ttl,omitempty"` + RespSampleJson string `protobuf:"bytes,7,opt,name=resp_sample_json,json=respSampleJson,proto3" json:"resp_sample_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateApiReq) Reset() { + *x = UpdateApiReq{} + mi := &file_datasvc_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateApiReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateApiReq) ProtoMessage() {} + +func (x *UpdateApiReq) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateApiReq.ProtoReflect.Descriptor instead. +func (*UpdateApiReq) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{4} +} + +func (x *UpdateApiReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UpdateApiReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *UpdateApiReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateApiReq) GetSqlText() string { + if x != nil { + return x.SqlText + } + return "" +} + +func (x *UpdateApiReq) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *UpdateApiReq) GetCacheTtl() int64 { + if x != nil { + return x.CacheTtl + } + return 0 +} + +func (x *UpdateApiReq) GetRespSampleJson() string { + if x != nil { + return x.RespSampleJson + } + return "" +} + +type ApiListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApiListReq) Reset() { + *x = ApiListReq{} + mi := &file_datasvc_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApiListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiListReq) ProtoMessage() {} + +func (x *ApiListReq) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApiListReq.ProtoReflect.Descriptor instead. +func (*ApiListReq) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{5} +} + +func (x *ApiListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *ApiListReq) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ApiListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ApiListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type PathReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PathReq) Reset() { + *x = PathReq{} + mi := &file_datasvc_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PathReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PathReq) ProtoMessage() {} + +func (x *PathReq) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PathReq.ProtoReflect.Descriptor instead. +func (*PathReq) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{6} +} + +func (x *PathReq) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *PathReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ApiApp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + AppKey string `protobuf:"bytes,2,opt,name=app_key,json=appKey,proto3" json:"app_key,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + RateLimitQps int64 `protobuf:"varint,4,opt,name=rate_limit_qps,json=rateLimitQps,proto3" json:"rate_limit_qps,omitempty"` + IpWhitelist string `protobuf:"bytes,5,opt,name=ip_whitelist,json=ipWhitelist,proto3" json:"ip_whitelist,omitempty"` //逗号分隔,空为不限 + Beid int64 `protobuf:"varint,6,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApiApp) Reset() { + *x = ApiApp{} + mi := &file_datasvc_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApiApp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiApp) ProtoMessage() {} + +func (x *ApiApp) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApiApp.ProtoReflect.Descriptor instead. +func (*ApiApp) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{7} +} + +func (x *ApiApp) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ApiApp) GetAppKey() string { + if x != nil { + return x.AppKey + } + return "" +} + +func (x *ApiApp) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ApiApp) GetRateLimitQps() int64 { + if x != nil { + return x.RateLimitQps + } + return 0 +} + +func (x *ApiApp) GetIpWhitelist() string { + if x != nil { + return x.IpWhitelist + } + return "" +} + +func (x *ApiApp) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type RegisterAppReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + RateLimitQps int64 `protobuf:"varint,2,opt,name=rate_limit_qps,json=rateLimitQps,proto3" json:"rate_limit_qps,omitempty"` + IpWhitelist string `protobuf:"bytes,3,opt,name=ip_whitelist,json=ipWhitelist,proto3" json:"ip_whitelist,omitempty"` + Beid int64 `protobuf:"varint,4,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterAppReq) Reset() { + *x = RegisterAppReq{} + mi := &file_datasvc_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterAppReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterAppReq) ProtoMessage() {} + +func (x *RegisterAppReq) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterAppReq.ProtoReflect.Descriptor instead. +func (*RegisterAppReq) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{8} +} + +func (x *RegisterAppReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RegisterAppReq) GetRateLimitQps() int64 { + if x != nil { + return x.RateLimitQps + } + return 0 +} + +func (x *RegisterAppReq) GetIpWhitelist() string { + if x != nil { + return x.IpWhitelist + } + return "" +} + +func (x *RegisterAppReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type AppReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + App *ApiApp `protobuf:"bytes,1,opt,name=app,proto3" json:"app,omitempty"` + AppSecret string `protobuf:"bytes,5,opt,name=app_secret,json=appSecret,proto3" json:"app_secret,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppReply) Reset() { + *x = AppReply{} + mi := &file_datasvc_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppReply) ProtoMessage() {} + +func (x *AppReply) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppReply.ProtoReflect.Descriptor instead. +func (*AppReply) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{9} +} + +func (x *AppReply) GetApp() *ApiApp { + if x != nil { + return x.App + } + return nil +} + +func (x *AppReply) GetAppSecret() string { + if x != nil { + return x.AppSecret + } + return "" +} + +type GrantApiReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + ApiId int64 `protobuf:"varint,1,opt,name=api_id,json=apiId,proto3" json:"api_id,omitempty"` + AppId int64 `protobuf:"varint,2,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` + ExpireAt int64 `protobuf:"varint,3,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` //0 永久 + FieldMask string `protobuf:"bytes,4,opt,name=field_mask,json=fieldMask,proto3" json:"field_mask,omitempty"` //脱敏字段逗号分隔 + Beid int64 `protobuf:"varint,5,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GrantApiReq) Reset() { + *x = GrantApiReq{} + mi := &file_datasvc_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GrantApiReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GrantApiReq) ProtoMessage() {} + +func (x *GrantApiReq) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GrantApiReq.ProtoReflect.Descriptor instead. +func (*GrantApiReq) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{10} +} + +func (x *GrantApiReq) GetApiId() int64 { + if x != nil { + return x.ApiId + } + return 0 +} + +func (x *GrantApiReq) GetAppId() int64 { + if x != nil { + return x.AppId + } + return 0 +} + +func (x *GrantApiReq) GetExpireAt() int64 { + if x != nil { + return x.ExpireAt + } + return 0 +} + +func (x *GrantApiReq) GetFieldMask() string { + if x != nil { + return x.FieldMask + } + return "" +} + +func (x *GrantApiReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ExecuteReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + AppKey string `protobuf:"bytes,1,opt,name=app_key,json=appKey,proto3" json:"app_key,omitempty"` + Sign string `protobuf:"bytes,2,opt,name=sign,proto3" json:"sign,omitempty"` //hmac-sha256(app_key+ts, app_secret) + Ts string `protobuf:"bytes,3,opt,name=ts,proto3" json:"ts,omitempty"` + Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"` + ParamsJson string `protobuf:"bytes,5,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` //业务参数 + Ip string `protobuf:"bytes,6,opt,name=ip,proto3" json:"ip,omitempty"` + Beid int64 `protobuf:"varint,7,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteReq) Reset() { + *x = ExecuteReq{} + mi := &file_datasvc_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteReq) ProtoMessage() {} + +func (x *ExecuteReq) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteReq.ProtoReflect.Descriptor instead. +func (*ExecuteReq) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{11} +} + +func (x *ExecuteReq) GetAppKey() string { + if x != nil { + return x.AppKey + } + return "" +} + +func (x *ExecuteReq) GetSign() string { + if x != nil { + return x.Sign + } + return "" +} + +func (x *ExecuteReq) GetTs() string { + if x != nil { + return x.Ts + } + return "" +} + +func (x *ExecuteReq) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ExecuteReq) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *ExecuteReq) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *ExecuteReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ExecuteReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + RowsJson string `protobuf:"bytes,1,opt,name=rows_json,json=rowsJson,proto3" json:"rows_json,omitempty"` //查询结果(json数组) + CostMs int64 `protobuf:"varint,2,opt,name=cost_ms,json=costMs,proto3" json:"cost_ms,omitempty"` + CacheHit bool `protobuf:"varint,3,opt,name=cache_hit,json=cacheHit,proto3" json:"cache_hit,omitempty"` + Affected int64 `protobuf:"varint,4,opt,name=affected,proto3" json:"affected,omitempty"` //写语句受影响行数 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteReply) Reset() { + *x = ExecuteReply{} + mi := &file_datasvc_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteReply) ProtoMessage() {} + +func (x *ExecuteReply) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteReply.ProtoReflect.Descriptor instead. +func (*ExecuteReply) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{12} +} + +func (x *ExecuteReply) GetRowsJson() string { + if x != nil { + return x.RowsJson + } + return "" +} + +func (x *ExecuteReply) GetCostMs() int64 { + if x != nil { + return x.CostMs + } + return 0 +} + +func (x *ExecuteReply) GetCacheHit() bool { + if x != nil { + return x.CacheHit + } + return false +} + +func (x *ExecuteReply) GetAffected() int64 { + if x != nil { + return x.Affected + } + return 0 +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_datasvc_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{13} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type OkReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OkReply) Reset() { + *x = OkReply{} + mi := &file_datasvc_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OkReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OkReply) ProtoMessage() {} + +func (x *OkReply) ProtoReflect() protoreflect.Message { + mi := &file_datasvc_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OkReply.ProtoReflect.Descriptor instead. +func (*OkReply) Descriptor() ([]byte, []int) { + return file_datasvc_proto_rawDescGZIP(), []int{14} +} + +func (x *OkReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +var File_datasvc_proto protoreflect.FileDescriptor + +const file_datasvc_proto_rawDesc = "" + + "\n" + + "\rdatasvc.proto\x12\adatasvc\"\xbb\x02\n" + + "\x03Api\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04path\x18\x03 \x01(\tR\x04path\x12\x12\n" + + "\x04mode\x18\x04 \x01(\tR\x04mode\x12#\n" + + "\rdatasource_id\x18\x05 \x01(\x03R\fdatasourceId\x12\x19\n" + + "\bsql_text\x18\x06 \x01(\tR\asqlText\x12\x1f\n" + + "\vparams_json\x18\a \x01(\tR\n" + + "paramsJson\x12\x1b\n" + + "\tcache_ttl\x18\b \x01(\x03R\bcacheTtl\x12\x16\n" + + "\x06status\x18\t \x01(\tR\x06status\x12(\n" + + "\x10resp_sample_json\x18\n" + + " \x01(\tR\x0erespSampleJson\x12\x14\n" + + "\x05owner\x18\v \x01(\tR\x05owner\x12\x12\n" + + "\x04beid\x18\f \x01(\x03R\x04beid\"*\n" + + "\bApiReply\x12\x1e\n" + + "\x03api\x18\x01 \x01(\v2\f.datasvc.ApiR\x03api\"F\n" + + "\fApiListReply\x12 \n" + + "\x04list\x18\x01 \x03(\v2\f.datasvc.ApiR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\x9c\x02\n" + + "\fCreateApiReq\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + + "\x04mode\x18\x03 \x01(\tR\x04mode\x12#\n" + + "\rdatasource_id\x18\x04 \x01(\x03R\fdatasourceId\x12\x19\n" + + "\bsql_text\x18\x05 \x01(\tR\asqlText\x12\x1f\n" + + "\vparams_json\x18\x06 \x01(\tR\n" + + "paramsJson\x12\x1b\n" + + "\tcache_ttl\x18\a \x01(\x03R\bcacheTtl\x12(\n" + + "\x10resp_sample_json\x18\b \x01(\tR\x0erespSampleJson\x12\x14\n" + + "\x05owner\x18\t \x01(\tR\x05owner\x12\x12\n" + + "\x04beid\x18\n" + + " \x01(\x03R\x04beid\"\xc9\x01\n" + + "\fUpdateApiReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x19\n" + + "\bsql_text\x18\x04 \x01(\tR\asqlText\x12\x1f\n" + + "\vparams_json\x18\x05 \x01(\tR\n" + + "paramsJson\x12\x1b\n" + + "\tcache_ttl\x18\x06 \x01(\x03R\bcacheTtl\x12(\n" + + "\x10resp_sample_json\x18\a \x01(\tR\x0erespSampleJson\"`\n" + + "\n" + + "ApiListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"1\n" + + "\aPathReq\x12\x12\n" + + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"\xa2\x01\n" + + "\x06ApiApp\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" + + "\aapp_key\x18\x02 \x01(\tR\x06appKey\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12$\n" + + "\x0erate_limit_qps\x18\x04 \x01(\x03R\frateLimitQps\x12!\n" + + "\fip_whitelist\x18\x05 \x01(\tR\vipWhitelist\x12\x12\n" + + "\x04beid\x18\x06 \x01(\x03R\x04beid\"\x81\x01\n" + + "\x0eRegisterAppReq\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12$\n" + + "\x0erate_limit_qps\x18\x02 \x01(\x03R\frateLimitQps\x12!\n" + + "\fip_whitelist\x18\x03 \x01(\tR\vipWhitelist\x12\x12\n" + + "\x04beid\x18\x04 \x01(\x03R\x04beid\"L\n" + + "\bAppReply\x12!\n" + + "\x03app\x18\x01 \x01(\v2\x0f.datasvc.ApiAppR\x03app\x12\x1d\n" + + "\n" + + "app_secret\x18\x05 \x01(\tR\tappSecret\"\x8b\x01\n" + + "\vGrantApiReq\x12\x15\n" + + "\x06api_id\x18\x01 \x01(\x03R\x05apiId\x12\x15\n" + + "\x06app_id\x18\x02 \x01(\x03R\x05appId\x12\x1b\n" + + "\texpire_at\x18\x03 \x01(\x03R\bexpireAt\x12\x1d\n" + + "\n" + + "field_mask\x18\x04 \x01(\tR\tfieldMask\x12\x12\n" + + "\x04beid\x18\x05 \x01(\x03R\x04beid\"\xa2\x01\n" + + "\n" + + "ExecuteReq\x12\x17\n" + + "\aapp_key\x18\x01 \x01(\tR\x06appKey\x12\x12\n" + + "\x04sign\x18\x02 \x01(\tR\x04sign\x12\x0e\n" + + "\x02ts\x18\x03 \x01(\tR\x02ts\x12\x12\n" + + "\x04path\x18\x04 \x01(\tR\x04path\x12\x1f\n" + + "\vparams_json\x18\x05 \x01(\tR\n" + + "paramsJson\x12\x0e\n" + + "\x02ip\x18\x06 \x01(\tR\x02ip\x12\x12\n" + + "\x04beid\x18\a \x01(\x03R\x04beid\"}\n" + + "\fExecuteReply\x12\x1b\n" + + "\trows_json\x18\x01 \x01(\tR\browsJson\x12\x17\n" + + "\acost_ms\x18\x02 \x01(\x03R\x06costMs\x12\x1b\n" + + "\tcache_hit\x18\x03 \x01(\bR\bcacheHit\x12\x1a\n" + + "\baffected\x18\x04 \x01(\x03R\baffected\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"\x19\n" + + "\aOkReply\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok2\x95\x04\n" + + "\aDataSvc\x125\n" + + "\tCreateApi\x12\x15.datasvc.CreateApiReq\x1a\x11.datasvc.ApiReply\x124\n" + + "\tUpdateApi\x12\x15.datasvc.UpdateApiReq\x1a\x10.datasvc.OkReply\x12-\n" + + "\tDeleteApi\x12\x0e.datasvc.IdReq\x1a\x10.datasvc.OkReply\x12-\n" + + "\x06GetApi\x12\x10.datasvc.PathReq\x1a\x11.datasvc.ApiReply\x126\n" + + "\bListApis\x12\x13.datasvc.ApiListReq\x1a\x15.datasvc.ApiListReply\x12.\n" + + "\n" + + "PublishApi\x12\x0e.datasvc.IdReq\x1a\x10.datasvc.OkReply\x12.\n" + + "\n" + + "OfflineApi\x12\x0e.datasvc.IdReq\x1a\x10.datasvc.OkReply\x129\n" + + "\vRegisterApp\x12\x17.datasvc.RegisterAppReq\x1a\x11.datasvc.AppReply\x122\n" + + "\bGrantApi\x12\x14.datasvc.GrantApiReq\x1a\x10.datasvc.OkReply\x128\n" + + "\n" + + "ExecuteApi\x12\x13.datasvc.ExecuteReq\x1a\x15.datasvc.ExecuteReplyB\vZ\t./datasvcb\x06proto3" + +var ( + file_datasvc_proto_rawDescOnce sync.Once + file_datasvc_proto_rawDescData []byte +) + +func file_datasvc_proto_rawDescGZIP() []byte { + file_datasvc_proto_rawDescOnce.Do(func() { + file_datasvc_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_datasvc_proto_rawDesc), len(file_datasvc_proto_rawDesc))) + }) + return file_datasvc_proto_rawDescData +} + +var file_datasvc_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_datasvc_proto_goTypes = []any{ + (*Api)(nil), // 0: datasvc.Api + (*ApiReply)(nil), // 1: datasvc.ApiReply + (*ApiListReply)(nil), // 2: datasvc.ApiListReply + (*CreateApiReq)(nil), // 3: datasvc.CreateApiReq + (*UpdateApiReq)(nil), // 4: datasvc.UpdateApiReq + (*ApiListReq)(nil), // 5: datasvc.ApiListReq + (*PathReq)(nil), // 6: datasvc.PathReq + (*ApiApp)(nil), // 7: datasvc.ApiApp + (*RegisterAppReq)(nil), // 8: datasvc.RegisterAppReq + (*AppReply)(nil), // 9: datasvc.AppReply + (*GrantApiReq)(nil), // 10: datasvc.GrantApiReq + (*ExecuteReq)(nil), // 11: datasvc.ExecuteReq + (*ExecuteReply)(nil), // 12: datasvc.ExecuteReply + (*IdReq)(nil), // 13: datasvc.IdReq + (*OkReply)(nil), // 14: datasvc.OkReply +} +var file_datasvc_proto_depIdxs = []int32{ + 0, // 0: datasvc.ApiReply.api:type_name -> datasvc.Api + 0, // 1: datasvc.ApiListReply.list:type_name -> datasvc.Api + 7, // 2: datasvc.AppReply.app:type_name -> datasvc.ApiApp + 3, // 3: datasvc.DataSvc.CreateApi:input_type -> datasvc.CreateApiReq + 4, // 4: datasvc.DataSvc.UpdateApi:input_type -> datasvc.UpdateApiReq + 13, // 5: datasvc.DataSvc.DeleteApi:input_type -> datasvc.IdReq + 6, // 6: datasvc.DataSvc.GetApi:input_type -> datasvc.PathReq + 5, // 7: datasvc.DataSvc.ListApis:input_type -> datasvc.ApiListReq + 13, // 8: datasvc.DataSvc.PublishApi:input_type -> datasvc.IdReq + 13, // 9: datasvc.DataSvc.OfflineApi:input_type -> datasvc.IdReq + 8, // 10: datasvc.DataSvc.RegisterApp:input_type -> datasvc.RegisterAppReq + 10, // 11: datasvc.DataSvc.GrantApi:input_type -> datasvc.GrantApiReq + 11, // 12: datasvc.DataSvc.ExecuteApi:input_type -> datasvc.ExecuteReq + 1, // 13: datasvc.DataSvc.CreateApi:output_type -> datasvc.ApiReply + 14, // 14: datasvc.DataSvc.UpdateApi:output_type -> datasvc.OkReply + 14, // 15: datasvc.DataSvc.DeleteApi:output_type -> datasvc.OkReply + 1, // 16: datasvc.DataSvc.GetApi:output_type -> datasvc.ApiReply + 2, // 17: datasvc.DataSvc.ListApis:output_type -> datasvc.ApiListReply + 14, // 18: datasvc.DataSvc.PublishApi:output_type -> datasvc.OkReply + 14, // 19: datasvc.DataSvc.OfflineApi:output_type -> datasvc.OkReply + 9, // 20: datasvc.DataSvc.RegisterApp:output_type -> datasvc.AppReply + 14, // 21: datasvc.DataSvc.GrantApi:output_type -> datasvc.OkReply + 12, // 22: datasvc.DataSvc.ExecuteApi:output_type -> datasvc.ExecuteReply + 13, // [13:23] is the sub-list for method output_type + 3, // [3:13] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_datasvc_proto_init() } +func file_datasvc_proto_init() { + if File_datasvc_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_datasvc_proto_rawDesc), len(file_datasvc_proto_rawDesc)), + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_datasvc_proto_goTypes, + DependencyIndexes: file_datasvc_proto_depIdxs, + MessageInfos: file_datasvc_proto_msgTypes, + }.Build() + File_datasvc_proto = out.File + file_datasvc_proto_goTypes = nil + file_datasvc_proto_depIdxs = nil +} diff --git a/datasvc/rpc/datasvc/datasvc_grpc.pb.go b/datasvc/rpc/datasvc/datasvc_grpc.pb.go new file mode 100644 index 0000000..3c4688c --- /dev/null +++ b/datasvc/rpc/datasvc/datasvc_grpc.pb.go @@ -0,0 +1,481 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: datasvc.proto + +package datasvc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DataSvc_CreateApi_FullMethodName = "/datasvc.DataSvc/CreateApi" + DataSvc_UpdateApi_FullMethodName = "/datasvc.DataSvc/UpdateApi" + DataSvc_DeleteApi_FullMethodName = "/datasvc.DataSvc/DeleteApi" + DataSvc_GetApi_FullMethodName = "/datasvc.DataSvc/GetApi" + DataSvc_ListApis_FullMethodName = "/datasvc.DataSvc/ListApis" + DataSvc_PublishApi_FullMethodName = "/datasvc.DataSvc/PublishApi" + DataSvc_OfflineApi_FullMethodName = "/datasvc.DataSvc/OfflineApi" + DataSvc_RegisterApp_FullMethodName = "/datasvc.DataSvc/RegisterApp" + DataSvc_GrantApi_FullMethodName = "/datasvc.DataSvc/GrantApi" + DataSvc_ExecuteApi_FullMethodName = "/datasvc.DataSvc/ExecuteApi" +) + +// DataSvcClient is the client API for DataSvc service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DataSvcClient interface { + //创建API + CreateApi(ctx context.Context, in *CreateApiReq, opts ...grpc.CallOption) (*ApiReply, error) + //更新API + UpdateApi(ctx context.Context, in *UpdateApiReq, opts ...grpc.CallOption) (*OkReply, error) + //删除API + DeleteApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) + //API详情 + GetApi(ctx context.Context, in *PathReq, opts ...grpc.CallOption) (*ApiReply, error) + //API列表 + ListApis(ctx context.Context, in *ApiListReq, opts ...grpc.CallOption) (*ApiListReply, error) + //发布/下线 + PublishApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) + OfflineApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) + //注册应用(返回 app_key/app_secret) + RegisterApp(ctx context.Context, in *RegisterAppReq, opts ...grpc.CallOption) (*AppReply, error) + //授权 API 给应用 + GrantApi(ctx context.Context, in *GrantApiReq, opts ...grpc.CallOption) (*OkReply, error) + //执行 API(核心:鉴权→限流→缓存→执行→脱敏) + ExecuteApi(ctx context.Context, in *ExecuteReq, opts ...grpc.CallOption) (*ExecuteReply, error) +} + +type dataSvcClient struct { + cc grpc.ClientConnInterface +} + +func NewDataSvcClient(cc grpc.ClientConnInterface) DataSvcClient { + return &dataSvcClient{cc} +} + +func (c *dataSvcClient) CreateApi(ctx context.Context, in *CreateApiReq, opts ...grpc.CallOption) (*ApiReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApiReply) + err := c.cc.Invoke(ctx, DataSvc_CreateApi_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) UpdateApi(ctx context.Context, in *UpdateApiReq, opts ...grpc.CallOption) (*OkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OkReply) + err := c.cc.Invoke(ctx, DataSvc_UpdateApi_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) DeleteApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OkReply) + err := c.cc.Invoke(ctx, DataSvc_DeleteApi_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) GetApi(ctx context.Context, in *PathReq, opts ...grpc.CallOption) (*ApiReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApiReply) + err := c.cc.Invoke(ctx, DataSvc_GetApi_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) ListApis(ctx context.Context, in *ApiListReq, opts ...grpc.CallOption) (*ApiListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApiListReply) + err := c.cc.Invoke(ctx, DataSvc_ListApis_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) PublishApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OkReply) + err := c.cc.Invoke(ctx, DataSvc_PublishApi_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) OfflineApi(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*OkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OkReply) + err := c.cc.Invoke(ctx, DataSvc_OfflineApi_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) RegisterApp(ctx context.Context, in *RegisterAppReq, opts ...grpc.CallOption) (*AppReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AppReply) + err := c.cc.Invoke(ctx, DataSvc_RegisterApp_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) GrantApi(ctx context.Context, in *GrantApiReq, opts ...grpc.CallOption) (*OkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OkReply) + err := c.cc.Invoke(ctx, DataSvc_GrantApi_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dataSvcClient) ExecuteApi(ctx context.Context, in *ExecuteReq, opts ...grpc.CallOption) (*ExecuteReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExecuteReply) + err := c.cc.Invoke(ctx, DataSvc_ExecuteApi_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DataSvcServer is the server API for DataSvc service. +// All implementations must embed UnimplementedDataSvcServer +// for forward compatibility. +type DataSvcServer interface { + //创建API + CreateApi(context.Context, *CreateApiReq) (*ApiReply, error) + //更新API + UpdateApi(context.Context, *UpdateApiReq) (*OkReply, error) + //删除API + DeleteApi(context.Context, *IdReq) (*OkReply, error) + //API详情 + GetApi(context.Context, *PathReq) (*ApiReply, error) + //API列表 + ListApis(context.Context, *ApiListReq) (*ApiListReply, error) + //发布/下线 + PublishApi(context.Context, *IdReq) (*OkReply, error) + OfflineApi(context.Context, *IdReq) (*OkReply, error) + //注册应用(返回 app_key/app_secret) + RegisterApp(context.Context, *RegisterAppReq) (*AppReply, error) + //授权 API 给应用 + GrantApi(context.Context, *GrantApiReq) (*OkReply, error) + //执行 API(核心:鉴权→限流→缓存→执行→脱敏) + ExecuteApi(context.Context, *ExecuteReq) (*ExecuteReply, error) + mustEmbedUnimplementedDataSvcServer() +} + +// UnimplementedDataSvcServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDataSvcServer struct{} + +func (UnimplementedDataSvcServer) CreateApi(context.Context, *CreateApiReq) (*ApiReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateApi not implemented") +} +func (UnimplementedDataSvcServer) UpdateApi(context.Context, *UpdateApiReq) (*OkReply, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateApi not implemented") +} +func (UnimplementedDataSvcServer) DeleteApi(context.Context, *IdReq) (*OkReply, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteApi not implemented") +} +func (UnimplementedDataSvcServer) GetApi(context.Context, *PathReq) (*ApiReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetApi not implemented") +} +func (UnimplementedDataSvcServer) ListApis(context.Context, *ApiListReq) (*ApiListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListApis not implemented") +} +func (UnimplementedDataSvcServer) PublishApi(context.Context, *IdReq) (*OkReply, error) { + return nil, status.Error(codes.Unimplemented, "method PublishApi not implemented") +} +func (UnimplementedDataSvcServer) OfflineApi(context.Context, *IdReq) (*OkReply, error) { + return nil, status.Error(codes.Unimplemented, "method OfflineApi not implemented") +} +func (UnimplementedDataSvcServer) RegisterApp(context.Context, *RegisterAppReq) (*AppReply, error) { + return nil, status.Error(codes.Unimplemented, "method RegisterApp not implemented") +} +func (UnimplementedDataSvcServer) GrantApi(context.Context, *GrantApiReq) (*OkReply, error) { + return nil, status.Error(codes.Unimplemented, "method GrantApi not implemented") +} +func (UnimplementedDataSvcServer) ExecuteApi(context.Context, *ExecuteReq) (*ExecuteReply, error) { + return nil, status.Error(codes.Unimplemented, "method ExecuteApi not implemented") +} +func (UnimplementedDataSvcServer) mustEmbedUnimplementedDataSvcServer() {} +func (UnimplementedDataSvcServer) testEmbeddedByValue() {} + +// UnsafeDataSvcServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DataSvcServer will +// result in compilation errors. +type UnsafeDataSvcServer interface { + mustEmbedUnimplementedDataSvcServer() +} + +func RegisterDataSvcServer(s grpc.ServiceRegistrar, srv DataSvcServer) { + // If the following call panics, it indicates UnimplementedDataSvcServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DataSvc_ServiceDesc, srv) +} + +func _DataSvc_CreateApi_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateApiReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).CreateApi(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_CreateApi_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).CreateApi(ctx, req.(*CreateApiReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_UpdateApi_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateApiReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).UpdateApi(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_UpdateApi_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).UpdateApi(ctx, req.(*UpdateApiReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_DeleteApi_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).DeleteApi(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_DeleteApi_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).DeleteApi(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_GetApi_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PathReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).GetApi(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_GetApi_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).GetApi(ctx, req.(*PathReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_ListApis_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApiListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).ListApis(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_ListApis_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).ListApis(ctx, req.(*ApiListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_PublishApi_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).PublishApi(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_PublishApi_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).PublishApi(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_OfflineApi_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).OfflineApi(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_OfflineApi_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).OfflineApi(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_RegisterApp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterAppReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).RegisterApp(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_RegisterApp_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).RegisterApp(ctx, req.(*RegisterAppReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_GrantApi_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GrantApiReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).GrantApi(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_GrantApi_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).GrantApi(ctx, req.(*GrantApiReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _DataSvc_ExecuteApi_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExecuteReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DataSvcServer).ExecuteApi(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DataSvc_ExecuteApi_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DataSvcServer).ExecuteApi(ctx, req.(*ExecuteReq)) + } + return interceptor(ctx, in, info, handler) +} + +// DataSvc_ServiceDesc is the grpc.ServiceDesc for DataSvc service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DataSvc_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "datasvc.DataSvc", + HandlerType: (*DataSvcServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateApi", + Handler: _DataSvc_CreateApi_Handler, + }, + { + MethodName: "UpdateApi", + Handler: _DataSvc_UpdateApi_Handler, + }, + { + MethodName: "DeleteApi", + Handler: _DataSvc_DeleteApi_Handler, + }, + { + MethodName: "GetApi", + Handler: _DataSvc_GetApi_Handler, + }, + { + MethodName: "ListApis", + Handler: _DataSvc_ListApis_Handler, + }, + { + MethodName: "PublishApi", + Handler: _DataSvc_PublishApi_Handler, + }, + { + MethodName: "OfflineApi", + Handler: _DataSvc_OfflineApi_Handler, + }, + { + MethodName: "RegisterApp", + Handler: _DataSvc_RegisterApp_Handler, + }, + { + MethodName: "GrantApi", + Handler: _DataSvc_GrantApi_Handler, + }, + { + MethodName: "ExecuteApi", + Handler: _DataSvc_ExecuteApi_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "datasvc.proto", +} diff --git a/datasvc/rpc/etc/datasvc.example.yaml b/datasvc/rpc/etc/datasvc.example.yaml new file mode 100644 index 0000000..8e84be0 --- /dev/null +++ b/datasvc/rpc/etc/datasvc.example.yaml @@ -0,0 +1,13 @@ +Name: datasvc.rpc +ListenOn: 0.0.0.0:8087 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: datasvc.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +CacheRedis: +- Host: 127.0.0.1:6379 + Pass: admin + Type: node +EncryptKey: 0123456789abcdef0123456789abcdef diff --git a/datasvc/rpc/internal/config/config.go b/datasvc/rpc/internal/config/config.go new file mode 100755 index 0000000..9a5dc6c --- /dev/null +++ b/datasvc/rpc/internal/config/config.go @@ -0,0 +1,15 @@ +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } + CacheRedis cache.ClusterConf + EncryptKey string //AES-GCM 密钥(32字节),用于 app_secret 加密 +} diff --git a/datasvc/rpc/internal/logic/createApiLogic.go b/datasvc/rpc/internal/logic/createApiLogic.go new file mode 100644 index 0000000..d60cb70 --- /dev/null +++ b/datasvc/rpc/internal/logic/createApiLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateApiLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateApiLogic { + return &CreateApiLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建API +func (l *CreateApiLogic) CreateApi(in *datasvc.CreateApiReq) (*datasvc.ApiReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.ApiReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/deleteApiLogic.go b/datasvc/rpc/internal/logic/deleteApiLogic.go new file mode 100644 index 0000000..f0aa252 --- /dev/null +++ b/datasvc/rpc/internal/logic/deleteApiLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteApiLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeleteApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteApiLogic { + return &DeleteApiLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 删除API +func (l *DeleteApiLogic) DeleteApi(in *datasvc.IdReq) (*datasvc.OkReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.OkReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/executeApiLogic.go b/datasvc/rpc/internal/logic/executeApiLogic.go new file mode 100644 index 0000000..e7ee100 --- /dev/null +++ b/datasvc/rpc/internal/logic/executeApiLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ExecuteApiLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewExecuteApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ExecuteApiLogic { + return &ExecuteApiLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 执行 API(核心:鉴权→限流→缓存→执行→脱敏) +func (l *ExecuteApiLogic) ExecuteApi(in *datasvc.ExecuteReq) (*datasvc.ExecuteReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.ExecuteReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/getApiLogic.go b/datasvc/rpc/internal/logic/getApiLogic.go new file mode 100644 index 0000000..01d5116 --- /dev/null +++ b/datasvc/rpc/internal/logic/getApiLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetApiLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApiLogic { + return &GetApiLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// API详情 +func (l *GetApiLogic) GetApi(in *datasvc.PathReq) (*datasvc.ApiReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.ApiReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/grantApiLogic.go b/datasvc/rpc/internal/logic/grantApiLogic.go new file mode 100644 index 0000000..f4fe421 --- /dev/null +++ b/datasvc/rpc/internal/logic/grantApiLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GrantApiLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGrantApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GrantApiLogic { + return &GrantApiLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 授权 API 给应用 +func (l *GrantApiLogic) GrantApi(in *datasvc.GrantApiReq) (*datasvc.OkReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.OkReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/listApisLogic.go b/datasvc/rpc/internal/logic/listApisLogic.go new file mode 100644 index 0000000..1351eff --- /dev/null +++ b/datasvc/rpc/internal/logic/listApisLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListApisLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListApisLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListApisLogic { + return &ListApisLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// API列表 +func (l *ListApisLogic) ListApis(in *datasvc.ApiListReq) (*datasvc.ApiListReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.ApiListReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/offlineApiLogic.go b/datasvc/rpc/internal/logic/offlineApiLogic.go new file mode 100644 index 0000000..b85c4fa --- /dev/null +++ b/datasvc/rpc/internal/logic/offlineApiLogic.go @@ -0,0 +1,30 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type OfflineApiLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewOfflineApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OfflineApiLogic { + return &OfflineApiLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +func (l *OfflineApiLogic) OfflineApi(in *datasvc.IdReq) (*datasvc.OkReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.OkReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/publishApiLogic.go b/datasvc/rpc/internal/logic/publishApiLogic.go new file mode 100644 index 0000000..5f09c72 --- /dev/null +++ b/datasvc/rpc/internal/logic/publishApiLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type PublishApiLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewPublishApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PublishApiLogic { + return &PublishApiLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 发布/下线 +func (l *PublishApiLogic) PublishApi(in *datasvc.IdReq) (*datasvc.OkReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.OkReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/registerAppLogic.go b/datasvc/rpc/internal/logic/registerAppLogic.go new file mode 100644 index 0000000..cfb46ef --- /dev/null +++ b/datasvc/rpc/internal/logic/registerAppLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RegisterAppLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewRegisterAppLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterAppLogic { + return &RegisterAppLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 注册应用(返回 app_key/app_secret) +func (l *RegisterAppLogic) RegisterApp(in *datasvc.RegisterAppReq) (*datasvc.AppReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.AppReply{}, nil +} diff --git a/datasvc/rpc/internal/logic/updateApiLogic.go b/datasvc/rpc/internal/logic/updateApiLogic.go new file mode 100644 index 0000000..d7f0f19 --- /dev/null +++ b/datasvc/rpc/internal/logic/updateApiLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateApiLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewUpdateApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApiLogic { + return &UpdateApiLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 更新API +func (l *UpdateApiLogic) UpdateApi(in *datasvc.UpdateApiReq) (*datasvc.OkReply, error) { + // todo: add your logic here and delete this line + + return &datasvc.OkReply{}, nil +} diff --git a/datasvc/rpc/internal/server/dataSvcServer.go b/datasvc/rpc/internal/server/dataSvcServer.go new file mode 100644 index 0000000..83c8281 --- /dev/null +++ b/datasvc/rpc/internal/server/dataSvcServer.go @@ -0,0 +1,83 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: datasvc.proto + +package server + +import ( + "context" + + "datacenter/datasvc/rpc/datasvc" + "datacenter/datasvc/rpc/internal/logic" + "datacenter/datasvc/rpc/internal/svc" +) + +type DataSvcServer struct { + svcCtx *svc.ServiceContext + datasvc.UnimplementedDataSvcServer +} + +func NewDataSvcServer(svcCtx *svc.ServiceContext) *DataSvcServer { + return &DataSvcServer{ + svcCtx: svcCtx, + } +} + +// 创建API +func (s *DataSvcServer) CreateApi(ctx context.Context, in *datasvc.CreateApiReq) (*datasvc.ApiReply, error) { + l := logic.NewCreateApiLogic(ctx, s.svcCtx) + return l.CreateApi(in) +} + +// 更新API +func (s *DataSvcServer) UpdateApi(ctx context.Context, in *datasvc.UpdateApiReq) (*datasvc.OkReply, error) { + l := logic.NewUpdateApiLogic(ctx, s.svcCtx) + return l.UpdateApi(in) +} + +// 删除API +func (s *DataSvcServer) DeleteApi(ctx context.Context, in *datasvc.IdReq) (*datasvc.OkReply, error) { + l := logic.NewDeleteApiLogic(ctx, s.svcCtx) + return l.DeleteApi(in) +} + +// API详情 +func (s *DataSvcServer) GetApi(ctx context.Context, in *datasvc.PathReq) (*datasvc.ApiReply, error) { + l := logic.NewGetApiLogic(ctx, s.svcCtx) + return l.GetApi(in) +} + +// API列表 +func (s *DataSvcServer) ListApis(ctx context.Context, in *datasvc.ApiListReq) (*datasvc.ApiListReply, error) { + l := logic.NewListApisLogic(ctx, s.svcCtx) + return l.ListApis(in) +} + +// 发布/下线 +func (s *DataSvcServer) PublishApi(ctx context.Context, in *datasvc.IdReq) (*datasvc.OkReply, error) { + l := logic.NewPublishApiLogic(ctx, s.svcCtx) + return l.PublishApi(in) +} + +func (s *DataSvcServer) OfflineApi(ctx context.Context, in *datasvc.IdReq) (*datasvc.OkReply, error) { + l := logic.NewOfflineApiLogic(ctx, s.svcCtx) + return l.OfflineApi(in) +} + +// 注册应用(返回 app_key/app_secret) +func (s *DataSvcServer) RegisterApp(ctx context.Context, in *datasvc.RegisterAppReq) (*datasvc.AppReply, error) { + l := logic.NewRegisterAppLogic(ctx, s.svcCtx) + return l.RegisterApp(in) +} + +// 授权 API 给应用 +func (s *DataSvcServer) GrantApi(ctx context.Context, in *datasvc.GrantApiReq) (*datasvc.OkReply, error) { + l := logic.NewGrantApiLogic(ctx, s.svcCtx) + return l.GrantApi(in) +} + +// 执行 API(核心:鉴权→限流→缓存→执行→脱敏) +func (s *DataSvcServer) ExecuteApi(ctx context.Context, in *datasvc.ExecuteReq) (*datasvc.ExecuteReply, error) { + l := logic.NewExecuteApiLogic(ctx, s.svcCtx) + return l.ExecuteApi(in) +} diff --git a/datasvc/rpc/internal/svc/serviceContext.go b/datasvc/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..2d44a40 --- /dev/null +++ b/datasvc/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/datasvc/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/docs/PHASE1.md b/docs/PHASE1.md new file mode 100644 index 0000000..1338807 --- /dev/null +++ b/docs/PHASE1.md @@ -0,0 +1,36 @@ +# Phase 1 开发契约(数据源 / 元数据 / 数据服务 / 资产目录) + +> 本文档是 4 个服务并行开发的**契约**。骨架(proto → pb → logic 桩 → config → example yaml)已生成并提交,各服务实现**只允许修改自己目录内的文件**。 + +## 目录与端口分配 + +| 服务 | 目录 | 端口 | etcd key | 表 | +|---|---|---|---|---| +| 数据源 | `datasource/rpc` | :8085 | `datasource.rpc` | dc_datasource | +| 元数据 | `metadata/rpc` | :8086 | `metadata.rpc` | dc_meta_entity / dc_meta_relation / dc_meta_version | +| 数据服务 | `datasvc/rpc` | :8087 | `datasvc.rpc` | dc_api / dc_api_app / dc_api_grant | +| 资产目录 | `catalog/rpc` | :8088 | `catalog.rpc` | dc_asset / dc_asset_category / dc_asset_apply | + +接口契约:各 `*/rpc/*.proto`(已定稿,不得改 message/rpc 签名)。表结构:`docs/schema/phase1.sql`(已定稿)。 + +## 架构约定 + +1. **网关编排,rpc 纯业务**:跨服务依赖(如 metadata 采集需要 DSN)由网关 logic 先调 datasource-rpc 拿 DSN 再传参,rpc 服务之间不互相调用。唯一例外:`datasvc.ExecuteApi` 内部 import `datacenter/datasource/rpc/dataSourceClient` 获取 DSN(同 module 直连 client)。 +2. **model 层**:对齐现有风格(`*/model/` 目录 + go-zero sqlx/sqlc)。新服务直接用 `sqlx.NewMysql` + 手写 model(不需要 cache 的表用 sqlx;dc_api 可用 sqlc 缓存)。 +3. **密码/密钥加密**:AES-GCM,key 取 `config.EncryptKey`(32 字节)。实现在各自服务内部(datasource:`internal/logic/credential.go`;datasvc 同理),不建共享包。加密结果存 varbinary 列。 +4. **多租户**:所有查询带 `beid` 条件;软删仅 dc_datasource(deleted_at)。 +5. **SQL 占位符**:datasvc 的 SQL 模板用 `:name` 风格参数,执行前校验参数白名单(防注入),仅允许 SELECT 模板执行向导查询;sql_text 若含非 SELECT 前缀(INSERT/UPDATE/DELETE/DROP 等)必须在 CreateApi 时拒绝(mode=sql 允许写语句,写语句不缓存)。 +6. **注释与命名**:中文注释,命名对齐现有代码风格(goZero style)。 + +## 交付要求(每个服务) + +- [ ] `*/model/`:表 model(FindOne/List/Insert/Update/Delete + 业务方法) +- [ ] `internal/logic/*`:填充全部 logic 桩(错误用 `errors.New` 业务错误 + `xzrpc` 语义;对齐 votes 服务的写法) +- [ ] `internal/svc/serviceContext.go`:装配 model 连接 +- [ ] 至少 1 个核心 logic 的单元测试(sqlmock 或纯函数测试;datasvc 重点测 ExecuteApi 的参数校验/签名/脱敏/缓存) +- [ ] `go build .//...` 与 `go vet .//...` 通过 +- [ ] 不修改:proto、pb、client、server、main、config.go、etc/*.example.yaml、shared/、其他服务目录 + +## 网关接入(主线收口,各代理不管) + +`datasource.api` / `metadata.api` / `datasvc.api` / `catalog.api` 四个网关文件与编排 logic 由主线统一完成。 diff --git a/docs/schema/phase1.sql b/docs/schema/phase1.sql new file mode 100644 index 0000000..172054d --- /dev/null +++ b/docs/schema/phase1.sql @@ -0,0 +1,166 @@ +-- ============================================================ +-- Phase 1 数据中台基础表(数据源/元数据/数据服务/资产目录) +-- 目标库: datacenter 字符集: utf8mb4 引擎: InnoDB +-- 约定: 所有表带 beid 多租户字段; created_at/updated_at 由服务端维护 +-- ============================================================ + +-- 数据源登记表 +CREATE TABLE IF NOT EXISTS `dc_datasource` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '数据源id', + `name` varchar(128) NOT NULL COMMENT '名称', + `type` varchar(32) NOT NULL COMMENT '类型: mysql/redis/es/http/kafka', + `host` varchar(255) NOT NULL DEFAULT '' COMMENT '主机', + `port` int NOT NULL DEFAULT 0 COMMENT '端口', + `db_name` varchar(128) NOT NULL DEFAULT '' COMMENT '库名', + `username` varchar(128) NOT NULL DEFAULT '' COMMENT '用户名', + `password_enc` varbinary(512) NOT NULL COMMENT 'AES-GCM 加密后的密码', + `params_json` text COMMENT '扩展参数(json)', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '1.启用 0.停用', + `owner` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人', + `beid` bigint NOT NULL DEFAULT 0 COMMENT '平台id', + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `deleted_at` datetime(3) DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid_type` (`beid`,`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据源登记表'; + +-- 元数据实体表(database/table/column) +CREATE TABLE IF NOT EXISTS `dc_meta_entity` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '实体id', + `entity_type` varchar(32) NOT NULL COMMENT 'database/table/column', + `fqn` varchar(512) NOT NULL COMMENT '全限定名,如 mysql://db.table.column', + `name` varchar(255) NOT NULL COMMENT '名称', + `comment` varchar(1024) NOT NULL DEFAULT '' COMMENT '注释', + `engine` varchar(64) NOT NULL DEFAULT '' COMMENT '存储引擎', + `rows_cnt` bigint NOT NULL DEFAULT 0 COMMENT '行数', + `props_json` text COMMENT '扩展属性(json)', + `classification` varchar(8) NOT NULL DEFAULT 'L2' COMMENT '分级:L1公开/L2内部/L3敏感/L4机密', + `owner` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人', + `ds_id` bigint NOT NULL DEFAULT 0 COMMENT '来源数据源id', + `beid` bigint NOT NULL DEFAULT 0 COMMENT '平台id', + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_beid_fqn` (`beid`,`fqn`), + KEY `idx_beid_type` (`beid`,`entity_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='元数据实体表'; + +-- 元数据关系表(contains/lineage/depends) +CREATE TABLE IF NOT EXISTS `dc_meta_relation` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `rel_type` varchar(32) NOT NULL COMMENT 'contains/lineage/depends', + `from_id` bigint NOT NULL COMMENT '起点实体id', + `to_id` bigint NOT NULL COMMENT '终点实体id', + `props_json` text COMMENT '关系属性(json,如转换SQL摘要)', + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_edge` (`rel_type`,`from_id`,`to_id`), + KEY `idx_to_id` (`to_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='元数据关系/血缘表'; + +-- 元数据版本表(定版快照) +CREATE TABLE IF NOT EXISTS `dc_meta_version` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `entity_id` bigint NOT NULL COMMENT '实体id', + `version` int NOT NULL COMMENT '版本号,实体内自增', + `snapshot_json` mediumtext COMMENT '实体快照(json)', + `operator` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人', + `published_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_entity_version` (`entity_id`,`version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='元数据版本表'; + +-- 数据服务 API 表 +CREATE TABLE IF NOT EXISTS `dc_api` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL COMMENT '名称', + `path` varchar(128) NOT NULL COMMENT '执行路径标识', + `mode` varchar(16) NOT NULL DEFAULT 'sql' COMMENT 'wizard/sql/register', + `datasource_id` bigint NOT NULL COMMENT '绑定的数据源id', + `sql_text` text COMMENT 'SQL模板,参数用 :name 占位', + `params_json` text COMMENT '参数定义[{"name":"","type":"","required":true,"default":""}]', + `cache_ttl` int NOT NULL DEFAULT 0 COMMENT '结果缓存秒数,0不缓存', + `status` varchar(16) NOT NULL DEFAULT 'draft' COMMENT 'draft/published/offline', + `resp_sample_json` text COMMENT '响应样例', + `owner` varchar(64) NOT NULL DEFAULT '', + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_beid_path` (`beid`,`path`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据服务API表'; + +-- 数据服务应用表 +CREATE TABLE IF NOT EXISTS `dc_api_app` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `app_key` varchar(64) NOT NULL COMMENT '应用标识', + `app_secret_enc` varbinary(512) NOT NULL COMMENT 'AES-GCM 加密的密钥', + `name` varchar(128) NOT NULL, + `rate_limit_qps` int NOT NULL DEFAULT 10 COMMENT '限流QPS', + `ip_whitelist` varchar(512) NOT NULL DEFAULT '' COMMENT '白名单,逗号分隔,空为不限', + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_app_key` (`app_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据服务应用表'; + +-- API 授权表 +CREATE TABLE IF NOT EXISTS `dc_api_grant` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `api_id` bigint NOT NULL, + `app_id` bigint NOT NULL, + `expire_at` datetime(3) DEFAULT NULL COMMENT '过期时间,NULL永久', + `field_mask` varchar(512) NOT NULL DEFAULT '' COMMENT '脱敏字段,逗号分隔', + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_api_app` (`api_id`,`app_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='API授权表'; + +-- 资产目录分类表 +CREATE TABLE IF NOT EXISTS `dc_asset_category` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父分类,0为根', + `name` varchar(128) NOT NULL, + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid_parent` (`beid`,`parent_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资产目录分类表'; + +-- 资产表(元数据实体入目录,冗余快照避免跨服务联查) +CREATE TABLE IF NOT EXISTS `dc_asset` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `entity_id` bigint NOT NULL COMMENT '元数据实体id', + `category_id` bigint NOT NULL DEFAULT 0, + `name` varchar(255) NOT NULL COMMENT '资产名(冗余快照)', + `entity_type` varchar(32) NOT NULL DEFAULT '' COMMENT '冗余快照', + `comment` varchar(1024) NOT NULL DEFAULT '' COMMENT '冗余快照', + `classification` varchar(8) NOT NULL DEFAULT 'L2' COMMENT '冗余快照', + `asset_tags_json` text COMMENT '资产标签(json)', + `visibility` varchar(16) NOT NULL DEFAULT 'private' COMMENT 'public/private', + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_beid_entity` (`beid`,`entity_id`), + KEY `idx_category` (`category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资产表'; + +-- 资产申请使用表 +CREATE TABLE IF NOT EXISTS `dc_asset_apply` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `asset_id` bigint NOT NULL, + `reason` varchar(1024) NOT NULL DEFAULT '' COMMENT '申请理由', + `applicant` varchar(64) NOT NULL DEFAULT '' COMMENT '申请人', + `status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected', + `reviewer` varchar(64) NOT NULL DEFAULT '' COMMENT '审批人', + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid_status` (`beid`,`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资产申请使用表'; diff --git a/metadata/rpc/etc/metadata.example.yaml b/metadata/rpc/etc/metadata.example.yaml new file mode 100644 index 0000000..f3bfb10 --- /dev/null +++ b/metadata/rpc/etc/metadata.example.yaml @@ -0,0 +1,8 @@ +Name: metadata.rpc +ListenOn: 0.0.0.0:8086 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: metadata.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/metadata/rpc/internal/config/config.go b/metadata/rpc/internal/config/config.go new file mode 100755 index 0000000..96863e9 --- /dev/null +++ b/metadata/rpc/internal/config/config.go @@ -0,0 +1,10 @@ +package config + +import "github.com/zeromicro/go-zero/zrpc" + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } +} diff --git a/metadata/rpc/internal/logic/addLineageLogic.go b/metadata/rpc/internal/logic/addLineageLogic.go new file mode 100644 index 0000000..7f46c8b --- /dev/null +++ b/metadata/rpc/internal/logic/addLineageLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AddLineageLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewAddLineageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddLineageLogic { + return &AddLineageLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 登记血缘 +func (l *AddLineageLogic) AddLineage(in *metadata.AddLineageReq) (*metadata.OkReply, error) { + // todo: add your logic here and delete this line + + return &metadata.OkReply{}, nil +} diff --git a/metadata/rpc/internal/logic/collectDatabaseLogic.go b/metadata/rpc/internal/logic/collectDatabaseLogic.go new file mode 100644 index 0000000..6613f83 --- /dev/null +++ b/metadata/rpc/internal/logic/collectDatabaseLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CollectDatabaseLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCollectDatabaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CollectDatabaseLogic { + return &CollectDatabaseLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 采集库表元数据(含 contains 关系) +func (l *CollectDatabaseLogic) CollectDatabase(in *metadata.CollectDatabaseReq) (*metadata.CollectReply, error) { + // todo: add your logic here and delete this line + + return &metadata.CollectReply{}, nil +} diff --git a/metadata/rpc/internal/logic/compareVersionsLogic.go b/metadata/rpc/internal/logic/compareVersionsLogic.go new file mode 100644 index 0000000..46e2617 --- /dev/null +++ b/metadata/rpc/internal/logic/compareVersionsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CompareVersionsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCompareVersionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CompareVersionsLogic { + return &CompareVersionsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 版本对比 +func (l *CompareVersionsLogic) CompareVersions(in *metadata.CompareReq) (*metadata.DiffReply, error) { + // todo: add your logic here and delete this line + + return &metadata.DiffReply{}, nil +} diff --git a/metadata/rpc/internal/logic/createVersionLogic.go b/metadata/rpc/internal/logic/createVersionLogic.go new file mode 100644 index 0000000..dd4ed1c --- /dev/null +++ b/metadata/rpc/internal/logic/createVersionLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateVersionLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateVersionLogic { + return &CreateVersionLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 定版快照 +func (l *CreateVersionLogic) CreateVersion(in *metadata.IdReq) (*metadata.VersionReply, error) { + // todo: add your logic here and delete this line + + return &metadata.VersionReply{}, nil +} diff --git a/metadata/rpc/internal/logic/getEntityLogic.go b/metadata/rpc/internal/logic/getEntityLogic.go new file mode 100644 index 0000000..98f9a12 --- /dev/null +++ b/metadata/rpc/internal/logic/getEntityLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetEntityLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetEntityLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetEntityLogic { + return &GetEntityLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 实体详情 +func (l *GetEntityLogic) GetEntity(in *metadata.IdReq) (*metadata.EntityReply, error) { + // todo: add your logic here and delete this line + + return &metadata.EntityReply{}, nil +} diff --git a/metadata/rpc/internal/logic/getLineageLogic.go b/metadata/rpc/internal/logic/getLineageLogic.go new file mode 100644 index 0000000..582fb40 --- /dev/null +++ b/metadata/rpc/internal/logic/getLineageLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetLineageLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetLineageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLineageLogic { + return &GetLineageLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 血缘图(上下游 N 跳) +func (l *GetLineageLogic) GetLineage(in *metadata.LineageReq) (*metadata.LineageReply, error) { + // todo: add your logic here and delete this line + + return &metadata.LineageReply{}, nil +} diff --git a/metadata/rpc/internal/logic/listEntitiesLogic.go b/metadata/rpc/internal/logic/listEntitiesLogic.go new file mode 100644 index 0000000..4d58cca --- /dev/null +++ b/metadata/rpc/internal/logic/listEntitiesLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListEntitiesLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListEntitiesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListEntitiesLogic { + return &ListEntitiesLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 实体列表 +func (l *ListEntitiesLogic) ListEntities(in *metadata.EntityListReq) (*metadata.EntityListReply, error) { + // todo: add your logic here and delete this line + + return &metadata.EntityListReply{}, nil +} diff --git a/metadata/rpc/internal/logic/listVersionsLogic.go b/metadata/rpc/internal/logic/listVersionsLogic.go new file mode 100644 index 0000000..0b202c8 --- /dev/null +++ b/metadata/rpc/internal/logic/listVersionsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListVersionsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListVersionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListVersionsLogic { + return &ListVersionsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 版本列表 +func (l *ListVersionsLogic) ListVersions(in *metadata.IdReq) (*metadata.VersionListReply, error) { + // todo: add your logic here and delete this line + + return &metadata.VersionListReply{}, nil +} diff --git a/metadata/rpc/internal/server/metadataServer.go b/metadata/rpc/internal/server/metadataServer.go new file mode 100644 index 0000000..d14baf3 --- /dev/null +++ b/metadata/rpc/internal/server/metadataServer.go @@ -0,0 +1,72 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: metadata.proto + +package server + +import ( + "context" + + "datacenter/metadata/rpc/internal/logic" + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" +) + +type MetadataServer struct { + svcCtx *svc.ServiceContext + metadata.UnimplementedMetadataServer +} + +func NewMetadataServer(svcCtx *svc.ServiceContext) *MetadataServer { + return &MetadataServer{ + svcCtx: svcCtx, + } +} + +// 采集库表元数据(含 contains 关系) +func (s *MetadataServer) CollectDatabase(ctx context.Context, in *metadata.CollectDatabaseReq) (*metadata.CollectReply, error) { + l := logic.NewCollectDatabaseLogic(ctx, s.svcCtx) + return l.CollectDatabase(in) +} + +// 实体详情 +func (s *MetadataServer) GetEntity(ctx context.Context, in *metadata.IdReq) (*metadata.EntityReply, error) { + l := logic.NewGetEntityLogic(ctx, s.svcCtx) + return l.GetEntity(in) +} + +// 实体列表 +func (s *MetadataServer) ListEntities(ctx context.Context, in *metadata.EntityListReq) (*metadata.EntityListReply, error) { + l := logic.NewListEntitiesLogic(ctx, s.svcCtx) + return l.ListEntities(in) +} + +// 登记血缘 +func (s *MetadataServer) AddLineage(ctx context.Context, in *metadata.AddLineageReq) (*metadata.OkReply, error) { + l := logic.NewAddLineageLogic(ctx, s.svcCtx) + return l.AddLineage(in) +} + +// 血缘图(上下游 N 跳) +func (s *MetadataServer) GetLineage(ctx context.Context, in *metadata.LineageReq) (*metadata.LineageReply, error) { + l := logic.NewGetLineageLogic(ctx, s.svcCtx) + return l.GetLineage(in) +} + +// 定版快照 +func (s *MetadataServer) CreateVersion(ctx context.Context, in *metadata.IdReq) (*metadata.VersionReply, error) { + l := logic.NewCreateVersionLogic(ctx, s.svcCtx) + return l.CreateVersion(in) +} + +// 版本列表 +func (s *MetadataServer) ListVersions(ctx context.Context, in *metadata.IdReq) (*metadata.VersionListReply, error) { + l := logic.NewListVersionsLogic(ctx, s.svcCtx) + return l.ListVersions(in) +} + +// 版本对比 +func (s *MetadataServer) CompareVersions(ctx context.Context, in *metadata.CompareReq) (*metadata.DiffReply, error) { + l := logic.NewCompareVersionsLogic(ctx, s.svcCtx) + return l.CompareVersions(in) +} diff --git a/metadata/rpc/internal/svc/serviceContext.go b/metadata/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..a9dcb38 --- /dev/null +++ b/metadata/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/metadata/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/metadata/rpc/metadata.go b/metadata/rpc/metadata.go new file mode 100644 index 0000000..0894aa0 --- /dev/null +++ b/metadata/rpc/metadata.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/metadata/rpc/internal/config" + "datacenter/metadata/rpc/internal/server" + "datacenter/metadata/rpc/internal/svc" + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/metadata.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + metadata.RegisterMetadataServer(grpcServer, server.NewMetadataServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/metadata/rpc/metadata.proto b/metadata/rpc/metadata.proto new file mode 100644 index 0000000..b31a30f --- /dev/null +++ b/metadata/rpc/metadata.proto @@ -0,0 +1,118 @@ +syntax = "proto3"; + +package metadata; + +option go_package = "./metadata"; + +//元数据实体 +message MetaEntity { + int64 id = 1; + string entity_type = 2; //database/table/column + string fqn = 3; //全限定名 mysql://{db}.{table}.{column} + string name = 4; + string comment = 5; + string engine = 6; + int64 rows_cnt = 7; + string props_json = 8; //扩展属性(json) + string classification = 9; //L1/L2/L3/L4 分级 + string owner = 10; + int64 ds_id = 11; //来源数据源id + int64 beid = 12; +} + +message EntityReply { MetaEntity entity = 1; } +message EntityListReply { + repeated MetaEntity list = 1; + int64 total = 2; +} + +message CollectDatabaseReq { + int64 ds_id = 1; //数据源id(血缘登记用) + int64 beid = 2; + string db_type = 3; //mysql + string dsn = 4; //由调用方(网关编排)传入 + string db_name = 5; +} +message CollectReply { + int64 tables_created = 1; + int64 columns_created = 2; +} + +message AddLineageReq { + int64 from_id = 1; + int64 to_id = 2; + string props_json = 3; //如转换SQL摘要 + int64 beid = 4; +} + +message LineageNode { + int64 id = 1; + string fqn = 2; + string name = 3; +} +message LineageEdge { + int64 from_id = 1; + int64 to_id = 2; + string props_json = 3; +} +message LineageReply { + repeated LineageNode nodes = 1; + repeated LineageEdge edges = 2; +} + +message LineageReq { + int64 entity_id = 1; + string direction = 2; //upstream/downstream + int32 depth = 3; + int64 beid = 4; +} + +message EntityListReq { + int64 beid = 1; + string entity_type = 2; //为空查全部 + string keyword = 3; //fqn/name 模糊 + int64 ds_id = 4; + int64 page = 5; + int64 size = 6; +} + +message MetaVersion { + int64 id = 1; + int64 entity_id = 2; + int64 version = 3; + string snapshot_json = 4; + string operator = 5; + int64 published_at = 6; +} +message VersionReply { MetaVersion version = 1; } +message VersionListReply { repeated MetaVersion list = 1; } + +message CompareReq { + int64 version_a = 1; + int64 version_b = 2; +} +message DiffReply { + string diff_json = 1; //[]{field,before,after} +} + +service Metadata { + //采集库表元数据(含 contains 关系) + rpc CollectDatabase(CollectDatabaseReq) returns (CollectReply); + //实体详情 + rpc GetEntity(IdReq) returns (EntityReply); + //实体列表 + rpc ListEntities(EntityListReq) returns (EntityListReply); + //登记血缘 + rpc AddLineage(AddLineageReq) returns (OkReply); + //血缘图(上下游 N 跳) + rpc GetLineage(LineageReq) returns (LineageReply); + //定版快照 + rpc CreateVersion(IdReq) returns (VersionReply); + //版本列表 + rpc ListVersions(IdReq) returns (VersionListReply); + //版本对比 + rpc CompareVersions(CompareReq) returns (DiffReply); +} + +message IdReq { int64 id = 1; int64 beid = 2; } +message OkReply { bool ok = 1; } diff --git a/metadata/rpc/metadata/metadata.pb.go b/metadata/rpc/metadata/metadata.pb.go new file mode 100644 index 0000000..4386925 --- /dev/null +++ b/metadata/rpc/metadata/metadata.pb.go @@ -0,0 +1,1325 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: metadata.proto + +package metadata + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// 元数据实体 +type MetaEntity struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + EntityType string `protobuf:"bytes,2,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"` //database/table/column + Fqn string `protobuf:"bytes,3,opt,name=fqn,proto3" json:"fqn,omitempty"` //全限定名 mysql://{db}.{table}.{column} + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + Comment string `protobuf:"bytes,5,opt,name=comment,proto3" json:"comment,omitempty"` + Engine string `protobuf:"bytes,6,opt,name=engine,proto3" json:"engine,omitempty"` + RowsCnt int64 `protobuf:"varint,7,opt,name=rows_cnt,json=rowsCnt,proto3" json:"rows_cnt,omitempty"` + PropsJson string `protobuf:"bytes,8,opt,name=props_json,json=propsJson,proto3" json:"props_json,omitempty"` //扩展属性(json) + Classification string `protobuf:"bytes,9,opt,name=classification,proto3" json:"classification,omitempty"` //L1/L2/L3/L4 分级 + Owner string `protobuf:"bytes,10,opt,name=owner,proto3" json:"owner,omitempty"` + DsId int64 `protobuf:"varint,11,opt,name=ds_id,json=dsId,proto3" json:"ds_id,omitempty"` //来源数据源id + Beid int64 `protobuf:"varint,12,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MetaEntity) Reset() { + *x = MetaEntity{} + mi := &file_metadata_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MetaEntity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MetaEntity) ProtoMessage() {} + +func (x *MetaEntity) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MetaEntity.ProtoReflect.Descriptor instead. +func (*MetaEntity) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{0} +} + +func (x *MetaEntity) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *MetaEntity) GetEntityType() string { + if x != nil { + return x.EntityType + } + return "" +} + +func (x *MetaEntity) GetFqn() string { + if x != nil { + return x.Fqn + } + return "" +} + +func (x *MetaEntity) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *MetaEntity) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +func (x *MetaEntity) GetEngine() string { + if x != nil { + return x.Engine + } + return "" +} + +func (x *MetaEntity) GetRowsCnt() int64 { + if x != nil { + return x.RowsCnt + } + return 0 +} + +func (x *MetaEntity) GetPropsJson() string { + if x != nil { + return x.PropsJson + } + return "" +} + +func (x *MetaEntity) GetClassification() string { + if x != nil { + return x.Classification + } + return "" +} + +func (x *MetaEntity) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *MetaEntity) GetDsId() int64 { + if x != nil { + return x.DsId + } + return 0 +} + +func (x *MetaEntity) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type EntityReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entity *MetaEntity `protobuf:"bytes,1,opt,name=entity,proto3" json:"entity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EntityReply) Reset() { + *x = EntityReply{} + mi := &file_metadata_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EntityReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntityReply) ProtoMessage() {} + +func (x *EntityReply) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EntityReply.ProtoReflect.Descriptor instead. +func (*EntityReply) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{1} +} + +func (x *EntityReply) GetEntity() *MetaEntity { + if x != nil { + return x.Entity + } + return nil +} + +type EntityListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*MetaEntity `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EntityListReply) Reset() { + *x = EntityListReply{} + mi := &file_metadata_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EntityListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntityListReply) ProtoMessage() {} + +func (x *EntityListReply) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EntityListReply.ProtoReflect.Descriptor instead. +func (*EntityListReply) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{2} +} + +func (x *EntityListReply) GetList() []*MetaEntity { + if x != nil { + return x.List + } + return nil +} + +func (x *EntityListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type CollectDatabaseReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + DsId int64 `protobuf:"varint,1,opt,name=ds_id,json=dsId,proto3" json:"ds_id,omitempty"` //数据源id(血缘登记用) + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + DbType string `protobuf:"bytes,3,opt,name=db_type,json=dbType,proto3" json:"db_type,omitempty"` //mysql + Dsn string `protobuf:"bytes,4,opt,name=dsn,proto3" json:"dsn,omitempty"` //由调用方(网关编排)传入 + DbName string `protobuf:"bytes,5,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CollectDatabaseReq) Reset() { + *x = CollectDatabaseReq{} + mi := &file_metadata_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CollectDatabaseReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CollectDatabaseReq) ProtoMessage() {} + +func (x *CollectDatabaseReq) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CollectDatabaseReq.ProtoReflect.Descriptor instead. +func (*CollectDatabaseReq) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{3} +} + +func (x *CollectDatabaseReq) GetDsId() int64 { + if x != nil { + return x.DsId + } + return 0 +} + +func (x *CollectDatabaseReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *CollectDatabaseReq) GetDbType() string { + if x != nil { + return x.DbType + } + return "" +} + +func (x *CollectDatabaseReq) GetDsn() string { + if x != nil { + return x.Dsn + } + return "" +} + +func (x *CollectDatabaseReq) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +type CollectReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + TablesCreated int64 `protobuf:"varint,1,opt,name=tables_created,json=tablesCreated,proto3" json:"tables_created,omitempty"` + ColumnsCreated int64 `protobuf:"varint,2,opt,name=columns_created,json=columnsCreated,proto3" json:"columns_created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CollectReply) Reset() { + *x = CollectReply{} + mi := &file_metadata_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CollectReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CollectReply) ProtoMessage() {} + +func (x *CollectReply) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CollectReply.ProtoReflect.Descriptor instead. +func (*CollectReply) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{4} +} + +func (x *CollectReply) GetTablesCreated() int64 { + if x != nil { + return x.TablesCreated + } + return 0 +} + +func (x *CollectReply) GetColumnsCreated() int64 { + if x != nil { + return x.ColumnsCreated + } + return 0 +} + +type AddLineageReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromId int64 `protobuf:"varint,1,opt,name=from_id,json=fromId,proto3" json:"from_id,omitempty"` + ToId int64 `protobuf:"varint,2,opt,name=to_id,json=toId,proto3" json:"to_id,omitempty"` + PropsJson string `protobuf:"bytes,3,opt,name=props_json,json=propsJson,proto3" json:"props_json,omitempty"` //如转换SQL摘要 + Beid int64 `protobuf:"varint,4,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddLineageReq) Reset() { + *x = AddLineageReq{} + mi := &file_metadata_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddLineageReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddLineageReq) ProtoMessage() {} + +func (x *AddLineageReq) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddLineageReq.ProtoReflect.Descriptor instead. +func (*AddLineageReq) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{5} +} + +func (x *AddLineageReq) GetFromId() int64 { + if x != nil { + return x.FromId + } + return 0 +} + +func (x *AddLineageReq) GetToId() int64 { + if x != nil { + return x.ToId + } + return 0 +} + +func (x *AddLineageReq) GetPropsJson() string { + if x != nil { + return x.PropsJson + } + return "" +} + +func (x *AddLineageReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type LineageNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Fqn string `protobuf:"bytes,2,opt,name=fqn,proto3" json:"fqn,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LineageNode) Reset() { + *x = LineageNode{} + mi := &file_metadata_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LineageNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LineageNode) ProtoMessage() {} + +func (x *LineageNode) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LineageNode.ProtoReflect.Descriptor instead. +func (*LineageNode) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{6} +} + +func (x *LineageNode) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *LineageNode) GetFqn() string { + if x != nil { + return x.Fqn + } + return "" +} + +func (x *LineageNode) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type LineageEdge struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromId int64 `protobuf:"varint,1,opt,name=from_id,json=fromId,proto3" json:"from_id,omitempty"` + ToId int64 `protobuf:"varint,2,opt,name=to_id,json=toId,proto3" json:"to_id,omitempty"` + PropsJson string `protobuf:"bytes,3,opt,name=props_json,json=propsJson,proto3" json:"props_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LineageEdge) Reset() { + *x = LineageEdge{} + mi := &file_metadata_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LineageEdge) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LineageEdge) ProtoMessage() {} + +func (x *LineageEdge) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LineageEdge.ProtoReflect.Descriptor instead. +func (*LineageEdge) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{7} +} + +func (x *LineageEdge) GetFromId() int64 { + if x != nil { + return x.FromId + } + return 0 +} + +func (x *LineageEdge) GetToId() int64 { + if x != nil { + return x.ToId + } + return 0 +} + +func (x *LineageEdge) GetPropsJson() string { + if x != nil { + return x.PropsJson + } + return "" +} + +type LineageReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Nodes []*LineageNode `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` + Edges []*LineageEdge `protobuf:"bytes,2,rep,name=edges,proto3" json:"edges,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LineageReply) Reset() { + *x = LineageReply{} + mi := &file_metadata_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LineageReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LineageReply) ProtoMessage() {} + +func (x *LineageReply) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LineageReply.ProtoReflect.Descriptor instead. +func (*LineageReply) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{8} +} + +func (x *LineageReply) GetNodes() []*LineageNode { + if x != nil { + return x.Nodes + } + return nil +} + +func (x *LineageReply) GetEdges() []*LineageEdge { + if x != nil { + return x.Edges + } + return nil +} + +type LineageReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId int64 `protobuf:"varint,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + Direction string `protobuf:"bytes,2,opt,name=direction,proto3" json:"direction,omitempty"` //upstream/downstream + Depth int32 `protobuf:"varint,3,opt,name=depth,proto3" json:"depth,omitempty"` + Beid int64 `protobuf:"varint,4,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LineageReq) Reset() { + *x = LineageReq{} + mi := &file_metadata_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LineageReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LineageReq) ProtoMessage() {} + +func (x *LineageReq) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LineageReq.ProtoReflect.Descriptor instead. +func (*LineageReq) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{9} +} + +func (x *LineageReq) GetEntityId() int64 { + if x != nil { + return x.EntityId + } + return 0 +} + +func (x *LineageReq) GetDirection() string { + if x != nil { + return x.Direction + } + return "" +} + +func (x *LineageReq) GetDepth() int32 { + if x != nil { + return x.Depth + } + return 0 +} + +func (x *LineageReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type EntityListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + EntityType string `protobuf:"bytes,2,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"` //为空查全部 + Keyword string `protobuf:"bytes,3,opt,name=keyword,proto3" json:"keyword,omitempty"` //fqn/name 模糊 + DsId int64 `protobuf:"varint,4,opt,name=ds_id,json=dsId,proto3" json:"ds_id,omitempty"` + Page int64 `protobuf:"varint,5,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EntityListReq) Reset() { + *x = EntityListReq{} + mi := &file_metadata_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EntityListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntityListReq) ProtoMessage() {} + +func (x *EntityListReq) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EntityListReq.ProtoReflect.Descriptor instead. +func (*EntityListReq) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{10} +} + +func (x *EntityListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *EntityListReq) GetEntityType() string { + if x != nil { + return x.EntityType + } + return "" +} + +func (x *EntityListReq) GetKeyword() string { + if x != nil { + return x.Keyword + } + return "" +} + +func (x *EntityListReq) GetDsId() int64 { + if x != nil { + return x.DsId + } + return 0 +} + +func (x *EntityListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *EntityListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type MetaVersion struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + EntityId int64 `protobuf:"varint,2,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + Version int64 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` + SnapshotJson string `protobuf:"bytes,4,opt,name=snapshot_json,json=snapshotJson,proto3" json:"snapshot_json,omitempty"` + Operator string `protobuf:"bytes,5,opt,name=operator,proto3" json:"operator,omitempty"` + PublishedAt int64 `protobuf:"varint,6,opt,name=published_at,json=publishedAt,proto3" json:"published_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MetaVersion) Reset() { + *x = MetaVersion{} + mi := &file_metadata_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MetaVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MetaVersion) ProtoMessage() {} + +func (x *MetaVersion) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MetaVersion.ProtoReflect.Descriptor instead. +func (*MetaVersion) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{11} +} + +func (x *MetaVersion) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *MetaVersion) GetEntityId() int64 { + if x != nil { + return x.EntityId + } + return 0 +} + +func (x *MetaVersion) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *MetaVersion) GetSnapshotJson() string { + if x != nil { + return x.SnapshotJson + } + return "" +} + +func (x *MetaVersion) GetOperator() string { + if x != nil { + return x.Operator + } + return "" +} + +func (x *MetaVersion) GetPublishedAt() int64 { + if x != nil { + return x.PublishedAt + } + return 0 +} + +type VersionReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version *MetaVersion `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VersionReply) Reset() { + *x = VersionReply{} + mi := &file_metadata_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VersionReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VersionReply) ProtoMessage() {} + +func (x *VersionReply) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VersionReply.ProtoReflect.Descriptor instead. +func (*VersionReply) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{12} +} + +func (x *VersionReply) GetVersion() *MetaVersion { + if x != nil { + return x.Version + } + return nil +} + +type VersionListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*MetaVersion `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VersionListReply) Reset() { + *x = VersionListReply{} + mi := &file_metadata_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VersionListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VersionListReply) ProtoMessage() {} + +func (x *VersionListReply) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VersionListReply.ProtoReflect.Descriptor instead. +func (*VersionListReply) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{13} +} + +func (x *VersionListReply) GetList() []*MetaVersion { + if x != nil { + return x.List + } + return nil +} + +type CompareReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + VersionA int64 `protobuf:"varint,1,opt,name=version_a,json=versionA,proto3" json:"version_a,omitempty"` + VersionB int64 `protobuf:"varint,2,opt,name=version_b,json=versionB,proto3" json:"version_b,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CompareReq) Reset() { + *x = CompareReq{} + mi := &file_metadata_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CompareReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CompareReq) ProtoMessage() {} + +func (x *CompareReq) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CompareReq.ProtoReflect.Descriptor instead. +func (*CompareReq) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{14} +} + +func (x *CompareReq) GetVersionA() int64 { + if x != nil { + return x.VersionA + } + return 0 +} + +func (x *CompareReq) GetVersionB() int64 { + if x != nil { + return x.VersionB + } + return 0 +} + +type DiffReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + DiffJson string `protobuf:"bytes,1,opt,name=diff_json,json=diffJson,proto3" json:"diff_json,omitempty"` //[]{field,before,after} + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiffReply) Reset() { + *x = DiffReply{} + mi := &file_metadata_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiffReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiffReply) ProtoMessage() {} + +func (x *DiffReply) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiffReply.ProtoReflect.Descriptor instead. +func (*DiffReply) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{15} +} + +func (x *DiffReply) GetDiffJson() string { + if x != nil { + return x.DiffJson + } + return "" +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_metadata_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{16} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type OkReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OkReply) Reset() { + *x = OkReply{} + mi := &file_metadata_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OkReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OkReply) ProtoMessage() {} + +func (x *OkReply) ProtoReflect() protoreflect.Message { + mi := &file_metadata_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OkReply.ProtoReflect.Descriptor instead. +func (*OkReply) Descriptor() ([]byte, []int) { + return file_metadata_proto_rawDescGZIP(), []int{17} +} + +func (x *OkReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +var File_metadata_proto protoreflect.FileDescriptor + +const file_metadata_proto_rawDesc = "" + + "\n" + + "\x0emetadata.proto\x12\bmetadata\"\xb6\x02\n" + + "\n" + + "MetaEntity\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1f\n" + + "\ventity_type\x18\x02 \x01(\tR\n" + + "entityType\x12\x10\n" + + "\x03fqn\x18\x03 \x01(\tR\x03fqn\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12\x18\n" + + "\acomment\x18\x05 \x01(\tR\acomment\x12\x16\n" + + "\x06engine\x18\x06 \x01(\tR\x06engine\x12\x19\n" + + "\brows_cnt\x18\a \x01(\x03R\arowsCnt\x12\x1d\n" + + "\n" + + "props_json\x18\b \x01(\tR\tpropsJson\x12&\n" + + "\x0eclassification\x18\t \x01(\tR\x0eclassification\x12\x14\n" + + "\x05owner\x18\n" + + " \x01(\tR\x05owner\x12\x13\n" + + "\x05ds_id\x18\v \x01(\x03R\x04dsId\x12\x12\n" + + "\x04beid\x18\f \x01(\x03R\x04beid\";\n" + + "\vEntityReply\x12,\n" + + "\x06entity\x18\x01 \x01(\v2\x14.metadata.MetaEntityR\x06entity\"Q\n" + + "\x0fEntityListReply\x12(\n" + + "\x04list\x18\x01 \x03(\v2\x14.metadata.MetaEntityR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\x81\x01\n" + + "\x12CollectDatabaseReq\x12\x13\n" + + "\x05ds_id\x18\x01 \x01(\x03R\x04dsId\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\x12\x17\n" + + "\adb_type\x18\x03 \x01(\tR\x06dbType\x12\x10\n" + + "\x03dsn\x18\x04 \x01(\tR\x03dsn\x12\x17\n" + + "\adb_name\x18\x05 \x01(\tR\x06dbName\"^\n" + + "\fCollectReply\x12%\n" + + "\x0etables_created\x18\x01 \x01(\x03R\rtablesCreated\x12'\n" + + "\x0fcolumns_created\x18\x02 \x01(\x03R\x0ecolumnsCreated\"p\n" + + "\rAddLineageReq\x12\x17\n" + + "\afrom_id\x18\x01 \x01(\x03R\x06fromId\x12\x13\n" + + "\x05to_id\x18\x02 \x01(\x03R\x04toId\x12\x1d\n" + + "\n" + + "props_json\x18\x03 \x01(\tR\tpropsJson\x12\x12\n" + + "\x04beid\x18\x04 \x01(\x03R\x04beid\"C\n" + + "\vLineageNode\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x10\n" + + "\x03fqn\x18\x02 \x01(\tR\x03fqn\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"Z\n" + + "\vLineageEdge\x12\x17\n" + + "\afrom_id\x18\x01 \x01(\x03R\x06fromId\x12\x13\n" + + "\x05to_id\x18\x02 \x01(\x03R\x04toId\x12\x1d\n" + + "\n" + + "props_json\x18\x03 \x01(\tR\tpropsJson\"h\n" + + "\fLineageReply\x12+\n" + + "\x05nodes\x18\x01 \x03(\v2\x15.metadata.LineageNodeR\x05nodes\x12+\n" + + "\x05edges\x18\x02 \x03(\v2\x15.metadata.LineageEdgeR\x05edges\"q\n" + + "\n" + + "LineageReq\x12\x1b\n" + + "\tentity_id\x18\x01 \x01(\x03R\bentityId\x12\x1c\n" + + "\tdirection\x18\x02 \x01(\tR\tdirection\x12\x14\n" + + "\x05depth\x18\x03 \x01(\x05R\x05depth\x12\x12\n" + + "\x04beid\x18\x04 \x01(\x03R\x04beid\"\x9b\x01\n" + + "\rEntityListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x1f\n" + + "\ventity_type\x18\x02 \x01(\tR\n" + + "entityType\x12\x18\n" + + "\akeyword\x18\x03 \x01(\tR\akeyword\x12\x13\n" + + "\x05ds_id\x18\x04 \x01(\x03R\x04dsId\x12\x12\n" + + "\x04page\x18\x05 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x06 \x01(\x03R\x04size\"\xb8\x01\n" + + "\vMetaVersion\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1b\n" + + "\tentity_id\x18\x02 \x01(\x03R\bentityId\x12\x18\n" + + "\aversion\x18\x03 \x01(\x03R\aversion\x12#\n" + + "\rsnapshot_json\x18\x04 \x01(\tR\fsnapshotJson\x12\x1a\n" + + "\boperator\x18\x05 \x01(\tR\boperator\x12!\n" + + "\fpublished_at\x18\x06 \x01(\x03R\vpublishedAt\"?\n" + + "\fVersionReply\x12/\n" + + "\aversion\x18\x01 \x01(\v2\x15.metadata.MetaVersionR\aversion\"=\n" + + "\x10VersionListReply\x12)\n" + + "\x04list\x18\x01 \x03(\v2\x15.metadata.MetaVersionR\x04list\"F\n" + + "\n" + + "CompareReq\x12\x1b\n" + + "\tversion_a\x18\x01 \x01(\x03R\bversionA\x12\x1b\n" + + "\tversion_b\x18\x02 \x01(\x03R\bversionB\"(\n" + + "\tDiffReply\x12\x1b\n" + + "\tdiff_json\x18\x01 \x01(\tR\bdiffJson\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"\x19\n" + + "\aOkReply\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok2\xf7\x03\n" + + "\bMetadata\x12G\n" + + "\x0fCollectDatabase\x12\x1c.metadata.CollectDatabaseReq\x1a\x16.metadata.CollectReply\x123\n" + + "\tGetEntity\x12\x0f.metadata.IdReq\x1a\x15.metadata.EntityReply\x12B\n" + + "\fListEntities\x12\x17.metadata.EntityListReq\x1a\x19.metadata.EntityListReply\x128\n" + + "\n" + + "AddLineage\x12\x17.metadata.AddLineageReq\x1a\x11.metadata.OkReply\x12:\n" + + "\n" + + "GetLineage\x12\x14.metadata.LineageReq\x1a\x16.metadata.LineageReply\x128\n" + + "\rCreateVersion\x12\x0f.metadata.IdReq\x1a\x16.metadata.VersionReply\x12;\n" + + "\fListVersions\x12\x0f.metadata.IdReq\x1a\x1a.metadata.VersionListReply\x12<\n" + + "\x0fCompareVersions\x12\x14.metadata.CompareReq\x1a\x13.metadata.DiffReplyB\fZ\n" + + "./metadatab\x06proto3" + +var ( + file_metadata_proto_rawDescOnce sync.Once + file_metadata_proto_rawDescData []byte +) + +func file_metadata_proto_rawDescGZIP() []byte { + file_metadata_proto_rawDescOnce.Do(func() { + file_metadata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_metadata_proto_rawDesc), len(file_metadata_proto_rawDesc))) + }) + return file_metadata_proto_rawDescData +} + +var file_metadata_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_metadata_proto_goTypes = []any{ + (*MetaEntity)(nil), // 0: metadata.MetaEntity + (*EntityReply)(nil), // 1: metadata.EntityReply + (*EntityListReply)(nil), // 2: metadata.EntityListReply + (*CollectDatabaseReq)(nil), // 3: metadata.CollectDatabaseReq + (*CollectReply)(nil), // 4: metadata.CollectReply + (*AddLineageReq)(nil), // 5: metadata.AddLineageReq + (*LineageNode)(nil), // 6: metadata.LineageNode + (*LineageEdge)(nil), // 7: metadata.LineageEdge + (*LineageReply)(nil), // 8: metadata.LineageReply + (*LineageReq)(nil), // 9: metadata.LineageReq + (*EntityListReq)(nil), // 10: metadata.EntityListReq + (*MetaVersion)(nil), // 11: metadata.MetaVersion + (*VersionReply)(nil), // 12: metadata.VersionReply + (*VersionListReply)(nil), // 13: metadata.VersionListReply + (*CompareReq)(nil), // 14: metadata.CompareReq + (*DiffReply)(nil), // 15: metadata.DiffReply + (*IdReq)(nil), // 16: metadata.IdReq + (*OkReply)(nil), // 17: metadata.OkReply +} +var file_metadata_proto_depIdxs = []int32{ + 0, // 0: metadata.EntityReply.entity:type_name -> metadata.MetaEntity + 0, // 1: metadata.EntityListReply.list:type_name -> metadata.MetaEntity + 6, // 2: metadata.LineageReply.nodes:type_name -> metadata.LineageNode + 7, // 3: metadata.LineageReply.edges:type_name -> metadata.LineageEdge + 11, // 4: metadata.VersionReply.version:type_name -> metadata.MetaVersion + 11, // 5: metadata.VersionListReply.list:type_name -> metadata.MetaVersion + 3, // 6: metadata.Metadata.CollectDatabase:input_type -> metadata.CollectDatabaseReq + 16, // 7: metadata.Metadata.GetEntity:input_type -> metadata.IdReq + 10, // 8: metadata.Metadata.ListEntities:input_type -> metadata.EntityListReq + 5, // 9: metadata.Metadata.AddLineage:input_type -> metadata.AddLineageReq + 9, // 10: metadata.Metadata.GetLineage:input_type -> metadata.LineageReq + 16, // 11: metadata.Metadata.CreateVersion:input_type -> metadata.IdReq + 16, // 12: metadata.Metadata.ListVersions:input_type -> metadata.IdReq + 14, // 13: metadata.Metadata.CompareVersions:input_type -> metadata.CompareReq + 4, // 14: metadata.Metadata.CollectDatabase:output_type -> metadata.CollectReply + 1, // 15: metadata.Metadata.GetEntity:output_type -> metadata.EntityReply + 2, // 16: metadata.Metadata.ListEntities:output_type -> metadata.EntityListReply + 17, // 17: metadata.Metadata.AddLineage:output_type -> metadata.OkReply + 8, // 18: metadata.Metadata.GetLineage:output_type -> metadata.LineageReply + 12, // 19: metadata.Metadata.CreateVersion:output_type -> metadata.VersionReply + 13, // 20: metadata.Metadata.ListVersions:output_type -> metadata.VersionListReply + 15, // 21: metadata.Metadata.CompareVersions:output_type -> metadata.DiffReply + 14, // [14:22] is the sub-list for method output_type + 6, // [6:14] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_metadata_proto_init() } +func file_metadata_proto_init() { + if File_metadata_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_metadata_proto_rawDesc), len(file_metadata_proto_rawDesc)), + NumEnums: 0, + NumMessages: 18, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_metadata_proto_goTypes, + DependencyIndexes: file_metadata_proto_depIdxs, + MessageInfos: file_metadata_proto_msgTypes, + }.Build() + File_metadata_proto = out.File + file_metadata_proto_goTypes = nil + file_metadata_proto_depIdxs = nil +} diff --git a/metadata/rpc/metadata/metadata_grpc.pb.go b/metadata/rpc/metadata/metadata_grpc.pb.go new file mode 100644 index 0000000..08afea4 --- /dev/null +++ b/metadata/rpc/metadata/metadata_grpc.pb.go @@ -0,0 +1,403 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: metadata.proto + +package metadata + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Metadata_CollectDatabase_FullMethodName = "/metadata.Metadata/CollectDatabase" + Metadata_GetEntity_FullMethodName = "/metadata.Metadata/GetEntity" + Metadata_ListEntities_FullMethodName = "/metadata.Metadata/ListEntities" + Metadata_AddLineage_FullMethodName = "/metadata.Metadata/AddLineage" + Metadata_GetLineage_FullMethodName = "/metadata.Metadata/GetLineage" + Metadata_CreateVersion_FullMethodName = "/metadata.Metadata/CreateVersion" + Metadata_ListVersions_FullMethodName = "/metadata.Metadata/ListVersions" + Metadata_CompareVersions_FullMethodName = "/metadata.Metadata/CompareVersions" +) + +// MetadataClient is the client API for Metadata service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type MetadataClient interface { + //采集库表元数据(含 contains 关系) + CollectDatabase(ctx context.Context, in *CollectDatabaseReq, opts ...grpc.CallOption) (*CollectReply, error) + //实体详情 + GetEntity(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*EntityReply, error) + //实体列表 + ListEntities(ctx context.Context, in *EntityListReq, opts ...grpc.CallOption) (*EntityListReply, error) + //登记血缘 + AddLineage(ctx context.Context, in *AddLineageReq, opts ...grpc.CallOption) (*OkReply, error) + //血缘图(上下游 N 跳) + GetLineage(ctx context.Context, in *LineageReq, opts ...grpc.CallOption) (*LineageReply, error) + //定版快照 + CreateVersion(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*VersionReply, error) + //版本列表 + ListVersions(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*VersionListReply, error) + //版本对比 + CompareVersions(ctx context.Context, in *CompareReq, opts ...grpc.CallOption) (*DiffReply, error) +} + +type metadataClient struct { + cc grpc.ClientConnInterface +} + +func NewMetadataClient(cc grpc.ClientConnInterface) MetadataClient { + return &metadataClient{cc} +} + +func (c *metadataClient) CollectDatabase(ctx context.Context, in *CollectDatabaseReq, opts ...grpc.CallOption) (*CollectReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CollectReply) + err := c.cc.Invoke(ctx, Metadata_CollectDatabase_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metadataClient) GetEntity(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*EntityReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EntityReply) + err := c.cc.Invoke(ctx, Metadata_GetEntity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metadataClient) ListEntities(ctx context.Context, in *EntityListReq, opts ...grpc.CallOption) (*EntityListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EntityListReply) + err := c.cc.Invoke(ctx, Metadata_ListEntities_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metadataClient) AddLineage(ctx context.Context, in *AddLineageReq, opts ...grpc.CallOption) (*OkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(OkReply) + err := c.cc.Invoke(ctx, Metadata_AddLineage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metadataClient) GetLineage(ctx context.Context, in *LineageReq, opts ...grpc.CallOption) (*LineageReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LineageReply) + err := c.cc.Invoke(ctx, Metadata_GetLineage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metadataClient) CreateVersion(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*VersionReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VersionReply) + err := c.cc.Invoke(ctx, Metadata_CreateVersion_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metadataClient) ListVersions(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*VersionListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VersionListReply) + err := c.cc.Invoke(ctx, Metadata_ListVersions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metadataClient) CompareVersions(ctx context.Context, in *CompareReq, opts ...grpc.CallOption) (*DiffReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DiffReply) + err := c.cc.Invoke(ctx, Metadata_CompareVersions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MetadataServer is the server API for Metadata service. +// All implementations must embed UnimplementedMetadataServer +// for forward compatibility. +type MetadataServer interface { + //采集库表元数据(含 contains 关系) + CollectDatabase(context.Context, *CollectDatabaseReq) (*CollectReply, error) + //实体详情 + GetEntity(context.Context, *IdReq) (*EntityReply, error) + //实体列表 + ListEntities(context.Context, *EntityListReq) (*EntityListReply, error) + //登记血缘 + AddLineage(context.Context, *AddLineageReq) (*OkReply, error) + //血缘图(上下游 N 跳) + GetLineage(context.Context, *LineageReq) (*LineageReply, error) + //定版快照 + CreateVersion(context.Context, *IdReq) (*VersionReply, error) + //版本列表 + ListVersions(context.Context, *IdReq) (*VersionListReply, error) + //版本对比 + CompareVersions(context.Context, *CompareReq) (*DiffReply, error) + mustEmbedUnimplementedMetadataServer() +} + +// UnimplementedMetadataServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedMetadataServer struct{} + +func (UnimplementedMetadataServer) CollectDatabase(context.Context, *CollectDatabaseReq) (*CollectReply, error) { + return nil, status.Error(codes.Unimplemented, "method CollectDatabase not implemented") +} +func (UnimplementedMetadataServer) GetEntity(context.Context, *IdReq) (*EntityReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetEntity not implemented") +} +func (UnimplementedMetadataServer) ListEntities(context.Context, *EntityListReq) (*EntityListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListEntities not implemented") +} +func (UnimplementedMetadataServer) AddLineage(context.Context, *AddLineageReq) (*OkReply, error) { + return nil, status.Error(codes.Unimplemented, "method AddLineage not implemented") +} +func (UnimplementedMetadataServer) GetLineage(context.Context, *LineageReq) (*LineageReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetLineage not implemented") +} +func (UnimplementedMetadataServer) CreateVersion(context.Context, *IdReq) (*VersionReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateVersion not implemented") +} +func (UnimplementedMetadataServer) ListVersions(context.Context, *IdReq) (*VersionListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListVersions not implemented") +} +func (UnimplementedMetadataServer) CompareVersions(context.Context, *CompareReq) (*DiffReply, error) { + return nil, status.Error(codes.Unimplemented, "method CompareVersions not implemented") +} +func (UnimplementedMetadataServer) mustEmbedUnimplementedMetadataServer() {} +func (UnimplementedMetadataServer) testEmbeddedByValue() {} + +// UnsafeMetadataServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to MetadataServer will +// result in compilation errors. +type UnsafeMetadataServer interface { + mustEmbedUnimplementedMetadataServer() +} + +func RegisterMetadataServer(s grpc.ServiceRegistrar, srv MetadataServer) { + // If the following call panics, it indicates UnimplementedMetadataServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Metadata_ServiceDesc, srv) +} + +func _Metadata_CollectDatabase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CollectDatabaseReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetadataServer).CollectDatabase(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Metadata_CollectDatabase_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetadataServer).CollectDatabase(ctx, req.(*CollectDatabaseReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Metadata_GetEntity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetadataServer).GetEntity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Metadata_GetEntity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetadataServer).GetEntity(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Metadata_ListEntities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EntityListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetadataServer).ListEntities(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Metadata_ListEntities_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetadataServer).ListEntities(ctx, req.(*EntityListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Metadata_AddLineage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddLineageReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetadataServer).AddLineage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Metadata_AddLineage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetadataServer).AddLineage(ctx, req.(*AddLineageReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Metadata_GetLineage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LineageReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetadataServer).GetLineage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Metadata_GetLineage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetadataServer).GetLineage(ctx, req.(*LineageReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Metadata_CreateVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetadataServer).CreateVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Metadata_CreateVersion_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetadataServer).CreateVersion(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Metadata_ListVersions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetadataServer).ListVersions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Metadata_ListVersions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetadataServer).ListVersions(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Metadata_CompareVersions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CompareReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetadataServer).CompareVersions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Metadata_CompareVersions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetadataServer).CompareVersions(ctx, req.(*CompareReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Metadata_ServiceDesc is the grpc.ServiceDesc for Metadata service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Metadata_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "metadata.Metadata", + HandlerType: (*MetadataServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CollectDatabase", + Handler: _Metadata_CollectDatabase_Handler, + }, + { + MethodName: "GetEntity", + Handler: _Metadata_GetEntity_Handler, + }, + { + MethodName: "ListEntities", + Handler: _Metadata_ListEntities_Handler, + }, + { + MethodName: "AddLineage", + Handler: _Metadata_AddLineage_Handler, + }, + { + MethodName: "GetLineage", + Handler: _Metadata_GetLineage_Handler, + }, + { + MethodName: "CreateVersion", + Handler: _Metadata_CreateVersion_Handler, + }, + { + MethodName: "ListVersions", + Handler: _Metadata_ListVersions_Handler, + }, + { + MethodName: "CompareVersions", + Handler: _Metadata_CompareVersions_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "metadata.proto", +} diff --git a/metadata/rpc/metadataClient/metadata.go b/metadata/rpc/metadataClient/metadata.go new file mode 100644 index 0000000..65c9653 --- /dev/null +++ b/metadata/rpc/metadataClient/metadata.go @@ -0,0 +1,112 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: metadata.proto + +package metadataClient + +import ( + "context" + + "datacenter/metadata/rpc/metadata" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + AddLineageReq = metadata.AddLineageReq + CollectDatabaseReq = metadata.CollectDatabaseReq + CollectReply = metadata.CollectReply + CompareReq = metadata.CompareReq + DiffReply = metadata.DiffReply + EntityListReply = metadata.EntityListReply + EntityListReq = metadata.EntityListReq + EntityReply = metadata.EntityReply + IdReq = metadata.IdReq + LineageEdge = metadata.LineageEdge + LineageNode = metadata.LineageNode + LineageReply = metadata.LineageReply + LineageReq = metadata.LineageReq + MetaEntity = metadata.MetaEntity + MetaVersion = metadata.MetaVersion + OkReply = metadata.OkReply + VersionListReply = metadata.VersionListReply + VersionReply = metadata.VersionReply + + Metadata interface { + // 采集库表元数据(含 contains 关系) + CollectDatabase(ctx context.Context, in *CollectDatabaseReq, opts ...grpc.CallOption) (*CollectReply, error) + // 实体详情 + GetEntity(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*EntityReply, error) + // 实体列表 + ListEntities(ctx context.Context, in *EntityListReq, opts ...grpc.CallOption) (*EntityListReply, error) + // 登记血缘 + AddLineage(ctx context.Context, in *AddLineageReq, opts ...grpc.CallOption) (*OkReply, error) + // 血缘图(上下游 N 跳) + GetLineage(ctx context.Context, in *LineageReq, opts ...grpc.CallOption) (*LineageReply, error) + // 定版快照 + CreateVersion(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*VersionReply, error) + // 版本列表 + ListVersions(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*VersionListReply, error) + // 版本对比 + CompareVersions(ctx context.Context, in *CompareReq, opts ...grpc.CallOption) (*DiffReply, error) + } + + defaultMetadata struct { + cli zrpc.Client + } +) + +func NewMetadata(cli zrpc.Client) Metadata { + return &defaultMetadata{ + cli: cli, + } +} + +// 采集库表元数据(含 contains 关系) +func (m *defaultMetadata) CollectDatabase(ctx context.Context, in *CollectDatabaseReq, opts ...grpc.CallOption) (*CollectReply, error) { + client := metadata.NewMetadataClient(m.cli.Conn()) + return client.CollectDatabase(ctx, in, opts...) +} + +// 实体详情 +func (m *defaultMetadata) GetEntity(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*EntityReply, error) { + client := metadata.NewMetadataClient(m.cli.Conn()) + return client.GetEntity(ctx, in, opts...) +} + +// 实体列表 +func (m *defaultMetadata) ListEntities(ctx context.Context, in *EntityListReq, opts ...grpc.CallOption) (*EntityListReply, error) { + client := metadata.NewMetadataClient(m.cli.Conn()) + return client.ListEntities(ctx, in, opts...) +} + +// 登记血缘 +func (m *defaultMetadata) AddLineage(ctx context.Context, in *AddLineageReq, opts ...grpc.CallOption) (*OkReply, error) { + client := metadata.NewMetadataClient(m.cli.Conn()) + return client.AddLineage(ctx, in, opts...) +} + +// 血缘图(上下游 N 跳) +func (m *defaultMetadata) GetLineage(ctx context.Context, in *LineageReq, opts ...grpc.CallOption) (*LineageReply, error) { + client := metadata.NewMetadataClient(m.cli.Conn()) + return client.GetLineage(ctx, in, opts...) +} + +// 定版快照 +func (m *defaultMetadata) CreateVersion(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*VersionReply, error) { + client := metadata.NewMetadataClient(m.cli.Conn()) + return client.CreateVersion(ctx, in, opts...) +} + +// 版本列表 +func (m *defaultMetadata) ListVersions(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*VersionListReply, error) { + client := metadata.NewMetadataClient(m.cli.Conn()) + return client.ListVersions(ctx, in, opts...) +} + +// 版本对比 +func (m *defaultMetadata) CompareVersions(ctx context.Context, in *CompareReq, opts ...grpc.CallOption) (*DiffReply, error) { + client := metadata.NewMetadataClient(m.cli.Conn()) + return client.CompareVersions(ctx, in, opts...) +} From 1adf12c8fcba1f1c91dba2c3a00a3dd73b722f79 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 12:19:36 +0800 Subject: [PATCH 03/12] =?UTF-8?q?Phase=201=EF=BC=9A=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=B8=AD=E5=8F=B0=E5=9B=9B=E5=A4=A7=E6=9C=8D=E5=8A=A1=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=EF=BC=88=E6=95=B0=E6=8D=AE=E6=BA=90/=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE/=E6=95=B0=E6=8D=AE=E6=9C=8D=E5=8A=A1API/?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E7=9B=AE=E5=BD=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - datasource-rpc(:8085):数据源登记/探活,密码 AES-GCM 加密存储,with_secret 供内部服务换取 DSN - metadata-rpc(:8086):information_schema 采集(24表276列实测),contains/lineage 关系,BFS 血缘,定版快照与版本对比 - datasvc-rpc(:8087):OneService 模式数据 API 执行器——HMAC 签名→IP白名单→限流→授权→参数白名单→:name 占位→Redis 缓存→field_mask 出口脱敏(实测脱敏+cache_hit 全通过) - catalog-rpc(:8088):资产分类/登记(快照冗余)/申请审批状态机(乐观锁防并发重复审批) - 网关:4 个 .api 契约 + 30 个路由 + 28 个编排 logic + ServiceContext 装配 - 修复 4 处 RawFieldNames 反引号失配(新 go-zero 对少列查询扫宽 struct 严格校验) - 新增 42 个单元测试全部通过;docs/PHASE1.md 契约文档 Closes #14 Closes #15 Closes #16 Closes #17 --- catalog.api | 81 +++ catalog/model/applymodel.go | 145 +++++ catalog/model/assetmodel.go | 159 +++++ catalog/model/categorymodel.go | 101 ++++ catalog/model/vars.go | 6 + catalog/rpc/internal/logic/applyAssetLogic.go | 41 +- .../rpc/internal/logic/createCategoryLogic.go | 37 +- catalog/rpc/internal/logic/error.go | 29 + .../rpc/internal/logic/listAppliesLogic.go | 19 +- catalog/rpc/internal/logic/listAssetsLogic.go | 17 +- .../rpc/internal/logic/listCategoriesLogic.go | 22 +- .../rpc/internal/logic/reviewApplyLogic.go | 47 +- catalog/rpc/internal/logic/reviewstate.go | 20 + .../rpc/internal/logic/reviewstate_test.go | 58 ++ catalog/rpc/internal/logic/tagAssetLogic.go | 58 +- catalog/rpc/internal/svc/serviceContext.go | 18 +- datacenter.api | 4 + datasource.api | 103 ++++ datasource/model/datasourcemodel.go | 178 ++++++ datasource/model/vars.go | 6 + datasource/rpc/internal/config/config.go | 2 +- datasource/rpc/internal/logic/convert.go | 38 ++ .../internal/logic/createDataSourceLogic.go | 65 ++- datasource/rpc/internal/logic/credential.go | 68 +++ .../rpc/internal/logic/credential_test.go | 133 +++++ .../internal/logic/deleteDataSourceLogic.go | 20 +- datasource/rpc/internal/logic/error.go | 33 ++ .../rpc/internal/logic/getDataSourceLogic.go | 29 +- .../internal/logic/listDataSourcesLogic.go | 32 +- .../rpc/internal/logic/testConnectionLogic.go | 51 +- .../internal/logic/updateDataSourceLogic.go | 77 ++- datasource/rpc/internal/svc/serviceContext.go | 16 +- datasvc.api | 147 +++++ datasvc/model/apiModel.go | 180 ++++++ datasvc/model/appModel.go | 91 +++ datasvc/model/grantModel.go | 99 ++++ datasvc/model/vars.go | 6 + datasvc/rpc/internal/logic/convert.go | 42 ++ datasvc/rpc/internal/logic/createApiLogic.go | 75 ++- datasvc/rpc/internal/logic/credential.go | 135 +++++ datasvc/rpc/internal/logic/credential_test.go | 134 +++++ datasvc/rpc/internal/logic/deleteApiLogic.go | 31 +- datasvc/rpc/internal/logic/error.go | 33 ++ datasvc/rpc/internal/logic/executeApiLogic.go | 172 +++++- datasvc/rpc/internal/logic/executor.go | 273 +++++++++ datasvc/rpc/internal/logic/executor_test.go | 256 +++++++++ datasvc/rpc/internal/logic/getApiLogic.go | 16 +- datasvc/rpc/internal/logic/grantApiLogic.go | 58 +- datasvc/rpc/internal/logic/listApisLogic.go | 15 +- datasvc/rpc/internal/logic/offlineApiLogic.go | 26 +- datasvc/rpc/internal/logic/publishApiLogic.go | 36 +- .../rpc/internal/logic/registerAppLogic.go | 52 +- datasvc/rpc/internal/logic/updateApiLogic.go | 52 +- datasvc/rpc/internal/svc/serviceContext.go | 82 ++- etc/datacenter-api.example.yaml | 21 +- internal/config/config.go | 6 + internal/handler/catalog/assetlisthandler.go | 32 ++ .../handler/catalog/categorylisthandler.go | 32 ++ .../handler/catalog/createcategoryhandler.go | 32 ++ internal/handler/catalog/tagassethandler.go | 32 ++ .../datasource/createdatasourcehandler.go | 32 ++ .../datasource/datasourceinfohandler.go | 32 ++ .../datasource/datasourcelisthandler.go | 32 ++ .../datasource/datasourcetesthandler.go | 32 ++ .../datasource/deletedatasourcehandler.go | 32 ++ .../datasource/updatedatasourcehandler.go | 32 ++ internal/handler/datasvc/apiinfohandler.go | 32 ++ internal/handler/datasvc/apilisthandler.go | 32 ++ internal/handler/datasvc/createapihandler.go | 32 ++ internal/handler/datasvc/deleteapihandler.go | 32 ++ internal/handler/datasvc/executeapihandler.go | 32 ++ internal/handler/datasvc/grantapihandler.go | 32 ++ internal/handler/datasvc/offlineapihandler.go | 32 ++ internal/handler/datasvc/publishapihandler.go | 32 ++ .../handler/datasvc/registerapphandler.go | 32 ++ internal/handler/datasvc/updateapihandler.go | 32 ++ .../handler/metadata/addlineagehandler.go | 32 ++ .../metadata/collectdatabasehandler.go | 32 ++ .../handler/metadata/createversionhandler.go | 32 ++ internal/handler/metadata/lineagehandler.go | 32 ++ .../handler/metadata/metaentityinfohandler.go | 32 ++ .../handler/metadata/metaentitylisthandler.go | 32 ++ .../handler/metadata/versioncomparehandler.go | 32 ++ .../handler/metadata/versionlisthandler.go | 32 ++ internal/handler/routes.go | 358 +++++++++--- internal/logic/catalog/assetlistlogic.go | 76 +++ internal/logic/catalog/categorylistlogic.go | 50 ++ internal/logic/catalog/convert.go | 17 + internal/logic/catalog/createcategorylogic.go | 47 ++ internal/logic/catalog/tagassetlogic.go | 74 +++ internal/logic/datasource/convert.go | 27 + .../logic/datasource/createdatasourcelogic.go | 50 ++ .../logic/datasource/datasourceinfologic.go | 43 ++ .../logic/datasource/datasourcelistlogic.go | 58 ++ .../logic/datasource/datasourcetestlogic.go | 46 ++ .../logic/datasource/deletedatasourcelogic.go | 38 ++ .../logic/datasource/updatedatasourcelogic.go | 48 ++ internal/logic/datasvc/apiinfologic.go | 46 ++ internal/logic/datasvc/apilistlogic.go | 65 +++ internal/logic/datasvc/convert.go | 24 + internal/logic/datasvc/createapilogic.go | 59 ++ internal/logic/datasvc/deleteapilogic.go | 40 ++ internal/logic/datasvc/executeapilogic.go | 54 ++ internal/logic/datasvc/grantapilogic.go | 44 ++ internal/logic/datasvc/offlineapilogic.go | 40 ++ internal/logic/datasvc/publishapilogic.go | 40 ++ internal/logic/datasvc/registerapplogic.go | 52 ++ internal/logic/datasvc/updateapilogic.go | 44 ++ internal/logic/metadata/addlineagelogic.go | 40 ++ .../logic/metadata/collectdatabaselogic.go | 68 +++ internal/logic/metadata/convert.go | 68 +++ internal/logic/metadata/createversionlogic.go | 45 ++ internal/logic/metadata/lineagelogic.go | 54 ++ .../logic/metadata/metaentityinfologic.go | 42 ++ .../logic/metadata/metaentitylistlogic.go | 60 ++ .../logic/metadata/versioncomparelogic.go | 44 ++ internal/logic/metadata/versionlistlogic.go | 48 ++ internal/svc/servicecontext.go | 26 +- internal/types/types.go | 543 ++++++++++++++---- metadata.api | 141 +++++ metadata/model/entitymodel.go | 300 ++++++++++ metadata/model/relationmodel.go | 147 +++++ metadata/model/vars.go | 65 +++ metadata/model/versionmodel.go | 106 ++++ .../rpc/internal/logic/addLineageLogic.go | 19 +- .../internal/logic/collectDatabaseLogic.go | 215 ++++++- .../internal/logic/compareVersionsLogic.go | 38 +- metadata/rpc/internal/logic/convert.go | 36 ++ .../rpc/internal/logic/createVersionLogic.go | 42 +- metadata/rpc/internal/logic/error.go | 17 + metadata/rpc/internal/logic/getEntityLogic.go | 16 +- .../rpc/internal/logic/getLineageLogic.go | 52 +- metadata/rpc/internal/logic/lineage.go | 78 +++ metadata/rpc/internal/logic/lineage_test.go | 183 ++++++ .../rpc/internal/logic/listEntitiesLogic.go | 17 +- .../rpc/internal/logic/listVersionsLogic.go | 12 +- metadata/rpc/internal/logic/snapshot.go | 72 +++ metadata/rpc/internal/logic/snapshot_test.go | 93 +++ metadata/rpc/internal/svc/serviceContext.go | 20 +- restart.sh | 8 + 140 files changed, 8493 insertions(+), 309 deletions(-) create mode 100644 catalog.api create mode 100644 catalog/model/applymodel.go create mode 100644 catalog/model/assetmodel.go create mode 100644 catalog/model/categorymodel.go create mode 100644 catalog/model/vars.go create mode 100644 catalog/rpc/internal/logic/error.go create mode 100644 catalog/rpc/internal/logic/reviewstate.go create mode 100644 catalog/rpc/internal/logic/reviewstate_test.go create mode 100644 datasource.api create mode 100644 datasource/model/datasourcemodel.go create mode 100644 datasource/model/vars.go create mode 100644 datasource/rpc/internal/logic/convert.go create mode 100644 datasource/rpc/internal/logic/credential.go create mode 100644 datasource/rpc/internal/logic/credential_test.go create mode 100644 datasource/rpc/internal/logic/error.go create mode 100644 datasvc.api create mode 100644 datasvc/model/apiModel.go create mode 100644 datasvc/model/appModel.go create mode 100644 datasvc/model/grantModel.go create mode 100644 datasvc/model/vars.go create mode 100644 datasvc/rpc/internal/logic/convert.go create mode 100644 datasvc/rpc/internal/logic/credential.go create mode 100644 datasvc/rpc/internal/logic/credential_test.go create mode 100644 datasvc/rpc/internal/logic/error.go create mode 100644 datasvc/rpc/internal/logic/executor.go create mode 100644 datasvc/rpc/internal/logic/executor_test.go create mode 100644 internal/handler/catalog/assetlisthandler.go create mode 100644 internal/handler/catalog/categorylisthandler.go create mode 100644 internal/handler/catalog/createcategoryhandler.go create mode 100644 internal/handler/catalog/tagassethandler.go create mode 100644 internal/handler/datasource/createdatasourcehandler.go create mode 100644 internal/handler/datasource/datasourceinfohandler.go create mode 100644 internal/handler/datasource/datasourcelisthandler.go create mode 100644 internal/handler/datasource/datasourcetesthandler.go create mode 100644 internal/handler/datasource/deletedatasourcehandler.go create mode 100644 internal/handler/datasource/updatedatasourcehandler.go create mode 100644 internal/handler/datasvc/apiinfohandler.go create mode 100644 internal/handler/datasvc/apilisthandler.go create mode 100644 internal/handler/datasvc/createapihandler.go create mode 100644 internal/handler/datasvc/deleteapihandler.go create mode 100644 internal/handler/datasvc/executeapihandler.go create mode 100644 internal/handler/datasvc/grantapihandler.go create mode 100644 internal/handler/datasvc/offlineapihandler.go create mode 100644 internal/handler/datasvc/publishapihandler.go create mode 100644 internal/handler/datasvc/registerapphandler.go create mode 100644 internal/handler/datasvc/updateapihandler.go create mode 100644 internal/handler/metadata/addlineagehandler.go create mode 100644 internal/handler/metadata/collectdatabasehandler.go create mode 100644 internal/handler/metadata/createversionhandler.go create mode 100644 internal/handler/metadata/lineagehandler.go create mode 100644 internal/handler/metadata/metaentityinfohandler.go create mode 100644 internal/handler/metadata/metaentitylisthandler.go create mode 100644 internal/handler/metadata/versioncomparehandler.go create mode 100644 internal/handler/metadata/versionlisthandler.go create mode 100644 internal/logic/catalog/assetlistlogic.go create mode 100644 internal/logic/catalog/categorylistlogic.go create mode 100644 internal/logic/catalog/convert.go create mode 100644 internal/logic/catalog/createcategorylogic.go create mode 100644 internal/logic/catalog/tagassetlogic.go create mode 100644 internal/logic/datasource/convert.go create mode 100644 internal/logic/datasource/createdatasourcelogic.go create mode 100644 internal/logic/datasource/datasourceinfologic.go create mode 100644 internal/logic/datasource/datasourcelistlogic.go create mode 100644 internal/logic/datasource/datasourcetestlogic.go create mode 100644 internal/logic/datasource/deletedatasourcelogic.go create mode 100644 internal/logic/datasource/updatedatasourcelogic.go create mode 100644 internal/logic/datasvc/apiinfologic.go create mode 100644 internal/logic/datasvc/apilistlogic.go create mode 100644 internal/logic/datasvc/convert.go create mode 100644 internal/logic/datasvc/createapilogic.go create mode 100644 internal/logic/datasvc/deleteapilogic.go create mode 100644 internal/logic/datasvc/executeapilogic.go create mode 100644 internal/logic/datasvc/grantapilogic.go create mode 100644 internal/logic/datasvc/offlineapilogic.go create mode 100644 internal/logic/datasvc/publishapilogic.go create mode 100644 internal/logic/datasvc/registerapplogic.go create mode 100644 internal/logic/datasvc/updateapilogic.go create mode 100644 internal/logic/metadata/addlineagelogic.go create mode 100644 internal/logic/metadata/collectdatabaselogic.go create mode 100644 internal/logic/metadata/convert.go create mode 100644 internal/logic/metadata/createversionlogic.go create mode 100644 internal/logic/metadata/lineagelogic.go create mode 100644 internal/logic/metadata/metaentityinfologic.go create mode 100644 internal/logic/metadata/metaentitylistlogic.go create mode 100644 internal/logic/metadata/versioncomparelogic.go create mode 100644 internal/logic/metadata/versionlistlogic.go create mode 100644 metadata.api create mode 100644 metadata/model/entitymodel.go create mode 100644 metadata/model/relationmodel.go create mode 100644 metadata/model/vars.go create mode 100644 metadata/model/versionmodel.go create mode 100644 metadata/rpc/internal/logic/convert.go create mode 100644 metadata/rpc/internal/logic/error.go create mode 100644 metadata/rpc/internal/logic/lineage.go create mode 100644 metadata/rpc/internal/logic/lineage_test.go create mode 100644 metadata/rpc/internal/logic/snapshot.go create mode 100644 metadata/rpc/internal/logic/snapshot_test.go diff --git a/catalog.api b/catalog.api new file mode 100644 index 0000000..73caa5a --- /dev/null +++ b/catalog.api @@ -0,0 +1,81 @@ +syntax = "v1" + +//数据资产目录(Phase 1) +type AssetCategory { + Id int64 `json:"id,optional"` + ParentId int64 `json:"parent_id,optional"` + Name string `json:"name,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateCategoryReq { + ParentId int64 `json:"parent_id,optional"` + Name string `json:"name"` + Beid int64 `json:"beid,optional"` +} + +type CategoryListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` +} +type CategoryListResp { List []AssetCategory `json:"list"` } + +type TagAssetReq { + EntityId int64 `json:"entity_id"` + CategoryId int64 `json:"category_id,optional"` + Name string `json:"name"` + EntityType string `json:"entity_type,optional"` + Comment string `json:"comment,optional"` + Classification string `json:"classification,optional"` + AssetTagsJson string `json:"asset_tags_json,optional"` + Visibility string `json:"visibility,optional"` + Beid int64 `json:"beid,optional"` +} + +type AssetListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + CategoryId int64 `json:"category_id,optional" form:"category_id,optional"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type Asset { + Id int64 `json:"id,optional"` + EntityId int64 `json:"entity_id,optional"` + CategoryId int64 `json:"category_id,optional"` + Name string `json:"name,optional"` + EntityType string `json:"entity_type,optional"` + Comment string `json:"comment,optional"` + Classification string `json:"classification,optional"` + AssetTagsJson string `json:"asset_tags_json,optional"` + Visibility string `json:"visibility,optional"` +} +type AssetListResp { + List []Asset `json:"list"` + Total int64 `json:"total"` +} + +@server( + group: catalog +) +service datacenter-api { + @doc( + summary: "创建资产分类" + ) + @handler createCategory + post /catalog/category/create (CreateCategoryReq) returns (AssetCategory) + @doc( + summary: "分类列表" + ) + @handler categoryList + get /catalog/category/list (CategoryListReq) returns (CategoryListResp) + @doc( + summary: "登记资产" + ) + @handler tagAsset + post /catalog/asset/tag (TagAssetReq) returns (Asset) + @doc( + summary: "资产列表" + ) + @handler assetList + get /catalog/asset/list (AssetListReq) returns (AssetListResp) +} diff --git a/catalog/model/applymodel.go b/catalog/model/applymodel.go new file mode 100644 index 0000000..f1372ee --- /dev/null +++ b/catalog/model/applymodel.go @@ -0,0 +1,145 @@ +package model + +import ( + "database/sql" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + applyFieldNames = builder.RawFieldNames(&AssetApply{}) + applyRows = strings.Join(applyFieldNames, ",") + applyRowsExpectAutoSet = strings.Join(stringx.Remove(applyFieldNames, "`id`", "`created_at`", "`updated_at`"), ",") + applyRowsWithPlaceHolder = strings.Join(stringx.Remove(applyFieldNames, "`id`", "`created_at`", "`updated_at`"), "=?,") + "=?" +) + +// 分页默认值与上限 +const ( + applyDefaultPageSize = 10 + applyMaxPageSize = 200 +) + +type ( + // AssetApply 资产申请使用表 dc_asset_apply + AssetApply struct { + Id int64 `db:"id"` // 申请id + AssetId int64 `db:"asset_id"` // 资产id + Reason string `db:"reason"` // 申请理由 + Applicant string `db:"applicant"` // 申请人 + Status string `db:"status"` // pending/approved/rejected + Reviewer string `db:"reviewer"` // 审批人 + Beid int64 `db:"beid"` // 平台id + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + // ApplyModel 资产申请表操作接口 + ApplyModel interface { + Insert(data AssetApply) (sql.Result, error) + FindOne(id, beid int64) (*AssetApply, error) + Update(data AssetApply) error + // UpdateStatus 审批落库:仅当当前状态仍为 pending 时生效(乐观并发控制), + // 返回受影响行数,0 表示已被他人审批 + UpdateStatus(id, beid int64, status, reviewer string) (int64, error) + // ListByBeid 申请列表:beid 必带,status 非空则过滤;返回列表+总数 + ListByBeid(beid int64, status string, page, size int64) ([]*AssetApply, int64, error) + } + + defaultApplyModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewApplyModel 创建资产申请 model +func NewApplyModel(conn sqlx.SqlConn) ApplyModel { + return &defaultApplyModel{ + conn: conn, + table: "`dc_asset_apply`", + } +} + +// Insert 新增申请 +func (m *defaultApplyModel) Insert(data AssetApply) (sql.Result, error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?)", m.table, applyRowsExpectAutoSet) + ret, err := m.conn.Exec(query, data.AssetId, data.Reason, data.Applicant, data.Status, data.Reviewer, data.Beid) + return ret, err +} + +// FindOne 按申请id查询(带平台隔离) +func (m *defaultApplyModel) FindOne(id, beid int64) (*AssetApply, error) { + query := fmt.Sprintf("select %s from %s where `id` = ? and `beid` = ? limit 1", applyRows, m.table) + var resp AssetApply + err := m.conn.QueryRowPartial(&resp, query, id, beid) + switch { + case err == nil: + return &resp, nil + case err == sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// Update 全量更新申请(带平台隔离) +func (m *defaultApplyModel) Update(data AssetApply) error { + query := fmt.Sprintf("update %s set %s where `id` = ? and `beid` = ?", m.table, applyRowsWithPlaceHolder) + _, err := m.conn.Exec(query, data.AssetId, data.Reason, data.Applicant, data.Status, data.Reviewer, data.Beid, + data.Id, data.Beid) + return err +} + +// UpdateStatus 审批落库:带 `status = 'pending'` 条件防止并发重复审批 +func (m *defaultApplyModel) UpdateStatus(id, beid int64, status, reviewer string) (int64, error) { + query := fmt.Sprintf("update %s set `status` = ?, `reviewer` = ? where `id` = ? and `beid` = ? and `status` = 'pending'", m.table) + ret, err := m.conn.Exec(query, status, reviewer, id, beid) + if err != nil { + return 0, err + } + return ret.RowsAffected() +} + +// ListByBeid 申请列表(分页 + 可选状态过滤),返回列表与总数 +func (m *defaultApplyModel) ListByBeid(beid int64, status string, page, size int64) ([]*AssetApply, int64, error) { + // 分页参数防御 + if page < 1 { + page = 1 + } + if size <= 0 { + size = applyDefaultPageSize + } + if size > applyMaxPageSize { + size = applyMaxPageSize + } + + // 动态拼接查询条件(beid 多租户必带) + conds := []string{"`beid` = ?"} + args := []interface{}{beid} + if status != "" { + conds = append(conds, "`status` = ?") + args = append(args, status) + } + where := strings.Join(conds, " and ") + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowPartial(&total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页数据 + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ? offset ?", applyRows, m.table, where) + listArgs := append(append([]interface{}{}, args...), size, (page-1)*size) + var resp []*AssetApply + if err := m.conn.QueryRowsPartial(&resp, listQuery, listArgs...); err != nil { + return nil, 0, err + } + return resp, total, nil +} diff --git a/catalog/model/assetmodel.go b/catalog/model/assetmodel.go new file mode 100644 index 0000000..37115aa --- /dev/null +++ b/catalog/model/assetmodel.go @@ -0,0 +1,159 @@ +package model + +import ( + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + assetFieldNames = builder.RawFieldNames(&Asset{}) + assetRows = strings.Join(assetFieldNames, ",") + assetRowsExpectAutoSet = strings.Join(stringx.Remove(assetFieldNames, "`id`", "`created_at`", "`updated_at`"), ",") + assetRowsWithPlaceHolder = strings.Join(stringx.Remove(assetFieldNames, "`id`", "`created_at`", "`updated_at`"), "=?,") + "=?" +) + +// 分页默认值与上限 +const ( + assetDefaultPageSize = 10 + assetMaxPageSize = 200 +) + +type ( + // Asset 资产表 dc_asset(元数据实体入目录,冗余快照避免跨服务联查) + Asset struct { + Id int64 `db:"id"` // 资产id + EntityId int64 `db:"entity_id"` // 元数据实体id + CategoryId int64 `db:"category_id"` + Name string `db:"name"` // 资产名(冗余快照) + EntityType string `db:"entity_type"` // 冗余快照 + Comment string `db:"comment"` // 冗余快照 + Classification string `db:"classification"` // 冗余快照 + AssetTagsJson string `db:"asset_tags_json"` // 资产标签(json) + Visibility string `db:"visibility"` // public/private + Beid int64 `db:"beid"` // 平台id + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + // AssetModel 资产表操作接口 + AssetModel interface { + // TagAsset 登记资产(upsert):uk_beid_entity 冲突时刷新分类/标签/可见性及冗余快照 + TagAsset(data Asset) (int64, error) + FindOne(id, beid int64) (*Asset, error) + Update(data Asset) error + Delete(id, beid int64) error + // FindList 资产列表:beid 必带,categoryId>0 过滤分类,keyword 模糊匹配 name/comment;返回列表+总数 + FindList(beid, categoryId int64, keyword string, page, size int64) ([]*Asset, int64, error) + } + + defaultAssetModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewAssetModel 创建资产 model +func NewAssetModel(conn sqlx.SqlConn) AssetModel { + return &defaultAssetModel{ + conn: conn, + table: "`dc_asset`", + } +} + +// TagAsset 登记资产(upsert)。 +// 唯一键 uk_beid_entity 冲突时更新分类/标签/可见性及其余冗余快照字段, +// 并通过 last_insert_id(id) 保证冲突更新时也能拿到既有资产id。 +func (m *defaultAssetModel) TagAsset(data Asset) (int64, error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?) "+ + "on duplicate key update `id` = last_insert_id(`id`), `category_id` = values(`category_id`), "+ + "`name` = values(`name`), `entity_type` = values(`entity_type`), `comment` = values(`comment`), "+ + "`classification` = values(`classification`), `asset_tags_json` = values(`asset_tags_json`), "+ + "`visibility` = values(`visibility`)", m.table, assetRowsExpectAutoSet) + ret, err := m.conn.Exec(query, data.EntityId, data.CategoryId, data.Name, data.EntityType, + data.Comment, data.Classification, data.AssetTagsJson, data.Visibility, data.Beid) + if err != nil { + return 0, err + } + return ret.LastInsertId() +} + +// FindOne 按资产id查询(带平台隔离) +func (m *defaultAssetModel) FindOne(id, beid int64) (*Asset, error) { + query := fmt.Sprintf("select %s from %s where `id` = ? and `beid` = ? limit 1", assetRows, m.table) + var resp Asset + err := m.conn.QueryRowPartial(&resp, query, id, beid) + switch { + case err == nil: + return &resp, nil + case err == sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// Update 更新资产(带平台隔离) +func (m *defaultAssetModel) Update(data Asset) error { + query := fmt.Sprintf("update %s set %s where `id` = ? and `beid` = ?", m.table, assetRowsWithPlaceHolder) + _, err := m.conn.Exec(query, data.EntityId, data.CategoryId, data.Name, data.EntityType, + data.Comment, data.Classification, data.AssetTagsJson, data.Visibility, data.Beid, + data.Id, data.Beid) + return err +} + +// Delete 删除资产(带平台隔离) +func (m *defaultAssetModel) Delete(id, beid int64) error { + query := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.Exec(query, id, beid) + return err +} + +// FindList 资产列表(分页 + 分类过滤 + keyword 模糊匹配 name/comment),返回列表与总数 +func (m *defaultAssetModel) FindList(beid, categoryId int64, keyword string, page, size int64) ([]*Asset, int64, error) { + // 分页参数防御 + if page < 1 { + page = 1 + } + if size <= 0 { + size = assetDefaultPageSize + } + if size > assetMaxPageSize { + size = assetMaxPageSize + } + + // 动态拼接查询条件(beid 多租户必带,参数化防注入) + conds := []string{"`beid` = ?"} + args := []interface{}{beid} + if categoryId > 0 { + conds = append(conds, "`category_id` = ?") + args = append(args, categoryId) + } + if keyword != "" { + conds = append(conds, "(`name` like ? or `comment` like ?)") + like := "%" + keyword + "%" + args = append(args, like, like) + } + where := strings.Join(conds, " and ") + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowPartial(&total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页数据 + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ? offset ?", assetRows, m.table, where) + listArgs := append(append([]interface{}{}, args...), size, (page-1)*size) + var resp []*Asset + if err := m.conn.QueryRowsPartial(&resp, listQuery, listArgs...); err != nil { + return nil, 0, err + } + return resp, total, nil +} diff --git a/catalog/model/categorymodel.go b/catalog/model/categorymodel.go new file mode 100644 index 0000000..e957642 --- /dev/null +++ b/catalog/model/categorymodel.go @@ -0,0 +1,101 @@ +package model + +import ( + "database/sql" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + categoryFieldNames = builder.RawFieldNames(&AssetCategory{}) + categoryRows = strings.Join(categoryFieldNames, ",") + categoryRowsExpectAutoSet = strings.Join(stringx.Remove(categoryFieldNames, "`id`", "`created_at`"), ",") + categoryRowsWithPlaceHolder = strings.Join(stringx.Remove(categoryFieldNames, "`id`", "`created_at`"), "=?,") + "=?" +) + +type ( + // AssetCategory 资产目录分类表 dc_asset_category + AssetCategory struct { + Id int64 `db:"id"` // 分类id + ParentId int64 `db:"parent_id"` + Name string `db:"name"` + Beid int64 `db:"beid"` // 平台id + CreatedAt time.Time `db:"created_at"` + } + + // CategoryModel 分类表操作接口 + CategoryModel interface { + Insert(data AssetCategory) (sql.Result, error) + FindOne(id, beid int64) (*AssetCategory, error) + Update(data AssetCategory) error + Delete(id, beid int64) error + // ListByBeid 按平台查询全部分类,parent_id 升序排列,便于前端组树 + ListByBeid(beid int64) ([]*AssetCategory, error) + } + + defaultCategoryModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewCategoryModel 创建分类 model +func NewCategoryModel(conn sqlx.SqlConn) CategoryModel { + return &defaultCategoryModel{ + conn: conn, + table: "`dc_asset_category`", + } +} + +// Insert 新增分类 +func (m *defaultCategoryModel) Insert(data AssetCategory) (sql.Result, error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?)", m.table, categoryRowsExpectAutoSet) + ret, err := m.conn.Exec(query, data.ParentId, data.Name, data.Beid) + return ret, err +} + +// FindOne 按分类id查询(带平台隔离) +func (m *defaultCategoryModel) FindOne(id, beid int64) (*AssetCategory, error) { + query := fmt.Sprintf("select %s from %s where `id` = ? and `beid` = ? limit 1", categoryRows, m.table) + var resp AssetCategory + err := m.conn.QueryRowPartial(&resp, query, id, beid) + switch { + case err == nil: + return &resp, nil + case err == sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// Update 更新分类(含 parent_id/name,带平台隔离) +func (m *defaultCategoryModel) Update(data AssetCategory) error { + query := fmt.Sprintf("update %s set %s where `id` = ? and `beid` = ?", m.table, categoryRowsWithPlaceHolder) + _, err := m.conn.Exec(query, data.ParentId, data.Name, data.Beid, data.Id, data.Beid) + return err +} + +// Delete 删除分类(带平台隔离) +func (m *defaultCategoryModel) Delete(id, beid int64) error { + query := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.Exec(query, id, beid) + return err +} + +// ListByBeid 查询平台下全部分类,parent_id 升序(同级再按 id 升序) +func (m *defaultCategoryModel) ListByBeid(beid int64) ([]*AssetCategory, error) { + query := fmt.Sprintf("select %s from %s where `beid` = ? order by `parent_id` asc, `id` asc", categoryRows, m.table) + var resp []*AssetCategory + err := m.conn.QueryRowsPartial(&resp, query, beid) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/catalog/model/vars.go b/catalog/model/vars.go new file mode 100644 index 0000000..69beeb8 --- /dev/null +++ b/catalog/model/vars.go @@ -0,0 +1,6 @@ +package model + +import "github.com/zeromicro/go-zero/core/stores/sqlx" + +// ErrNotFound 未找到记录 +var ErrNotFound = sqlx.ErrNotFound diff --git a/catalog/rpc/internal/logic/applyAssetLogic.go b/catalog/rpc/internal/logic/applyAssetLogic.go index e8c0fad..9194714 100644 --- a/catalog/rpc/internal/logic/applyAssetLogic.go +++ b/catalog/rpc/internal/logic/applyAssetLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/catalog/model" "datacenter/catalog/rpc/catalog" "datacenter/catalog/rpc/internal/svc" @@ -23,9 +24,41 @@ func NewApplyAssetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApplyA } } -// 申请使用 +// 申请使用资产(写入 pending 状态的申请单) func (l *ApplyAssetLogic) ApplyAsset(in *catalog.ApplyAssetReq) (*catalog.ApplyReply, error) { - // todo: add your logic here and delete this line - - return &catalog.ApplyReply{}, nil + // 参数校验 + if in.AssetId <= 0 || in.Beid <= 0 { + return nil, ErrInvalidParam + } + // 校验资产存在(带平台隔离) + if _, err := l.svcCtx.AssetModel.FindOne(in.AssetId, in.Beid); err != nil { + if err == model.ErrNotFound { + return nil, ErrAssetNotFound + } + return nil, err + } + ret, err := l.svcCtx.ApplyModel.Insert(model.AssetApply{ + AssetId: in.AssetId, + Reason: in.Reason, + Applicant: in.Applicant, + Status: ApplyStatusPending, + Beid: in.Beid, + }) + if err != nil { + return nil, err + } + id, err := ret.LastInsertId() + if err != nil { + return nil, err + } + return &catalog.ApplyReply{ + Apply: &catalog.AssetApply{ + Id: id, + AssetId: in.AssetId, + Reason: in.Reason, + Applicant: in.Applicant, + Status: ApplyStatusPending, + Beid: in.Beid, + }, + }, nil } diff --git a/catalog/rpc/internal/logic/createCategoryLogic.go b/catalog/rpc/internal/logic/createCategoryLogic.go index 8773974..c09027d 100644 --- a/catalog/rpc/internal/logic/createCategoryLogic.go +++ b/catalog/rpc/internal/logic/createCategoryLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/catalog/model" "datacenter/catalog/rpc/catalog" "datacenter/catalog/rpc/internal/svc" @@ -25,7 +26,37 @@ func NewCreateCategoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Cr // 创建目录分类 func (l *CreateCategoryLogic) CreateCategory(in *catalog.AssetCategory) (*catalog.CategoryReply, error) { - // todo: add your logic here and delete this line - - return &catalog.CategoryReply{}, nil + // 参数校验:名称必填 + if in.Name == "" || in.Beid <= 0 { + return nil, ErrInvalidParam + } + // 指定父分类时校验其存在(带平台隔离) + if in.ParentId > 0 { + if _, err := l.svcCtx.CategoryModel.FindOne(in.ParentId, in.Beid); err != nil { + if err == model.ErrNotFound { + return nil, ErrCategoryNotFound + } + return nil, err + } + } + ret, err := l.svcCtx.CategoryModel.Insert(model.AssetCategory{ + ParentId: in.ParentId, + Name: in.Name, + Beid: in.Beid, + }) + if err != nil { + return nil, err + } + id, err := ret.LastInsertId() + if err != nil { + return nil, err + } + return &catalog.CategoryReply{ + Category: &catalog.AssetCategory{ + Id: id, + ParentId: in.ParentId, + Name: in.Name, + Beid: in.Beid, + }, + }, nil } diff --git a/catalog/rpc/internal/logic/error.go b/catalog/rpc/internal/logic/error.go new file mode 100644 index 0000000..0154b1c --- /dev/null +++ b/catalog/rpc/internal/logic/error.go @@ -0,0 +1,29 @@ +package logic + +import "errors" + +// 申请状态取值 +const ( + ApplyStatusPending = "pending" // 待审批 + ApplyStatusApproved = "approved" // 已通过 + ApplyStatusRejected = "rejected" // 已驳回 +) + +// 资产可见性取值 +const ( + VisibilityPublic = "public" + VisibilityPrivate = "private" +) + +var ( + // ErrCategoryNotFound 分类不存在 + ErrCategoryNotFound = errors.New("分类不存在") + // ErrAssetNotFound 资产不存在 + ErrAssetNotFound = errors.New("资产不存在") + // ErrApplyNotFound 申请不存在 + ErrApplyNotFound = errors.New("申请不存在") + // ErrApplyAlreadyReviewed 申请已审批(仅 pending 状态可审批) + ErrApplyAlreadyReviewed = errors.New("该申请已审批,不能重复审批") + // ErrInvalidParam 请求参数不合法 + ErrInvalidParam = errors.New("请求参数不合法") +) diff --git a/catalog/rpc/internal/logic/listAppliesLogic.go b/catalog/rpc/internal/logic/listAppliesLogic.go index 4872a32..d0c6f2b 100644 --- a/catalog/rpc/internal/logic/listAppliesLogic.go +++ b/catalog/rpc/internal/logic/listAppliesLogic.go @@ -23,9 +23,20 @@ func NewListAppliesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListA } } -// 申请列表 +// 申请列表(分页 + 可选状态过滤) func (l *ListAppliesLogic) ListApplies(in *catalog.ApplyListReq) (*catalog.ApplyListReply, error) { - // todo: add your logic here and delete this line - - return &catalog.ApplyListReply{}, nil + if in.Beid <= 0 { + return nil, ErrInvalidParam + } + list, total, err := l.svcCtx.ApplyModel.ListByBeid(in.Beid, in.Status, in.Page, in.Size) + if err != nil { + return nil, err + } + // pb 契约中 ApplyListReply 无 total 字段,总数记录日志供排查 + l.Debugf("ListApplies beid=%d status=%s total=%d", in.Beid, in.Status, total) + reply := make([]*catalog.AssetApply, 0, len(list)) + for _, a := range list { + reply = append(reply, applyToPb(a)) + } + return &catalog.ApplyListReply{List: reply}, nil } diff --git a/catalog/rpc/internal/logic/listAssetsLogic.go b/catalog/rpc/internal/logic/listAssetsLogic.go index 40aaf86..5937a70 100644 --- a/catalog/rpc/internal/logic/listAssetsLogic.go +++ b/catalog/rpc/internal/logic/listAssetsLogic.go @@ -23,9 +23,18 @@ func NewListAssetsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListAs } } -// 资产列表 +// 资产列表(分页 + 分类过滤 + keyword 模糊匹配 name/comment) func (l *ListAssetsLogic) ListAssets(in *catalog.AssetListReq) (*catalog.AssetListReply, error) { - // todo: add your logic here and delete this line - - return &catalog.AssetListReply{}, nil + if in.Beid <= 0 { + return nil, ErrInvalidParam + } + list, total, err := l.svcCtx.AssetModel.FindList(in.Beid, in.CategoryId, in.Keyword, in.Page, in.Size) + if err != nil { + return nil, err + } + reply := make([]*catalog.Asset, 0, len(list)) + for _, a := range list { + reply = append(reply, assetToPb(a)) + } + return &catalog.AssetListReply{List: reply, Total: total}, nil } diff --git a/catalog/rpc/internal/logic/listCategoriesLogic.go b/catalog/rpc/internal/logic/listCategoriesLogic.go index c4b25a0..9d9aa6e 100644 --- a/catalog/rpc/internal/logic/listCategoriesLogic.go +++ b/catalog/rpc/internal/logic/listCategoriesLogic.go @@ -23,9 +23,23 @@ func NewListCategoriesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Li } } -// 分类树 +// 分类树(按 parent_id 升序平铺返回,前端按 parent_id 组树) func (l *ListCategoriesLogic) ListCategories(in *catalog.IdReq) (*catalog.CategoryListReply, error) { - // todo: add your logic here and delete this line - - return &catalog.CategoryListReply{}, nil + if in.Beid <= 0 { + return nil, ErrInvalidParam + } + list, err := l.svcCtx.CategoryModel.ListByBeid(in.Beid) + if err != nil { + return nil, err + } + reply := make([]*catalog.AssetCategory, 0, len(list)) + for _, c := range list { + reply = append(reply, &catalog.AssetCategory{ + Id: c.Id, + ParentId: c.ParentId, + Name: c.Name, + Beid: c.Beid, + }) + } + return &catalog.CategoryListReply{List: reply}, nil } diff --git a/catalog/rpc/internal/logic/reviewApplyLogic.go b/catalog/rpc/internal/logic/reviewApplyLogic.go index 37b59a5..28524c9 100644 --- a/catalog/rpc/internal/logic/reviewApplyLogic.go +++ b/catalog/rpc/internal/logic/reviewApplyLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/catalog/model" "datacenter/catalog/rpc/catalog" "datacenter/catalog/rpc/internal/svc" @@ -23,9 +24,49 @@ func NewReviewApplyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Revie } } -// 审批 +// 审批:仅 pending 状态可审批,approve→approved / reject→rejected,记录审批人 func (l *ReviewApplyLogic) ReviewApply(in *catalog.ReviewApplyReq) (*catalog.ApplyReply, error) { - // todo: add your logic here and delete this line + // 参数校验 + if in.Id <= 0 || in.Beid <= 0 || in.Reviewer == "" { + return nil, ErrInvalidParam + } + // 查询申请单(带平台隔离) + apply, err := l.svcCtx.ApplyModel.FindOne(in.Id, in.Beid) + if err != nil { + if err == model.ErrNotFound { + return nil, ErrApplyNotFound + } + return nil, err + } + // 状态机:仅 pending 可审批 + nextStatus, err := nextApplyStatus(apply.Status, in.Approve) + if err != nil { + return nil, err + } + // 落库(条件更新防并发重复审批) + affected, err := l.svcCtx.ApplyModel.UpdateStatus(in.Id, in.Beid, nextStatus, in.Reviewer) + if err != nil { + return nil, err + } + if affected == 0 { + // 并发下已被他人审批 + return nil, ErrApplyAlreadyReviewed + } + apply.Status = nextStatus + apply.Reviewer = in.Reviewer + return &catalog.ApplyReply{Apply: applyToPb(apply)}, nil +} - return &catalog.ApplyReply{}, nil +// applyToPb model 转 pb +func applyToPb(a *model.AssetApply) *catalog.AssetApply { + return &catalog.AssetApply{ + Id: a.Id, + AssetId: a.AssetId, + Reason: a.Reason, + Applicant: a.Applicant, + Status: a.Status, + Reviewer: a.Reviewer, + Beid: a.Beid, + CreatedAt: a.CreatedAt.Unix(), + } } diff --git a/catalog/rpc/internal/logic/reviewstate.go b/catalog/rpc/internal/logic/reviewstate.go new file mode 100644 index 0000000..623a16c --- /dev/null +++ b/catalog/rpc/internal/logic/reviewstate.go @@ -0,0 +1,20 @@ +package logic + +import "fmt" + +// nextApplyStatus 审批状态机(纯函数,便于单元测试)。 +// 规则:仅 pending 状态可审批;approve=true 通过(approved),否则驳回(rejected); +// 已审批(approved/rejected)再审批返回 ErrApplyAlreadyReviewed。 +func nextApplyStatus(current string, approve bool) (string, error) { + switch current { + case ApplyStatusPending: + if approve { + return ApplyStatusApproved, nil + } + return ApplyStatusRejected, nil + case ApplyStatusApproved, ApplyStatusRejected: + return "", ErrApplyAlreadyReviewed + default: + return "", fmt.Errorf("非法的申请状态: %s", current) + } +} diff --git a/catalog/rpc/internal/logic/reviewstate_test.go b/catalog/rpc/internal/logic/reviewstate_test.go new file mode 100644 index 0000000..53afd0e --- /dev/null +++ b/catalog/rpc/internal/logic/reviewstate_test.go @@ -0,0 +1,58 @@ +package logic + +import ( + "errors" + "testing" +) + +// TestNextApplyStatus 审批状态机纯函数测试: +// 仅 pending 可审批;approved/rejected 再审批报 ErrApplyAlreadyReviewed +func TestNextApplyStatus(t *testing.T) { + cases := []struct { + name string + current string // 当前申请状态 + approve bool // 是否通过 + wantStatus string // 期望的下一状态 + wantErr error // 期望的业务错误(哨兵) + wantErrMsg string // 期望的错误信息(非哨兵错误按信息比对) + }{ + {name: "待审批通过", current: ApplyStatusPending, approve: true, wantStatus: ApplyStatusApproved}, + {name: "待审批驳回", current: ApplyStatusPending, approve: false, wantStatus: ApplyStatusRejected}, + {name: "已通过再审批", current: ApplyStatusApproved, approve: true, wantErr: ErrApplyAlreadyReviewed}, + {name: "已驳回再审批", current: ApplyStatusRejected, approve: false, wantErr: ErrApplyAlreadyReviewed}, + {name: "已通过再审批_驳回操作", current: ApplyStatusApproved, approve: false, wantErr: ErrApplyAlreadyReviewed}, + {name: "已驳回再审批_通过操作", current: ApplyStatusRejected, approve: true, wantErr: ErrApplyAlreadyReviewed}, + {name: "非法状态", current: "unknown", approve: true, wantErrMsg: "非法的申请状态: unknown"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, err := nextApplyStatus(c.current, c.approve) + // 校验错误 + switch { + case c.wantErr == nil && c.wantErrMsg == "": + if err != nil { + t.Fatalf("期望成功,实际错误: %v", err) + } + case c.wantErr != nil: + if err == nil { + t.Fatalf("期望错误 %v,实际成功,状态: %s", c.wantErr, got) + } + if !errors.Is(err, c.wantErr) { + t.Fatalf("期望错误 %v,实际错误: %v", c.wantErr, err) + } + default: + if err == nil { + t.Fatalf("期望错误 %q,实际成功,状态: %s", c.wantErrMsg, got) + } + if err.Error() != c.wantErrMsg { + t.Fatalf("期望错误 %q,实际错误: %q", c.wantErrMsg, err.Error()) + } + } + // 校验状态 + if got != c.wantStatus { + t.Fatalf("期望状态 %q,实际状态 %q", c.wantStatus, got) + } + }) + } +} diff --git a/catalog/rpc/internal/logic/tagAssetLogic.go b/catalog/rpc/internal/logic/tagAssetLogic.go index 96e53ba..33eb230 100644 --- a/catalog/rpc/internal/logic/tagAssetLogic.go +++ b/catalog/rpc/internal/logic/tagAssetLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/catalog/model" "datacenter/catalog/rpc/catalog" "datacenter/catalog/rpc/internal/svc" @@ -23,9 +24,60 @@ func NewTagAssetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TagAsset } } -// 登记资产(元数据实体入目录) +// 登记资产(元数据实体入目录,uk_beid_entity 冲突则刷新快照) func (l *TagAssetLogic) TagAsset(in *catalog.TagAssetReq) (*catalog.AssetReply, error) { - // todo: add your logic here and delete this line + // 参数校验 + if in.EntityId <= 0 || in.Name == "" || in.Beid <= 0 { + return nil, ErrInvalidParam + } + // 指定分类时校验其存在(带平台隔离) + if in.CategoryId > 0 { + if _, err := l.svcCtx.CategoryModel.FindOne(in.CategoryId, in.Beid); err != nil { + if err == model.ErrNotFound { + return nil, ErrCategoryNotFound + } + return nil, err + } + } + // 可见性默认 private + visibility := in.Visibility + if visibility == "" { + visibility = VisibilityPrivate + } + id, err := l.svcCtx.AssetModel.TagAsset(model.Asset{ + EntityId: in.EntityId, + CategoryId: in.CategoryId, + Name: in.Name, + EntityType: in.EntityType, + Comment: in.Comment, + Classification: in.Classification, + AssetTagsJson: in.AssetTagsJson, + Visibility: visibility, + Beid: in.Beid, + }) + if err != nil { + return nil, err + } + // 回查落库后的完整快照(含创建/更新时间语义由冗余快照体现) + asset, err := l.svcCtx.AssetModel.FindOne(id, in.Beid) + if err != nil { + return nil, err + } + return &catalog.AssetReply{Asset: assetToPb(asset)}, nil +} - return &catalog.AssetReply{}, nil +// assetToPb model 转 pb +func assetToPb(a *model.Asset) *catalog.Asset { + return &catalog.Asset{ + Id: a.Id, + EntityId: a.EntityId, + CategoryId: a.CategoryId, + Name: a.Name, + EntityType: a.EntityType, + Comment: a.Comment, + Classification: a.Classification, + AssetTagsJson: a.AssetTagsJson, + Visibility: a.Visibility, + Beid: a.Beid, + } } diff --git a/catalog/rpc/internal/svc/serviceContext.go b/catalog/rpc/internal/svc/serviceContext.go index 5fab8a2..0f9de5f 100644 --- a/catalog/rpc/internal/svc/serviceContext.go +++ b/catalog/rpc/internal/svc/serviceContext.go @@ -1,13 +1,27 @@ package svc -import "datacenter/catalog/rpc/internal/config" +import ( + "datacenter/catalog/model" + "datacenter/catalog/rpc/internal/config" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) type ServiceContext struct { Config config.Config + + CategoryModel model.CategoryModel // 资产目录分类 + AssetModel model.AssetModel // 资产 + ApplyModel model.ApplyModel // 资产申请使用 } func NewServiceContext(c config.Config) *ServiceContext { + // 无缓存需求的表统一用 sqlx 直连 + conn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ - Config: c, + Config: c, + CategoryModel: model.NewCategoryModel(conn), + AssetModel: model.NewAssetModel(conn), + ApplyModel: model.NewApplyModel(conn), } } diff --git a/datacenter.api b/datacenter.api index e955502..f8ce5d3 100644 --- a/datacenter.api +++ b/datacenter.api @@ -8,6 +8,10 @@ info ( ) import "user.api" +import "datasource.api" +import "metadata.api" +import "datasvc.api" +import "catalog.api" import "votes.api" import "search.api" import "questions.api" diff --git a/datasource.api b/datasource.api new file mode 100644 index 0000000..d7c785a --- /dev/null +++ b/datasource.api @@ -0,0 +1,103 @@ +syntax = "v1" + +//数据源管理(Phase 1) +type DataSourceInfo { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + Type string `json:"type,optional"` + Host string `json:"host,optional"` + Port int32 `json:"port,optional"` + DbName string `json:"db_name,optional"` + Username string `json:"username,optional"` + Status int32 `json:"status,optional"` + Owner string `json:"owner,optional"` + CreatedAt int64 `json:"created_at,optional"` + UpdatedAt int64 `json:"updated_at,optional"` +} + +type CreateDataSourceReq { + Name string `json:"name"` + Type string `json:"type"` + Host string `json:"host,optional"` + Port int32 `json:"port,optional"` + DbName string `json:"db_name,optional"` + Username string `json:"username,optional"` + Password string `json:"password,optional"` + ParamsJson string `json:"params_json,optional"` + Owner string `json:"owner,optional"` + Beid int64 `json:"beid,optional"` +} + +type UpdateDataSourceReq { + Id int64 `json:"id"` + Beid int64 `json:"beid,optional"` + Name string `json:"name,optional"` + Type string `json:"type,optional"` + Host string `json:"host,optional"` + Port int32 `json:"port,optional"` + DbName string `json:"db_name,optional"` + Username string `json:"username,optional"` + Password string `json:"password,optional"` + ParamsJson string `json:"params_json,optional"` + Status int32 `json:"status,optional"` + Owner string `json:"owner,optional"` +} + +type DataSourceIdReq { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type DataSourceListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Type string `json:"type,optional" form:"type,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type DataSourceListResp { + List []DataSourceInfo `json:"list"` + Total int64 `json:"total"` +} + +type TestConnResp { + Ok bool `json:"ok"` + LatencyMs int64 `json:"latency_ms"` + ErrMsg string `json:"err_msg,optional"` +} + +@server( + group: datasource +) +service datacenter-api { + @doc( + summary: "创建数据源" + ) + @handler createDataSource + post /datasource/create (CreateDataSourceReq) returns (DataSourceInfo) + @doc( + summary: "更新数据源" + ) + @handler updateDataSource + post /datasource/update (UpdateDataSourceReq) + @doc( + summary: "删除数据源" + ) + @handler deleteDataSource + post /datasource/delete (DataSourceIdReq) + @doc( + summary: "数据源详情" + ) + @handler dataSourceInfo + get /datasource/info (DataSourceIdReq) returns (DataSourceInfo) + @doc( + summary: "数据源列表" + ) + @handler dataSourceList + get /datasource/list (DataSourceListReq) returns (DataSourceListResp) + @doc( + summary: "测试数据源连接" + ) + @handler dataSourceTest + get /datasource/test (DataSourceIdReq) returns (TestConnResp) +} diff --git a/datasource/model/datasourcemodel.go b/datasource/model/datasourcemodel.go new file mode 100644 index 0000000..472eb67 --- /dev/null +++ b/datasource/model/datasourcemodel.go @@ -0,0 +1,178 @@ +package model + +import ( + "database/sql" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + datasourceFieldNames = builder.RawFieldNames(&DataSource{}) + datasourceRows = strings.Join(datasourceFieldNames, ",") + datasourceRowsExpectAutoSet = strings.Join(stringx.Remove(datasourceFieldNames, "`id`", "`created_at`", "`updated_at`", "`deleted_at`"), ",") + datasourceRowsWithPlaceHolder = strings.Join(stringx.Remove(datasourceFieldNames, "`id`", "`created_at`", "`updated_at`", "`deleted_at`"), "=?,") + "=?" +) + +type ( + // DataSourceModel 数据源登记表(dc_datasource)操作接口 + DataSourceModel interface { + // Insert 新增数据源(调用前需自行做 name+beid 查重) + Insert(data DataSource) (sql.Result, error) + // Update 更新基础信息(不含密码与状态, 密码走 UpdatePassword, 状态走 UpdateStatus) + Update(data DataSource) error + // UpdatePassword 仅更新密码密文 + UpdatePassword(id, beid int64, enc []byte) error + // UpdateStatus 更新启停状态(1.启用 0.停用) + UpdateStatus(id, beid int64, status int32) error + // Delete 软删(写 deleted_at, 物理数据保留) + Delete(id, beid int64) error + // FindOne 按主键+平台查单个未删除数据源 + FindOne(id, beid int64) (*DataSource, error) + // FindOneByName 按名称+平台查重(未删除范围内) + FindOneByName(name string, beid int64) (*DataSource, error) + // FindList 分页列表(beid 必带, type 可选), 返回列表与总数 + FindList(beid int64, typ string, page, size int64) ([]DataSource, int64, error) + } + + defaultDataSourceModel struct { + conn sqlx.SqlConn + table string + } + + // DataSource dc_datasource 表模型 + DataSource struct { + Id int64 `db:"id"` //数据源id + Name string `db:"name"` //名称 + Type string `db:"type"` //类型: mysql/redis/es/http/kafka + Host string `db:"host"` //主机 + Port int32 `db:"port"` //端口 + DbName string `db:"db_name"` //库名 + Username string `db:"username"` //用户名 + PasswordEnc []byte `db:"password_enc"` //AES-GCM 加密后的密码(nonce+密文) + ParamsJson sql.NullString `db:"params_json"` //扩展参数(json, 可空) + Status int32 `db:"status"` //1.启用 0.停用 + Owner string `db:"owner"` //负责人 + Beid int64 `db:"beid"` //平台id + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + DeletedAt sql.NullTime `db:"deleted_at"` + } +) + +// NewDataSourceModel 构造数据源 model +func NewDataSourceModel(conn sqlx.SqlConn) DataSourceModel { + return &defaultDataSourceModel{ + conn: conn, + table: "dc_datasource", + } +} + +// Insert 新增, password_enc 列 NOT NULL, 空密码落空字节而非 NULL +func (m *defaultDataSourceModel) Insert(data DataSource) (sql.Result, error) { + if data.PasswordEnc == nil { + data.PasswordEnc = []byte{} + } + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, datasourceRowsExpectAutoSet) + ret, err := m.conn.Exec(query, data.Name, data.Type, data.Host, data.Port, data.DbName, + data.Username, data.PasswordEnc, data.ParamsJson, data.Status, data.Owner, data.Beid) + return ret, err +} + +// Update 更新基础信息(软删行不可更新) +func (m *defaultDataSourceModel) Update(data DataSource) error { + query := fmt.Sprintf("update %s set name = ?, type = ?, host = ?, port = ?, db_name = ?, username = ?, params_json = ?, owner = ? where id = ? and beid = ? and deleted_at is null", m.table) + _, err := m.conn.Exec(query, data.Name, data.Type, data.Host, data.Port, data.DbName, + data.Username, data.ParamsJson, data.Owner, data.Id, data.Beid) + return err +} + +// UpdatePassword 仅更新密码密文 +func (m *defaultDataSourceModel) UpdatePassword(id, beid int64, enc []byte) error { + if enc == nil { + enc = []byte{} + } + query := fmt.Sprintf("update %s set password_enc = ? where id = ? and beid = ? and deleted_at is null", m.table) + _, err := m.conn.Exec(query, enc, id, beid) + return err +} + +// UpdateStatus 更新启停状态 +func (m *defaultDataSourceModel) UpdateStatus(id, beid int64, status int32) error { + query := fmt.Sprintf("update %s set status = ? where id = ? and beid = ? and deleted_at is null", m.table) + _, err := m.conn.Exec(query, status, id, beid) + return err +} + +// Delete 软删: 仅写 deleted_at +func (m *defaultDataSourceModel) Delete(id, beid int64) error { + query := fmt.Sprintf("update %s set deleted_at = now(3) where id = ? and beid = ? and deleted_at is null", m.table) + _, err := m.conn.Exec(query, id, beid) + return err +} + +// FindOne 按主键+平台查询(过滤软删) +func (m *defaultDataSourceModel) FindOne(id, beid int64) (*DataSource, error) { + query := fmt.Sprintf("select %s from %s where id = ? and beid = ? and deleted_at is null limit 1", datasourceRows, m.table) + var resp DataSource + err := m.conn.QueryRow(&resp, query, id, beid) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindOneByName 按名称+平台查重(过滤软删, 已删除的数据源名称可复用) +func (m *defaultDataSourceModel) FindOneByName(name string, beid int64) (*DataSource, error) { + query := fmt.Sprintf("select %s from %s where name = ? and beid = ? and deleted_at is null limit 1", datasourceRows, m.table) + var resp DataSource + err := m.conn.QueryRow(&resp, query, name, beid) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindList 分页列表: beid 必带, typ 为空不过滤, 返回列表与 total +func (m *defaultDataSourceModel) FindList(beid int64, typ string, page, size int64) ([]DataSource, int64, error) { + // 分页参数兜底 + if page < 1 { + page = 1 + } + if size < 1 { + size = 10 + } + where := "where beid = ? and deleted_at is null" + args := []interface{}{beid} + if typ != "" { + where += " and type = ?" + args = append(args, typ) + } + // 先查总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) as total from %s %s", m.table, where) + if err := m.conn.QueryRowPartial(&total, countQuery, args...); err != nil { + return nil, 0, err + } + // 再查当前页 + query := fmt.Sprintf("select %s from %s %s order by id desc limit ?, ?", datasourceRows, m.table, where) + args = append(args, (page-1)*size, size) + var list []DataSource + if err := m.conn.QueryRows(&list, query, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} diff --git a/datasource/model/vars.go b/datasource/model/vars.go new file mode 100644 index 0000000..58154f0 --- /dev/null +++ b/datasource/model/vars.go @@ -0,0 +1,6 @@ +package model + +import "github.com/zeromicro/go-zero/core/stores/sqlx" + +// ErrNotFound 记录不存在 +var ErrNotFound = sqlx.ErrNotFound diff --git a/datasource/rpc/internal/config/config.go b/datasource/rpc/internal/config/config.go index d7111de..702bfe9 100755 --- a/datasource/rpc/internal/config/config.go +++ b/datasource/rpc/internal/config/config.go @@ -10,6 +10,6 @@ type Config struct { Mysql struct { DataSource string } - EncryptKey string `json:",optional"` //AES-GCM 密钥(32字节) + EncryptKey string `json:",optional"` //AES-GCM 密钥(32字节) CacheRedis cache.ClusterConf `json:",optional"` } diff --git a/datasource/rpc/internal/logic/convert.go b/datasource/rpc/internal/logic/convert.go new file mode 100644 index 0000000..ca5203d --- /dev/null +++ b/datasource/rpc/internal/logic/convert.go @@ -0,0 +1,38 @@ +package logic + +import ( + "encoding/base64" + "time" + + "datacenter/datasource/model" + "datacenter/datasource/rpc/datasource" +) + +// toPbDataSource 表模型转 pb 结构 +// password_enc 为密文, 以 base64 传输; 明文密码任何情况下不回传 +func toPbDataSource(m model.DataSource) *datasource.DataSourceInfo { + return &datasource.DataSourceInfo{ + Id: m.Id, + Name: m.Name, + Type: m.Type, + Host: m.Host, + Port: m.Port, + DbName: m.DbName, + Username: m.Username, + PasswordEnc: base64.StdEncoding.EncodeToString(m.PasswordEnc), + ParamsJson: m.ParamsJson.String, + Status: m.Status, + Owner: m.Owner, + Beid: m.Beid, + CreatedAt: unixSeconds(m.CreatedAt), + UpdatedAt: unixSeconds(m.UpdatedAt), + } +} + +// unixSeconds 时间转秒级时间戳, 零值返回 0 +func unixSeconds(t time.Time) int64 { + if t.IsZero() { + return 0 + } + return t.Unix() +} diff --git a/datasource/rpc/internal/logic/createDataSourceLogic.go b/datasource/rpc/internal/logic/createDataSourceLogic.go index 4e518e2..63f7a08 100644 --- a/datasource/rpc/internal/logic/createDataSourceLogic.go +++ b/datasource/rpc/internal/logic/createDataSourceLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "database/sql" + "datacenter/datasource/model" "datacenter/datasource/rpc/datasource" "datacenter/datasource/rpc/internal/svc" @@ -23,9 +25,64 @@ func NewCreateDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } } -// 创建数据源 +// 创建数据源: 校验必填 -> name+beid 查重 -> 密码 AES-GCM 加密 -> 落库 -> 返回(不含敏感信息) func (l *CreateDataSourceLogic) CreateDataSource(in *datasource.CreateDataSourceReq) (*datasource.DataSourceReply, error) { - // todo: add your logic here and delete this line - - return &datasource.DataSourceReply{}, nil + // 1.校验必填字段与类型白名单 + if in.Name == "" { + return nil, ErrNameEmpty + } + if !validDataSourceType(in.Type) { + return nil, ErrTypeInvalid + } + // 2.name+beid 唯一性查重(未删除范围内) + if _, err := l.svcCtx.DataSourceModel.FindOneByName(in.Name, in.Beid); err == nil { + return nil, ErrNameDuplicate + } else if err != model.ErrNotFound { + l.Errorf("查重失败, name: %s, beid: %d, err: %v", in.Name, in.Beid, err) + return nil, ErrQueryFail + } + // 3.明文密码 AES-GCM 加密(空密码加密为 nonce+空密文, 统一走同一逻辑) + enc, err := EncryptPassword(l.svcCtx.Config.EncryptKey, in.Password) + if err != nil { + return nil, err + } + // 4.落库, 新数据源默认启用 + ret, err := l.svcCtx.DataSourceModel.Insert(model.DataSource{ + Name: in.Name, + Type: in.Type, + Host: in.Host, + Port: in.Port, + DbName: in.DbName, + Username: in.Username, + PasswordEnc: enc, + ParamsJson: sql.NullString{String: in.ParamsJson, Valid: in.ParamsJson != ""}, + Status: 1, + Owner: in.Owner, + Beid: in.Beid, + }) + if err != nil { + l.Errorf("创建数据源失败, name: %s, beid: %d, err: %v", in.Name, in.Beid, err) + return nil, ErrCreateFail + } + id, err := ret.LastInsertId() + if err != nil { + l.Errorf("获取自增id失败, name: %s, beid: %d, err: %v", in.Name, in.Beid, err) + return nil, ErrCreateFail + } + // 5.返回结果不回传任何敏感信息(密文也不回传) + return &datasource.DataSourceReply{ + Datasource: &datasource.DataSourceInfo{ + Id: id, + Name: in.Name, + Type: in.Type, + Host: in.Host, + Port: in.Port, + DbName: in.DbName, + Username: in.Username, + ParamsJson: in.ParamsJson, + Status: 1, + Owner: in.Owner, + Beid: in.Beid, + }, + }, nil } diff --git a/datasource/rpc/internal/logic/credential.go b/datasource/rpc/internal/logic/credential.go new file mode 100644 index 0000000..60fb5d5 --- /dev/null +++ b/datasource/rpc/internal/logic/credential.go @@ -0,0 +1,68 @@ +package logic + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "fmt" +) + +const ( + // aesGcmNonceLen AES-GCM 标准 nonce 长度(12 字节) + aesGcmNonceLen = 12 + // aesGcmKeyLen 配置密钥要求长度(32 字节, 即 AES-256) + aesGcmKeyLen = 32 +) + +// newGCM 校验密钥长度并构造 AES-GCM 实例 +func newGCM(key string) (cipher.AEAD, error) { + if len(key) != aesGcmKeyLen { + return nil, ErrEncryptKeyInvalid + } + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return nil, fmt.Errorf("构造 AES cipher 失败: %w", err) + } + return cipher.NewGCM(block) +} + +// EncryptPassword AES-GCM 加密: 随机 nonce(12 字节) 前置拼接密文后整体落库(varbinary 列) +// 存储布局: nonce(12B) + ciphertext(含 GCM tag) +func EncryptPassword(key, plaintext string) ([]byte, error) { + gcm, err := newGCM(key) + if err != nil { + return nil, err + } + nonce := make([]byte, aesGcmNonceLen) + if _, err = rand.Read(nonce); err != nil { + return nil, fmt.Errorf("%w: %v", ErrEncryptFail, err) + } + // Seal 输出 = nonce 拼接密文, 解密时按前 12 字节切分 + return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil +} + +// DecryptPassword 解密 nonce 前置拼接的 AES-GCM 密文; 空密文(未设置密码)直接返回空串 +func DecryptPassword(key string, ciphertext []byte) (string, error) { + if len(ciphertext) == 0 { + return "", nil + } + if len(ciphertext) <= aesGcmNonceLen { + return "", errors.New("密文长度不合法") + } + gcm, err := newGCM(key) + if err != nil { + return "", err + } + plaintext, err := gcm.Open(nil, ciphertext[:aesGcmNonceLen], ciphertext[aesGcmNonceLen:], nil) + if err != nil { + return "", ErrDecryptFail + } + return string(plaintext), nil +} + +// BuildMysqlDsn 拼接 mysql 连接串(用户名/密码不转义, 与 go-sql-driver DSN 格式一致) +func BuildMysqlDsn(host string, port int32, dbName, username, password string) string { + return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Asia%%2FShanghai", + username, password, host, port, dbName) +} diff --git a/datasource/rpc/internal/logic/credential_test.go b/datasource/rpc/internal/logic/credential_test.go new file mode 100644 index 0000000..8ffd406 --- /dev/null +++ b/datasource/rpc/internal/logic/credential_test.go @@ -0,0 +1,133 @@ +package logic + +import ( + "bytes" + "strings" + "testing" +) + +// testKey 32 字节测试密钥(与 example yaml 一致) +const testKey = "0123456789abcdef0123456789abcdef" + +// wrongKey 另一把合法长度但内容不同的密钥 +const wrongKey = "ffffffffffffffffffffffffffffffff" + +// TestEncryptDecryptRoundTrip 加密→解密往返一致 +func TestEncryptDecryptRoundTrip(t *testing.T) { + plainList := []string{ + "", // 空密码 + "admin123", // 常规密码 + "p@ss w0rd!#$%^&*()", // 特殊字符 + "中文密码测试123", // 中文 + strings.Repeat("x", 200), // 超长密码 + } + for _, plain := range plainList { + enc, err := EncryptPassword(testKey, plain) + if err != nil { + t.Fatalf("加密 %q 失败: %v", plain, err) + } + // 密文长度必须大于 nonce 长度(携带 GCM tag) + if len(enc) <= aesGcmNonceLen { + t.Fatalf("密文长度 %d 不合法", len(enc)) + } + // 非空明文不允许在密文中出现 + if plain != "" && bytes.Contains(enc, []byte(plain)) { + t.Fatalf("密文中出现了明文") + } + got, err := DecryptPassword(testKey, enc) + if err != nil { + t.Fatalf("解密 %q 失败: %v", plain, err) + } + if got != plain { + t.Fatalf("往返不一致, 期望 %q, 实际 %q", plain, got) + } + } +} + +// TestEncryptRandomNonce 相同明文两次加密结果不同(随机 nonce 前置) +func TestEncryptRandomNonce(t *testing.T) { + a, err := EncryptPassword(testKey, "admin123") + if err != nil { + t.Fatalf("加密失败: %v", err) + } + b, err := EncryptPassword(testKey, "admin123") + if err != nil { + t.Fatalf("加密失败: %v", err) + } + if bytes.Equal(a, b) { + t.Fatal("两次加密结果相同, nonce 未随机化") + } + // nonce 前置: 前 12 字节应不同 + if bytes.Equal(a[:aesGcmNonceLen], b[:aesGcmNonceLen]) { + t.Fatal("两次加密的 nonce 相同") + } +} + +// TestDecryptWithWrongKey 错误密钥解密必须失败 +func TestDecryptWithWrongKey(t *testing.T) { + enc, err := EncryptPassword(testKey, "admin123") + if err != nil { + t.Fatalf("加密失败: %v", err) + } + if _, err = DecryptPassword(wrongKey, enc); err == nil { + t.Fatal("错误密钥解密应当失败") + } +} + +// TestDecryptWithTamperedCiphertext 篡改密文(nonce 或密文体)解密必须失败 +func TestDecryptWithTamperedCiphertext(t *testing.T) { + enc, err := EncryptPassword(testKey, "admin123") + if err != nil { + t.Fatalf("加密失败: %v", err) + } + // 篡改密文体最后一个字节(GCM tag 校验应失败) + tampered := bytes.Clone(enc) + tampered[len(tampered)-1] ^= 0xff + if _, err = DecryptPassword(testKey, tampered); err == nil { + t.Fatal("篡改密文后解密应当失败") + } + // 篡改 nonce 部分 + tamperedNonce := bytes.Clone(enc) + tamperedNonce[0] ^= 0xff + if _, err = DecryptPassword(testKey, tamperedNonce); err == nil { + t.Fatal("篡改 nonce 后解密应当失败") + } + // 截断密文 + if _, err = DecryptPassword(testKey, enc[:aesGcmNonceLen]); err == nil { + t.Fatal("截断密文解密应当失败") + } +} + +// TestInvalidKeyLength 非 32 字节密钥必须报错 +func TestInvalidKeyLength(t *testing.T) { + if _, err := EncryptPassword("short-key", "admin123"); err == nil { + t.Fatal("非 32 字节密钥加密应当失败") + } + enc, err := EncryptPassword(testKey, "admin123") + if err != nil { + t.Fatalf("加密失败: %v", err) + } + if _, err = DecryptPassword("short-key", enc); err == nil { + t.Fatal("非 32 字节密钥解密应当失败") + } +} + +// TestDecryptEmptyCiphertext 空密文(未设置密码)解密返回空串且不报错 +func TestDecryptEmptyCiphertext(t *testing.T) { + got, err := DecryptPassword(testKey, nil) + if err != nil { + t.Fatalf("空密文解密不应报错: %v", err) + } + if got != "" { + t.Fatalf("空密文解密应返回空串, 实际 %q", got) + } +} + +// TestBuildMysqlDsn mysql 连接串拼接格式 +func TestBuildMysqlDsn(t *testing.T) { + want := "user:pass@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai" + got := BuildMysqlDsn("127.0.0.1", 3306, "datacenter", "user", "pass") + if got != want { + t.Fatalf("DSN 不一致, 期望 %s, 实际 %s", want, got) + } +} diff --git a/datasource/rpc/internal/logic/deleteDataSourceLogic.go b/datasource/rpc/internal/logic/deleteDataSourceLogic.go index 3f1148e..275c5bd 100644 --- a/datasource/rpc/internal/logic/deleteDataSourceLogic.go +++ b/datasource/rpc/internal/logic/deleteDataSourceLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/datasource/model" "datacenter/datasource/rpc/datasource" "datacenter/datasource/rpc/internal/svc" @@ -23,9 +24,20 @@ func NewDeleteDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } } -// 删除数据源 +// 删除数据源: 软删(仅写 deleted_at, 物理数据保留) func (l *DeleteDataSourceLogic) DeleteDataSource(in *datasource.IdReq) (*datasource.OkReply, error) { - // todo: add your logic here and delete this line - - return &datasource.OkReply{}, nil + // 1.先校验存在性与 beid 归属 + if _, err := l.svcCtx.DataSourceModel.FindOne(in.Id, in.Beid); err != nil { + if err == model.ErrNotFound { + return nil, ErrNotFound + } + l.Errorf("查询数据源失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrQueryFail + } + // 2.软删 + if err := l.svcCtx.DataSourceModel.Delete(in.Id, in.Beid); err != nil { + l.Errorf("删除数据源失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrDeleteFail + } + return &datasource.OkReply{Ok: true}, nil } diff --git a/datasource/rpc/internal/logic/error.go b/datasource/rpc/internal/logic/error.go new file mode 100644 index 0000000..f457d56 --- /dev/null +++ b/datasource/rpc/internal/logic/error.go @@ -0,0 +1,33 @@ +package logic + +import "errors" + +// 业务错误定义(对齐 votes 服务风格), 交由 xzrpc 以非零 code 返回 +var ( + ErrNotFound = errors.New("数据源不存在") + ErrNameDuplicate = errors.New("数据源名称已存在") + ErrNameEmpty = errors.New("数据源名称不能为空") + ErrTypeInvalid = errors.New("数据源类型不支持, 仅支持: mysql/redis/es/http/kafka") + ErrEncryptKeyInvalid = errors.New("EncryptKey 配置不合法, 必须为 32 字节") + ErrEncryptFail = errors.New("密码加密失败") + ErrDecryptFail = errors.New("密码解密失败, 密钥不匹配或密文损坏") + ErrCreateFail = errors.New("创建数据源失败") + ErrUpdateFail = errors.New("更新数据源失败") + ErrDeleteFail = errors.New("删除数据源失败") + ErrQueryFail = errors.New("查询数据源失败") +) + +// datasourceTypes 支持的数据源类型白名单 +var datasourceTypes = map[string]struct{}{ + "mysql": {}, + "redis": {}, + "es": {}, + "http": {}, + "kafka": {}, +} + +// validDataSourceType 校验数据源类型是否在白名单内 +func validDataSourceType(typ string) bool { + _, ok := datasourceTypes[typ] + return ok +} diff --git a/datasource/rpc/internal/logic/getDataSourceLogic.go b/datasource/rpc/internal/logic/getDataSourceLogic.go index 1b5e067..ef5ccbd 100644 --- a/datasource/rpc/internal/logic/getDataSourceLogic.go +++ b/datasource/rpc/internal/logic/getDataSourceLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/datasource/model" "datacenter/datasource/rpc/datasource" "datacenter/datasource/rpc/internal/svc" @@ -23,9 +24,29 @@ func NewGetDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get } } -// 数据源详情 +// 数据源详情: with_secret=true 时解密密码并拼 DSN(仅内部服务调用), mysql 类型返回连接串 func (l *GetDataSourceLogic) GetDataSource(in *datasource.GetDataSourceReq) (*datasource.DataSourceReply, error) { - // todo: add your logic here and delete this line - - return &datasource.DataSourceReply{}, nil + // 1.查询(校验存在性与 beid 归属) + ds, err := l.svcCtx.DataSourceModel.FindOne(in.Id, in.Beid) + if err != nil { + if err == model.ErrNotFound { + return nil, ErrNotFound + } + l.Errorf("查询数据源失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrQueryFail + } + reply := &datasource.DataSourceReply{Datasource: toPbDataSource(*ds)} + if !in.WithSecret { + return reply, nil + } + // 2.with_secret: 解密密码(空密码返回空串), mysql 类型拼 DSN + plain, err := DecryptPassword(l.svcCtx.Config.EncryptKey, ds.PasswordEnc) + if err != nil { + l.Errorf("解密密码失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrDecryptFail + } + if ds.Type == "mysql" { + reply.Dsn = BuildMysqlDsn(ds.Host, ds.Port, ds.DbName, ds.Username, plain) + } + return reply, nil } diff --git a/datasource/rpc/internal/logic/listDataSourcesLogic.go b/datasource/rpc/internal/logic/listDataSourcesLogic.go index ea0bcc1..37bb879 100644 --- a/datasource/rpc/internal/logic/listDataSourcesLogic.go +++ b/datasource/rpc/internal/logic/listDataSourcesLogic.go @@ -9,6 +9,12 @@ import ( "github.com/zeromicro/go-zero/core/logx" ) +// 分页参数上限 +const ( + defaultPageSize = 10 + maxPageSize = 100 +) + type ListDataSourcesLogic struct { ctx context.Context svcCtx *svc.ServiceContext @@ -23,9 +29,27 @@ func NewListDataSourcesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *L } } -// 数据源列表 +// 数据源列表: beid 必带, type 可选过滤, 分页返回 func (l *ListDataSourcesLogic) ListDataSources(in *datasource.ListReq) (*datasource.DataSourceListReply, error) { - // todo: add your logic here and delete this line - - return &datasource.DataSourceListReply{}, nil + // 分页参数兜底 + page, size := in.Page, in.Size + if page < 1 { + page = 1 + } + if size < 1 { + size = defaultPageSize + } + if size > maxPageSize { + size = maxPageSize + } + list, total, err := l.svcCtx.DataSourceModel.FindList(in.Beid, in.Type, page, size) + if err != nil { + l.Errorf("查询数据源列表失败, beid: %d, type: %s, err: %v", in.Beid, in.Type, err) + return nil, ErrQueryFail + } + resp := &datasource.DataSourceListReply{Total: total} + for i := range list { + resp.List = append(resp.List, toPbDataSource(list[i])) + } + return resp, nil } diff --git a/datasource/rpc/internal/logic/testConnectionLogic.go b/datasource/rpc/internal/logic/testConnectionLogic.go index bc1affc..1b0c23f 100644 --- a/datasource/rpc/internal/logic/testConnectionLogic.go +++ b/datasource/rpc/internal/logic/testConnectionLogic.go @@ -2,13 +2,20 @@ package logic import ( "context" + "database/sql" + "time" + "datacenter/datasource/model" "datacenter/datasource/rpc/datasource" "datacenter/datasource/rpc/internal/svc" + _ "github.com/go-sql-driver/mysql" // 注册 mysql 驱动, 供探活使用 "github.com/zeromicro/go-zero/core/logx" ) +// testConnTimeout 探活超时时间 +const testConnTimeout = 3 * time.Second + type TestConnectionLogic struct { ctx context.Context svcCtx *svc.ServiceContext @@ -23,9 +30,45 @@ func NewTestConnectionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Te } } -// 测试连接 +// 测试连接: mysql 类型真实建连 ping 并记录耗时; 其他类型暂不支持自动探活, 直接返回 ok=true +// 注意: 探活失败不返回 error, 由 TestConnReply.Ok=false + ErrMsg 表达 func (l *TestConnectionLogic) TestConnection(in *datasource.IdReq) (*datasource.TestConnReply, error) { - // todo: add your logic here and delete this line - - return &datasource.TestConnReply{}, nil + // 1.查询数据源(校验存在性与 beid 归属) + ds, err := l.svcCtx.DataSourceModel.FindOne(in.Id, in.Beid) + if err != nil { + if err == model.ErrNotFound { + return nil, ErrNotFound + } + l.Errorf("查询数据源失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrQueryFail + } + // 2.非 mysql 类型暂不支持自动探活 + if ds.Type != "mysql" { + return &datasource.TestConnReply{Ok: true, ErrMsg: "该类型暂不支持自动探活"}, nil + } + // 3.解密密码并拼 DSN + plain, err := DecryptPassword(l.svcCtx.Config.EncryptKey, ds.PasswordEnc) + if err != nil { + l.Errorf("解密密码失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrDecryptFail + } + dsn := BuildMysqlDsn(ds.Host, ds.Port, ds.DbName, ds.Username, plain) + // 4.建连 ping, 记录耗时 + db, err := sql.Open("mysql", dsn) + if err != nil { + return &datasource.TestConnReply{Ok: false, ErrMsg: err.Error()}, nil + } + defer db.Close() + db.SetMaxOpenConns(1) + db.SetConnMaxIdleTime(0) + ctx, cancel := context.WithTimeout(l.ctx, testConnTimeout) + defer cancel() + start := time.Now() + pingErr := db.PingContext(ctx) + latencyMs := time.Since(start).Milliseconds() + if pingErr != nil { + l.Errorf("数据源探活失败, id: %d, host: %s:%d, err: %v", in.Id, ds.Host, ds.Port, pingErr) + return &datasource.TestConnReply{Ok: false, LatencyMs: latencyMs, ErrMsg: pingErr.Error()}, nil + } + return &datasource.TestConnReply{Ok: true, LatencyMs: latencyMs}, nil } diff --git a/datasource/rpc/internal/logic/updateDataSourceLogic.go b/datasource/rpc/internal/logic/updateDataSourceLogic.go index 978eb7c..fcee1e1 100644 --- a/datasource/rpc/internal/logic/updateDataSourceLogic.go +++ b/datasource/rpc/internal/logic/updateDataSourceLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "database/sql" + "datacenter/datasource/model" "datacenter/datasource/rpc/datasource" "datacenter/datasource/rpc/internal/svc" @@ -23,9 +25,76 @@ func NewUpdateDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } } -// 更新数据源 +// 更新数据源: 字符串字段非空才覆盖, port 非零才覆盖, password 为空不更新密码列, status 以请求值为准(0.停用 1.启用) func (l *UpdateDataSourceLogic) UpdateDataSource(in *datasource.UpdateDataSourceReq) (*datasource.OkReply, error) { - // todo: add your logic here and delete this line - - return &datasource.OkReply{}, nil + // 1.查询原记录(校验存在性与 beid 归属) + old, err := l.svcCtx.DataSourceModel.FindOne(in.Id, in.Beid) + if err != nil { + if err == model.ErrNotFound { + return nil, ErrNotFound + } + l.Errorf("查询数据源失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrQueryFail + } + // 2.名称变更时做 name+beid 唯一性查重(排除自身) + if in.Name != "" && in.Name != old.Name { + dup, err := l.svcCtx.DataSourceModel.FindOneByName(in.Name, in.Beid) + if err != nil && err != model.ErrNotFound { + l.Errorf("查重失败, name: %s, beid: %d, err: %v", in.Name, in.Beid, err) + return nil, ErrQueryFail + } + if dup != nil && dup.Id != in.Id { + return nil, ErrNameDuplicate + } + } + // 3.合并更新字段: 空值保留原值(proto3 无法区分未传, 按零值即不改处理) + data := *old + if in.Name != "" { + data.Name = in.Name + } + if in.Type != "" { + if !validDataSourceType(in.Type) { + return nil, ErrTypeInvalid + } + data.Type = in.Type + } + if in.Host != "" { + data.Host = in.Host + } + if in.Port != 0 { + data.Port = in.Port + } + if in.DbName != "" { + data.DbName = in.DbName + } + if in.Username != "" { + data.Username = in.Username + } + if in.ParamsJson != "" { + data.ParamsJson = sql.NullString{String: in.ParamsJson, Valid: true} + } + if in.Owner != "" { + data.Owner = in.Owner + } + if err := l.svcCtx.DataSourceModel.Update(data); err != nil { + l.Errorf("更新数据源失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrUpdateFail + } + // 4.password 为空不更新密码列, 非空则重新加密 + if in.Password != "" { + enc, err := EncryptPassword(l.svcCtx.Config.EncryptKey, in.Password) + if err != nil { + return nil, err + } + if err := l.svcCtx.DataSourceModel.UpdatePassword(in.Id, in.Beid, enc); err != nil { + l.Errorf("更新密码失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrUpdateFail + } + } + // 5.状态以请求值为准(0.停用 1.启用) + if err := l.svcCtx.DataSourceModel.UpdateStatus(in.Id, in.Beid, in.Status); err != nil { + l.Errorf("更新状态失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, ErrUpdateFail + } + return &datasource.OkReply{Ok: true}, nil } diff --git a/datasource/rpc/internal/svc/serviceContext.go b/datasource/rpc/internal/svc/serviceContext.go index 0dbf332..f62eb35 100644 --- a/datasource/rpc/internal/svc/serviceContext.go +++ b/datasource/rpc/internal/svc/serviceContext.go @@ -1,13 +1,23 @@ package svc -import "datacenter/datasource/rpc/internal/config" +import ( + "datacenter/datasource/model" + "datacenter/datasource/rpc/internal/config" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// ServiceContext 服务依赖装配 type ServiceContext struct { - Config config.Config + Config config.Config + DataSourceModel model.DataSourceModel } func NewServiceContext(c config.Config) *ServiceContext { + // mysql 连接池, 全服务共用 + conn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ - Config: c, + Config: c, + DataSourceModel: model.NewDataSourceModel(conn), } } diff --git a/datasvc.api b/datasvc.api new file mode 100644 index 0000000..0d2439f --- /dev/null +++ b/datasvc.api @@ -0,0 +1,147 @@ +syntax = "v1" + +//数据服务 API(Phase 1,参考 OneService:配置出 API、统一管控) +type ApiInfo { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + Path string `json:"path,optional"` + Mode string `json:"mode,optional"` + DatasourceId int64 `json:"datasource_id,optional"` + SqlText string `json:"sql_text,optional"` + ParamsJson string `json:"params_json,optional"` + CacheTtl int64 `json:"cache_ttl,optional"` + Status string `json:"status,optional"` + Owner string `json:"owner,optional"` +} + +type CreateApiReq { + Name string `json:"name"` + Path string `json:"path"` + Mode string `json:"mode,optional"` + DatasourceId int64 `json:"datasource_id"` + SqlText string `json:"sql_text"` + ParamsJson string `json:"params_json,optional"` + CacheTtl int64 `json:"cache_ttl,optional"` + Owner string `json:"owner,optional"` + Beid int64 `json:"beid,optional"` +} + +type UpdateApiReq { + Id int64 `json:"id"` + Beid int64 `json:"beid,optional"` + Name string `json:"name,optional"` + SqlText string `json:"sql_text,optional"` + ParamsJson string `json:"params_json,optional"` + CacheTtl int64 `json:"cache_ttl,optional"` +} + +type DsIdReq { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type ApiPathReq { + Path string `json:"path,optional" form:"path,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type ApiListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Status string `json:"status,optional" form:"status,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type ApiListResp { + List []ApiInfo `json:"list"` + Total int64 `json:"total"` +} + +type RegisterAppReq { + Name string `json:"name"` + RateLimitQps int64 `json:"rate_limit_qps,optional"` + IpWhitelist string `json:"ip_whitelist,optional"` + Beid int64 `json:"beid,optional"` +} +type AppResp { + AppId int64 `json:"app_id"` + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` //仅注册时返回一次 +} + +type GrantApiReq { + ApiId int64 `json:"api_id"` + AppId int64 `json:"app_id"` + ExpireAt int64 `json:"expire_at,optional"` //unix 秒,0 永久 + FieldMask string `json:"field_mask,optional"` + Beid int64 `json:"beid,optional"` +} + +type ExecuteApiReq { + AppKey string `json:"app_key"` + Sign string `json:"sign"` + Ts string `json:"ts"` + Path string `json:"path"` + ParamsJson string `json:"params_json,optional"` +} +type ExecuteApiResp { + RowsJson string `json:"rows_json"` + CostMs int64 `json:"cost_ms"` + CacheHit bool `json:"cache_hit"` + Affected int64 `json:"affected"` +} + +@server( + group: datasvc +) +service datacenter-api { + @doc( + summary: "创建数据API" + ) + @handler createApi + post /datasvc/api/create (CreateApiReq) returns (ApiInfo) + @doc( + summary: "更新数据API" + ) + @handler updateApi + post /datasvc/api/update (UpdateApiReq) + @doc( + summary: "删除数据API" + ) + @handler deleteApi + post /datasvc/api/delete (DsIdReq) + @doc( + summary: "API详情" + ) + @handler apiInfo + get /datasvc/api/info (ApiPathReq) returns (ApiInfo) + @doc( + summary: "API列表" + ) + @handler apiList + get /datasvc/api/list (ApiListReq) returns (ApiListResp) + @doc( + summary: "发布API" + ) + @handler publishApi + post /datasvc/api/publish (DsIdReq) + @doc( + summary: "下线API" + ) + @handler offlineApi + post /datasvc/api/offline (DsIdReq) + @doc( + summary: "注册调用应用" + ) + @handler registerApp + post /datasvc/app/register (RegisterAppReq) returns (AppResp) + @doc( + summary: "授权API给应用" + ) + @handler grantApi + post /datasvc/grant (GrantApiReq) + @doc( + summary: "执行数据API(外部统一入口)" + ) + @handler executeApi + post /datasvc/execute (ExecuteApiReq) returns (ExecuteApiResp) +} diff --git a/datasvc/model/apiModel.go b/datasvc/model/apiModel.go new file mode 100644 index 0000000..767dbca --- /dev/null +++ b/datasvc/model/apiModel.go @@ -0,0 +1,180 @@ +package model + +import ( + "fmt" + "strings" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + apiFieldNames = builder.RawFieldNames(&Api{}) + apiRowsExpectAutoSet = strings.Join(stringx.Remove(apiFieldNames, "`id`", "`created_at`", "`updated_at`"), ",") +) + +// apiSelectRows 查询列:sql_text/params_json/resp_sample_json/owner 为可空文本列, +// 用 ifnull 兜底为空串,避免 NULL 扫描报错 +const apiSelectRows = "`id`,`name`,`path`,`mode`,`datasource_id`," + + "ifnull(`sql_text`,'') as `sql_text`,ifnull(`params_json`,'') as `params_json`," + + "`cache_ttl`,`status`,ifnull(`resp_sample_json`,'') as `resp_sample_json`," + + "ifnull(`owner`,'') as `owner`,`beid`" + +type ( + // ApiModel dc_api 表模型(配置出 API 的核心元数据) + ApiModel interface { + // Insert 新建 API,返回自增 id + Insert(data *Api) (int64, error) + // FindOne 按主键查询 + FindOne(id int64) (*Api, error) + // FindByPath 按 beid+path 查询(执行入口查找 / 唯一性校验) + FindByPath(beid int64, path string) (*Api, error) + // List 按 beid(可选 status)分页查询,返回列表与总数 + List(beid int64, status string, page, size int64) ([]*Api, int64, error) + // Update 更新可编辑字段(name/sql_text/params_json/cache_ttl/resp_sample_json) + Update(data *Api) error + // UpdateStatus 状态流转:draft/published/offline + UpdateStatus(id int64, status string) error + // Delete 删除 API + Delete(id int64) error + } + + defaultApiModel struct { + conn sqlx.SqlConn + table string + } + + // Api dc_api 表结构 + Api struct { + Id int64 `db:"id"` + Name string `db:"name"` + Path string `db:"path"` //执行路径标识 + Mode string `db:"mode"` //wizard/sql/register + DatasourceId int64 `db:"datasource_id"` + SqlText string `db:"sql_text"` //SQL 模板,:name 占位 + ParamsJson string `db:"params_json"` + CacheTtl int64 `db:"cache_ttl"` //秒,0 不缓存 + Status string `db:"status"` //draft/published/offline + RespSampleJson string `db:"resp_sample_json"` + Owner string `db:"owner"` + Beid int64 `db:"beid"` + } +) + +func NewApiModel(conn sqlx.SqlConn) ApiModel { + return &defaultApiModel{ + conn: conn, + table: "`dc_api`", + } +} + +// Insert 新建 API,返回自增 id +func (m *defaultApiModel) Insert(data *Api) (int64, error) { + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + m.table, apiRowsExpectAutoSet) + ret, err := m.conn.Exec(query, + data.Name, data.Path, data.Mode, data.DatasourceId, data.SqlText, + data.ParamsJson, data.CacheTtl, data.Status, data.RespSampleJson, + data.Owner, data.Beid) + if err != nil { + return 0, err + } + return ret.LastInsertId() +} + +// FindOne 按主键查询 +func (m *defaultApiModel) FindOne(id int64) (*Api, error) { + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", apiSelectRows, m.table) + var resp Api + err := m.conn.QueryRow(&resp, query, id) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindByPath 按 beid+path 查询(uk_beid_path 唯一键) +func (m *defaultApiModel) FindByPath(beid int64, path string) (*Api, error) { + query := fmt.Sprintf("select %s from %s where `beid` = ? and `path` = ? limit 1", + apiSelectRows, m.table) + var resp Api + err := m.conn.QueryRow(&resp, query, beid, path) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// List 按 beid(可选 status)分页查询,返回列表与总数 +func (m *defaultApiModel) List(beid int64, status string, page, size int64) ([]*Api, int64, error) { + //分页参数兜底 + if page < 1 { + page = 1 + } + if size < 1 { + size = 20 + } + where := "`beid` = ?" + args := []interface{}{beid} + if strings.TrimSpace(status) != "" { + where += " and `status` = ?" + args = append(args, status) + } + var total int64 + err := m.conn.QueryRow(&total, + fmt.Sprintf("select count(*) from %s where %s", m.table, where), args...) + if err != nil { + return nil, 0, err + } + if total == 0 { + return []*Api{}, 0, nil + } + query := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ? offset ?", + apiSelectRows, m.table, where) + args = append(args, size, (page-1)*size) + var list []*Api + err = m.conn.QueryRows(&list, query, args...) + switch err { + case nil: + return list, total, nil + case sqlc.ErrNotFound: + return []*Api{}, total, nil + default: + return nil, 0, err + } +} + +// Update 更新可编辑字段(name/sql_text/params_json/cache_ttl/resp_sample_json),带 beid 租户条件 +func (m *defaultApiModel) Update(data *Api) error { + query := fmt.Sprintf( + "update %s set `name` = ?, `sql_text` = ?, `params_json` = ?, `cache_ttl` = ?, `resp_sample_json` = ? "+ + "where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.Exec(query, + data.Name, data.SqlText, data.ParamsJson, data.CacheTtl, data.RespSampleJson, + data.Id, data.Beid) + return err +} + +// UpdateStatus 状态流转 +func (m *defaultApiModel) UpdateStatus(id int64, status string) error { + query := fmt.Sprintf("update %s set `status` = ? where `id` = ?", m.table) + _, err := m.conn.Exec(query, status, id) + return err +} + +// Delete 删除 API +func (m *defaultApiModel) Delete(id int64) error { + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + _, err := m.conn.Exec(query, id) + return err +} diff --git a/datasvc/model/appModel.go b/datasvc/model/appModel.go new file mode 100644 index 0000000..d23113e --- /dev/null +++ b/datasvc/model/appModel.go @@ -0,0 +1,91 @@ +package model + +import ( + "fmt" + + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type ( + // AppModel dc_api_app 表模型(调用方应用) + AppModel interface { + // Insert 注册应用,返回自增 id + Insert(data *ApiApp) (int64, error) + // FindOne 按主键查询 + FindOne(id int64) (*ApiApp, error) + // FindByKey 按 app_key 查询(执行链鉴权入口) + FindByKey(appKey string) (*ApiApp, error) + } + + defaultAppModel struct { + conn sqlx.SqlConn + table string + } + + // ApiApp dc_api_app 表结构 + ApiApp struct { + Id int64 `db:"id"` + AppKey string `db:"app_key"` + AppSecretEnc []byte `db:"app_secret_enc"` //AES-GCM 加密后的密钥 + Name string `db:"name"` + RateLimitQps int64 `db:"rate_limit_qps"` + IpWhitelist string `db:"ip_whitelist"` //逗号分隔,空为不限 + Beid int64 `db:"beid"` + } +) + +func NewAppModel(conn sqlx.SqlConn) AppModel { + return &defaultAppModel{ + conn: conn, + table: "`dc_api_app`", + } +} + +// Insert 注册应用,返回自增 id +func (m *defaultAppModel) Insert(data *ApiApp) (int64, error) { + query := fmt.Sprintf( + "insert into %s (`app_key`, `app_secret_enc`, `name`, `rate_limit_qps`, `ip_whitelist`, `beid`) "+ + "values (?, ?, ?, ?, ?, ?)", m.table) + ret, err := m.conn.Exec(query, + data.AppKey, data.AppSecretEnc, data.Name, data.RateLimitQps, + data.IpWhitelist, data.Beid) + if err != nil { + return 0, err + } + return ret.LastInsertId() +} + +// FindOne 按主键查询 +func (m *defaultAppModel) FindOne(id int64) (*ApiApp, error) { + query := fmt.Sprintf( + "select `id`, `app_key`, `app_secret_enc`, `name`, `rate_limit_qps`, `ip_whitelist`, `beid` "+ + "from %s where `id` = ? limit 1", m.table) + var resp ApiApp + err := m.conn.QueryRow(&resp, query, id) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindByKey 按 app_key 查询(uk_app_key 唯一键) +func (m *defaultAppModel) FindByKey(appKey string) (*ApiApp, error) { + query := fmt.Sprintf( + "select `id`, `app_key`, `app_secret_enc`, `name`, `rate_limit_qps`, `ip_whitelist`, `beid` "+ + "from %s where `app_key` = ? limit 1", m.table) + var resp ApiApp + err := m.conn.QueryRow(&resp, query, appKey) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} diff --git a/datasvc/model/grantModel.go b/datasvc/model/grantModel.go new file mode 100644 index 0000000..cfcff8d --- /dev/null +++ b/datasvc/model/grantModel.go @@ -0,0 +1,99 @@ +package model + +import ( + "database/sql" + "fmt" + + "github.com/zeromicro/go-zero/core/stores/sqlc" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type ( + // GrantModel dc_api_grant 表模型(API 授权关系) + GrantModel interface { + // Insert 授权(幂等:uk_api_app 冲突时刷新过期时间与脱敏字段) + Insert(data *ApiGrant) error + // FindByApiAndApp 查询指定 API 与应用的授权 + FindByApiAndApp(apiId, appId int64) (*ApiGrant, error) + // ListByApp 查询应用名下全部授权 + ListByApp(appId int64) ([]*ApiGrant, error) + // DeleteByApi 删除某 API 的全部授权(API 删除时清理) + DeleteByApi(apiId int64) error + } + + defaultGrantModel struct { + conn sqlx.SqlConn + table string + } + + // ApiGrant dc_api_grant 表结构 + ApiGrant struct { + Id int64 `db:"id"` + ApiId int64 `db:"api_id"` + AppId int64 `db:"app_id"` + ExpireAt sql.NullTime `db:"expire_at"` //NULL 表示永久 + FieldMask string `db:"field_mask"` //脱敏字段,逗号分隔 + Beid int64 `db:"beid"` + } +) + +func NewGrantModel(conn sqlx.SqlConn) GrantModel { + return &defaultGrantModel{ + conn: conn, + table: "`dc_api_grant`", + } +} + +// grantSelectRows 查询列:field_mask 为可空列,用 ifnull 兜底为空串 +const grantSelectRows = "`id`, `api_id`, `app_id`, `expire_at`, " + + "ifnull(`field_mask`,'') as `field_mask`, `beid`" + +// Insert 授权(幂等):重复授权同 api+app 时不报错,刷新过期时间/脱敏字段/租户 +func (m *defaultGrantModel) Insert(data *ApiGrant) error { + query := fmt.Sprintf( + "insert into %s (`api_id`, `app_id`, `expire_at`, `field_mask`, `beid`) values (?, ?, ?, ?, ?) "+ + "on duplicate key update `expire_at` = values(`expire_at`), "+ + "`field_mask` = values(`field_mask`), `beid` = values(`beid`)", m.table) + _, err := m.conn.Exec(query, + data.ApiId, data.AppId, data.ExpireAt, data.FieldMask, data.Beid) + return err +} + +// FindByApiAndApp 查询指定 API 与应用的授权 +func (m *defaultGrantModel) FindByApiAndApp(apiId, appId int64) (*ApiGrant, error) { + query := fmt.Sprintf("select %s from %s where `api_id` = ? and `app_id` = ? limit 1", + grantSelectRows, m.table) + var resp ApiGrant + err := m.conn.QueryRow(&resp, query, apiId, appId) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } +} + +// ListByApp 查询应用名下全部授权 +func (m *defaultGrantModel) ListByApp(appId int64) ([]*ApiGrant, error) { + query := fmt.Sprintf("select %s from %s where `app_id` = ? order by `id` desc", + grantSelectRows, m.table) + var list []*ApiGrant + err := m.conn.QueryRows(&list, query, appId) + switch err { + case nil: + return list, nil + case sqlc.ErrNotFound: + return []*ApiGrant{}, nil + default: + return nil, err + } +} + +// DeleteByApi 删除某 API 的全部授权 +func (m *defaultGrantModel) DeleteByApi(apiId int64) error { + query := fmt.Sprintf("delete from %s where `api_id` = ?", m.table) + _, err := m.conn.Exec(query, apiId) + return err +} diff --git a/datasvc/model/vars.go b/datasvc/model/vars.go new file mode 100644 index 0000000..d8b5a90 --- /dev/null +++ b/datasvc/model/vars.go @@ -0,0 +1,6 @@ +package model + +import "github.com/zeromicro/go-zero/core/stores/sqlx" + +// ErrNotFound 通用未找到错误(对齐 votes 服务 model 风格) +var ErrNotFound = sqlx.ErrNotFound diff --git a/datasvc/rpc/internal/logic/convert.go b/datasvc/rpc/internal/logic/convert.go new file mode 100644 index 0000000..74a64b2 --- /dev/null +++ b/datasvc/rpc/internal/logic/convert.go @@ -0,0 +1,42 @@ +package logic + +import ( + "datacenter/datasvc/model" + "datacenter/datasvc/rpc/datasvc" +) + +// apiToPb model.Api → pb Api +func apiToPb(a *model.Api) *datasvc.Api { + if a == nil { + return nil + } + return &datasvc.Api{ + Id: a.Id, + Name: a.Name, + Path: a.Path, + Mode: a.Mode, + DatasourceId: a.DatasourceId, + SqlText: a.SqlText, + ParamsJson: a.ParamsJson, + CacheTtl: a.CacheTtl, + Status: a.Status, + RespSampleJson: a.RespSampleJson, + Owner: a.Owner, + Beid: a.Beid, + } +} + +// appToPb model.ApiApp → pb ApiApp(不回传加密密钥) +func appToPb(a *model.ApiApp) *datasvc.ApiApp { + if a == nil { + return nil + } + return &datasvc.ApiApp{ + Id: a.Id, + AppKey: a.AppKey, + Name: a.Name, + RateLimitQps: a.RateLimitQps, + IpWhitelist: a.IpWhitelist, + Beid: a.Beid, + } +} diff --git a/datasvc/rpc/internal/logic/createApiLogic.go b/datasvc/rpc/internal/logic/createApiLogic.go index d60cb70..d5b9924 100644 --- a/datasvc/rpc/internal/logic/createApiLogic.go +++ b/datasvc/rpc/internal/logic/createApiLogic.go @@ -2,7 +2,11 @@ package logic import ( "context" + "errors" + "fmt" + "strings" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -25,7 +29,72 @@ func NewCreateApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateA // 创建API func (l *CreateApiLogic) CreateApi(in *datasvc.CreateApiReq) (*datasvc.ApiReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.ApiReply{}, nil + //基础字段校验 + name := strings.TrimSpace(in.Name) + path := strings.TrimSpace(in.Path) + if name == "" { + return nil, fmt.Errorf("%w: name 不能为空", ErrParamInvalid) + } + if path == "" || strings.ContainsAny(path, " \t\r\n") { + return nil, fmt.Errorf("%w: path 非法,不能为空且不含空白字符", ErrParamInvalid) + } + if in.DatasourceId <= 0 { + return nil, fmt.Errorf("%w: datasource_id 必须大于 0", ErrParamInvalid) + } + if in.CacheTtl < 0 { + return nil, fmt.Errorf("%w: cache_ttl 不能为负数", ErrParamInvalid) + } + //模式:缺省 sql;wizard=向导生成(仅 SELECT)、sql=自由 SQL(允许写语句)、register=登记的外部 API + mode := strings.TrimSpace(in.Mode) + if mode == "" { + mode = "sql" + } + switch mode { + case "wizard", "sql", "register": + default: + return nil, fmt.Errorf("%w: mode 非法,仅支持 wizard/sql/register", ErrParamInvalid) + } + //params_json 合法性校验 + if _, err := ParseParamDefs(in.ParamsJson); err != nil { + return nil, fmt.Errorf("%w: %v", ErrParamInvalid, err) + } + //wizard 模式强制 sql_text 以 SELECT 开头(防写);sql 模式不限 + if mode == "wizard" { + if strings.TrimSpace(in.SqlText) == "" { + return nil, fmt.Errorf("%w: wizard 模式 sql_text 不能为空", ErrParamInvalid) + } + if !IsSelectStmt(in.SqlText) { + return nil, fmt.Errorf("%w: wizard 模式仅允许 SELECT 查询模板", ErrParamInvalid) + } + } + //path 唯一性(beid+path) + if _, err := l.svcCtx.ApiModel.FindByPath(in.Beid, path); err == nil { + return nil, ErrPathDuplicate + } else if !errors.Is(err, model.ErrNotFound) { + return nil, err + } + data := &model.Api{ + Name: name, + Path: path, + Mode: mode, + DatasourceId: in.DatasourceId, + SqlText: in.SqlText, + ParamsJson: strings.TrimSpace(in.ParamsJson), + CacheTtl: in.CacheTtl, + Status: "draft", //新建即为草稿,需显式发布 + RespSampleJson: in.RespSampleJson, + Owner: strings.TrimSpace(in.Owner), + Beid: in.Beid, + } + id, err := l.svcCtx.ApiModel.Insert(data) + if err != nil { + //兜底:并发创建触发 uk_beid_path 唯一键冲突 + if strings.Contains(err.Error(), "Duplicate entry") { + return nil, ErrPathDuplicate + } + return nil, err + } + data.Id = id + l.Infof("创建API成功 id=%d beid=%d path=%s mode=%s", id, data.Beid, data.Path, data.Mode) + return &datasvc.ApiReply{Api: apiToPb(data)}, nil } diff --git a/datasvc/rpc/internal/logic/credential.go b/datasvc/rpc/internal/logic/credential.go new file mode 100644 index 0000000..6ec1f9d --- /dev/null +++ b/datasvc/rpc/internal/logic/credential.go @@ -0,0 +1,135 @@ +package logic + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "datacenter/datasvc/rpc/internal/svc" +) + +// signToleranceSeconds 签名时间戳允许的服务器时间偏差(秒),超过即拒绝 +const signToleranceSeconds int64 = 300 + +// aesKey 规范化 AES 密钥:32 字节直接使用,否则用 sha256 派生 32 字节(AES-256) +func aesKey(key string) []byte { + if len(key) == 32 { + return []byte(key) + } + sum := sha256.Sum256([]byte(key)) + return sum[:] +} + +// aesGcmEncrypt AES-GCM 加密,输出 nonce||密文 的字节串(存 varbinary 列) +func aesGcmEncrypt(key, plaintext string) ([]byte, error) { + block, err := aes.NewCipher(aesKey(key)) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = rand.Read(nonce); err != nil { + return nil, err + } + return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil +} + +// aesGcmDecrypt AES-GCM 解密(输入 nonce||密文) +func aesGcmDecrypt(key string, data []byte) (string, error) { + block, err := aes.NewCipher(aesKey(key)) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + if len(data) < gcm.NonceSize() { + return "", errors.New("密文长度非法") + } + plain, err := gcm.Open(nil, data[:gcm.NonceSize()], data[gcm.NonceSize():], nil) + if err != nil { + return "", err + } + return string(plain), nil +} + +// randomHex 生成 n 字节加密随机数的 hex 字符串(2n 个字符) +func randomHex(n int) (string, error) { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +// genSign 计算执行签名:hex(hmac-sha256(key=app_secret, msg=app_key+ts)) +func genSign(appKey, ts, appSecret string) string { + mac := hmac.New(sha256.New, []byte(appSecret)) + mac.Write([]byte(appKey + ts)) + return hex.EncodeToString(mac.Sum(nil)) +} + +// verifySign 校验执行请求签名(纯函数,now 由调用方注入便于测试): +// 1) ts 必须是合法秒级时间戳;2) 与服务器时间偏差不得超过 300 秒; +// 3) sign 必须与 hex(hmac-sha256(app_key+ts)) 常量时间比对一致。 +func verifySign(appKey, ts, sign, appSecret string, now time.Time) error { + tsv, err := strconv.ParseInt(strings.TrimSpace(ts), 10, 64) + if err != nil { + return fmt.Errorf("%w: ts 非法", ErrSignInvalid) + } + diff := now.Unix() - tsv + if diff < 0 { + diff = -diff + } + if diff > signToleranceSeconds { + return fmt.Errorf("%w: ts 与服务器时间偏差超过 %d 秒", ErrSignInvalid, signToleranceSeconds) + } + expect, err := hex.DecodeString(genSign(appKey, ts, appSecret)) + if err != nil { + return fmt.Errorf("%w: 签名计算失败", ErrSignInvalid) + } + got, err := hex.DecodeString(strings.TrimSpace(sign)) + if err != nil { + return fmt.Errorf("%w: sign 格式非法", ErrSignInvalid) + } + if !hmac.Equal(expect, got) { + return fmt.Errorf("%w: 签名不匹配", ErrSignInvalid) + } + return nil +} + +// ipAllowed 校验来源 IP:白名单为空表示不限;非空时 ip 必须精确命中其中一项(忽略空白项) +func ipAllowed(ip, whitelist string) bool { + whitelist = strings.TrimSpace(whitelist) + if whitelist == "" { + return true + } + for _, item := range strings.Split(whitelist, ",") { + if strings.TrimSpace(item) == ip { + return true + } + } + return false +} + +// encryptAppSecret 用 config.EncryptKey 加密应用密钥 +func encryptAppSecret(svcCtx *svc.ServiceContext, secret string) ([]byte, error) { + return aesGcmEncrypt(svcCtx.Config.EncryptKey, secret) +} + +// decryptAppSecret 用 config.EncryptKey 解密应用密钥 +func decryptAppSecret(svcCtx *svc.ServiceContext, data []byte) (string, error) { + return aesGcmDecrypt(svcCtx.Config.EncryptKey, data) +} diff --git a/datasvc/rpc/internal/logic/credential_test.go b/datasvc/rpc/internal/logic/credential_test.go new file mode 100644 index 0000000..12b5a23 --- /dev/null +++ b/datasvc/rpc/internal/logic/credential_test.go @@ -0,0 +1,134 @@ +package logic + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "testing" + "time" +) + +// mustSign 按契约计算签名:hex(hmac-sha256(key=app_secret, msg=app_key+ts)) +func mustSign(t *testing.T, appKey, ts, secret string) string { + t.Helper() + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(appKey + ts)) + return hex.EncodeToString(mac.Sum(nil)) +} + +// 签名校验:正确签名 / 过期 ts / 错误密钥 / 非法 ts / 非法 sign +func TestVerifySign(t *testing.T) { + now := time.Unix(1700000000, 0) + const ( + appKey = "app_0123456789abcdef" + secret = "0123456789abcdef0123456789abcdef" + ) + ts := "1700000000" + + // 正确签名 + if err := verifySign(appKey, ts, mustSign(t, appKey, ts, secret), secret, now); err != nil { + t.Fatalf("正确签名不应报错: %v", err) + } + // 错误密钥 + if err := verifySign(appKey, ts, mustSign(t, appKey, ts, "wrong-secret"), secret, now); !errors.Is(err, ErrSignInvalid) { + t.Fatalf("错误密钥应返回 ErrSignInvalid, got: %v", err) + } + // 过期 ts(早 400 秒,超过 300 秒窗口) + oldTs := "1699999600" + if err := verifySign(appKey, oldTs, mustSign(t, appKey, oldTs, secret), secret, now); !errors.Is(err, ErrSignInvalid) { + t.Fatalf("过期 ts 应返回 ErrSignInvalid, got: %v", err) + } + // 未来 ts 超窗(晚 400 秒) + futureTs := "1700000400" + if err := verifySign(appKey, futureTs, mustSign(t, appKey, futureTs, secret), secret, now); !errors.Is(err, ErrSignInvalid) { + t.Fatalf("未来 ts 超窗应返回 ErrSignInvalid, got: %v", err) + } + // 恰好 300 秒边界,应放行 + boundaryTs := "1699999700" + if err := verifySign(appKey, boundaryTs, mustSign(t, appKey, boundaryTs, secret), secret, now); err != nil { + t.Fatalf("边界 ts(300s)不应报错: %v", err) + } + // ts 非数字 + if err := verifySign(appKey, "abc", mustSign(t, appKey, "abc", secret), secret, now); !errors.Is(err, ErrSignInvalid) { + t.Fatalf("非法 ts 应返回 ErrSignInvalid, got: %v", err) + } + // sign 非 hex + if err := verifySign(appKey, ts, "zz-not-hex", secret, now); !errors.Is(err, ErrSignInvalid) { + t.Fatalf("非法 sign 应返回 ErrSignInvalid, got: %v", err) + } + // sign 长度不足 + if err := verifySign(appKey, ts, "abcd", secret, now); !errors.Is(err, ErrSignInvalid) { + t.Fatalf("长度不足 sign 应返回 ErrSignInvalid, got: %v", err) + } +} + +// AES-GCM 加解密回环:加密后可解出明文,篡改密文解密失败 +func TestAesGcmRoundTrip(t *testing.T) { + const key = "0123456789abcdef0123456789abcdef" // 32 字节 + plain := "app_secret_plain_text" + enc, err := aesGcmEncrypt(key, plain) + if err != nil { + t.Fatalf("加密失败: %v", err) + } + got, err := aesGcmDecrypt(key, enc) + if err != nil { + t.Fatalf("解密失败: %v", err) + } + if got != plain { + t.Fatalf("回环不一致: want %q got %q", plain, got) + } + // 非对称密钥(非 32 字节)走 sha256 派生,也应回环一致 + enc2, _ := aesGcmEncrypt("short-key", plain) + got2, err := aesGcmDecrypt("short-key", enc2) + if err != nil || got2 != plain { + t.Fatalf("派生密钥回环失败: %v %q", err, got2) + } + // 篡改密文应解密失败 + if len(enc) > 0 { + bad := append([]byte{}, enc...) + bad[len(bad)-1] ^= 0xff + if _, err := aesGcmDecrypt(key, bad); err == nil { + t.Fatal("篡改后的密文不应解密成功") + } + } + // 密文过短 + if _, err := aesGcmDecrypt(key, []byte{1, 2}); err == nil { + t.Fatal("过短密文不应解密成功") + } +} + +// IP 白名单:空=不限;非空需精确命中(忽略空白项) +func TestIpAllowed(t *testing.T) { + cases := []struct { + ip string + whitelist string + want bool + }{ + {"10.0.0.1", "", true}, //空白名单=不限 + {"10.0.0.1", "10.0.0.1,192.168.1.1", true}, //命中 + {"10.0.0.1", " 10.0.0.1 , 192.168.1.1 ", true}, //带空格命中 + {"10.0.0.2", "10.0.0.1,192.168.1.1", false}, //未命中 + {"", "10.0.0.1", false}, //空 ip 不放行 + } + for _, c := range cases { + if got := ipAllowed(c.ip, c.whitelist); got != c.want { + t.Fatalf("ipAllowed(%q, %q) = %v, want %v", c.ip, c.whitelist, got, c.want) + } + } +} + +// 随机串:长度与唯一性 +func TestRandomHex(t *testing.T) { + a, err := randomHex(8) + if err != nil || len(a) != 16 { + t.Fatalf("randomHex(8) 长度应为 16, got %q err %v", a, err) + } + b, _ := randomHex(16) + if len(b) != 32 { + t.Fatalf("randomHex(16) 长度应为 32, got %q", b) + } + if a == b[:16] { + t.Fatal("随机串不应重复") + } +} diff --git a/datasvc/rpc/internal/logic/deleteApiLogic.go b/datasvc/rpc/internal/logic/deleteApiLogic.go index f0aa252..48a8c2f 100644 --- a/datasvc/rpc/internal/logic/deleteApiLogic.go +++ b/datasvc/rpc/internal/logic/deleteApiLogic.go @@ -2,7 +2,10 @@ package logic import ( "context" + "errors" + "fmt" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -23,9 +26,29 @@ func NewDeleteApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteA } } -// 删除API +// 删除API(同时清理该 API 的授权关系) func (l *DeleteApiLogic) DeleteApi(in *datasvc.IdReq) (*datasvc.OkReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.OkReply{}, nil + if in.Id <= 0 { + return nil, fmt.Errorf("%w: id 非法", ErrParamInvalid) + } + api, err := l.svcCtx.ApiModel.FindOne(in.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrApiNotFound + } + return nil, err + } + //多租户校验 + if in.Beid != 0 && api.Beid != in.Beid { + return nil, ErrApiNotFound + } + //先清理授权关系,再删 API + if err := l.svcCtx.GrantModel.DeleteByApi(api.Id); err != nil { + return nil, err + } + if err := l.svcCtx.ApiModel.Delete(api.Id); err != nil { + return nil, err + } + l.Infof("删除API成功 id=%d beid=%d path=%s", api.Id, api.Beid, api.Path) + return &datasvc.OkReply{Ok: true}, nil } diff --git a/datasvc/rpc/internal/logic/error.go b/datasvc/rpc/internal/logic/error.go new file mode 100644 index 0000000..90c7d10 --- /dev/null +++ b/datasvc/rpc/internal/logic/error.go @@ -0,0 +1,33 @@ +package logic + +import "errors" + +// 业务错误:调用方可用 errors.Is 判断语义(对齐 votes 服务的业务错误风格) +var ( + // ErrAppNotFound 应用不存在(含跨租户按不存在处理) + ErrAppNotFound = errors.New("应用不存在") + // ErrSignInvalid 签名校验失败(签名不匹配 / ts 缺失、非法或超出时间窗口) + ErrSignInvalid = errors.New("签名校验失败") + // ErrIpDenied 来源 IP 不在应用白名单内 + ErrIpDenied = errors.New("来源 IP 不在白名单内,拒绝访问") + // ErrRateLimited 触发应用级限流 + ErrRateLimited = errors.New("请求超过应用限流阈值,请稍后重试") + // ErrNotPublished API 未发布(draft/offline),拒绝执行 + ErrNotPublished = errors.New("API 未发布,拒绝执行") + // ErrNotGranted 应用未获得该 API 授权,或授权已过期 + ErrNotGranted = errors.New("应用未获得该 API 授权或授权已过期") + // ErrParamInvalid 参数校验失败(params_json 定义非法 / 业务参数缺必填、多余、类型错误等) + ErrParamInvalid = errors.New("参数校验失败") + // ErrPathDuplicate API 路径在同租户下已存在 + ErrPathDuplicate = errors.New("API 路径已存在") + // ErrApiNotFound API 不存在 + ErrApiNotFound = errors.New("API 不存在") + // ErrSqlEmpty SQL 模板为空(发布前置校验) + ErrSqlEmpty = errors.New("SQL 模板为空") + // ErrDatasource 获取目标数据源/DSN 失败 + ErrDatasource = errors.New("获取数据源连接失败") + // ErrSqlExec 目标库 SQL 执行失败 + ErrSqlExec = errors.New("SQL 执行失败") + // ErrSecretCrypto 应用密钥 AES-GCM 加解密失败 + ErrSecretCrypto = errors.New("应用密钥加解密失败") +) diff --git a/datasvc/rpc/internal/logic/executeApiLogic.go b/datasvc/rpc/internal/logic/executeApiLogic.go index e7ee100..3dc8c0f 100644 --- a/datasvc/rpc/internal/logic/executeApiLogic.go +++ b/datasvc/rpc/internal/logic/executeApiLogic.go @@ -2,7 +2,14 @@ package logic import ( "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -23,9 +30,168 @@ func NewExecuteApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Execut } } -// 执行 API(核心:鉴权→限流→缓存→执行→脱敏) +// 执行 API(核心链路,严格按序,任一步失败立即拒绝): +// a 应用鉴权(app_key+签名+ts) → b IP 白名单 → c 应用限流 → d API 配置(status=published)+授权校验 +// → e 参数校验 → f SQL 构建(:name→? 白名单替换) → g 结果缓存 → h 脱敏 → i 返回 rows/affected/cost func (l *ExecuteApiLogic) ExecuteApi(in *datasvc.ExecuteReq) (*datasvc.ExecuteReply, error) { - // todo: add your logic here and delete this line + start := time.Now() - return &datasvc.ExecuteReply{}, nil + //a. 应用鉴权:app_key 查应用(不存在→ErrAppNotFound) + app, err := l.svcCtx.AppModel.FindByKey(strings.TrimSpace(in.AppKey)) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrAppNotFound + } + return nil, err + } + //多租户隔离:beid 不匹配按应用不存在处理,避免泄漏存在性 + if in.Beid != 0 && app.Beid != in.Beid { + return nil, ErrAppNotFound + } + //比对签名:hex(hmac-sha256(app_key+ts)),密钥为解密后的 app_secret;ts 偏差>300s 拒绝 + secret, err := decryptAppSecret(l.svcCtx, app.AppSecretEnc) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrSecretCrypto, err) + } + if err := verifySign(app.AppKey, in.Ts, in.Sign, secret, time.Now()); err != nil { + return nil, err + } + + //b. IP 白名单(空=不限;非空且 ip 不在列表→ErrIpDenied) + if !ipAllowed(in.Ip, app.IpWhitelist) { + return nil, ErrIpDenied + } + + //c. 限流:每应用 rate_limit_qps(进程内令牌桶,超限→ErrRateLimited) + if !l.svcCtx.Allow(app.Id, app.RateLimitQps) { + return nil, ErrRateLimited + } + + //d. 查 API 配置(path+beid)且 status=published;查授权(api_id+app_id,含过期时间校验) + beid := in.Beid + if beid == 0 { + beid = app.Beid //未显式传租户时回落应用所属租户 + } + api, err := l.svcCtx.ApiModel.FindByPath(beid, strings.TrimSpace(in.Path)) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrApiNotFound + } + return nil, err + } + if api.Status != "published" { + return nil, ErrNotPublished + } + grant, err := l.svcCtx.GrantModel.FindByApiAndApp(api.Id, app.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrNotGranted + } + return nil, err + } + if grant.ExpireAt.Valid && !time.Now().Before(grant.ExpireAt.Time) { + return nil, fmt.Errorf("%w: 授权已过期", ErrNotGranted) + } + + //e. 参数校验:按 params_json 定义收敛业务参数(缺 required 报错/多余拒绝/缺省补 default) + defs, err := ParseParamDefs(api.ParamsJson) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrParamInvalid, err) + } + values, err := ValidateParams(defs, in.ParamsJson) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrParamInvalid, err) + } + + //f. SQL 执行准备::name 占位符按白名单顺序替换为 ? 并组装参数数组 + sqlText, args, err := BuildSql(api.SqlText, defs, values) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrParamInvalid, err) + } + isSelect := IsSelectStmt(sqlText) + //写语句(mode=sql 且非 SELECT 开头)不缓存 + cacheable := api.CacheTtl > 0 && isSelect + var cacheKey string + if cacheable { + cacheKey = CacheKey(api.Id, values) + var cached string + if err := l.svcCtx.ResultCache.Get(cacheKey, &cached); err == nil { + if rows, derr := decodeRows(cached); derr == nil { + //h. 脱敏(缓存存原始结果,脱敏按授权在出口执行) + MaskRows(rows, grant.FieldMask) + if out, merr := json.Marshal(rows); merr == nil { + return &datasvc.ExecuteReply{ + RowsJson: string(out), + CostMs: time.Since(start).Milliseconds(), + CacheHit: true, + }, nil + } + } + l.Errorf("ExecuteApi 缓存值解析失败,降级直查 api=%d key=%s", api.Id, cacheKey) + } else if !l.svcCtx.ResultCache.IsNotFound(err) { + l.Errorf("ExecuteApi 读缓存失败,降级直查 api=%d err=%v", api.Id, err) + } + } + + //获取目标库 DSN:datasource rpc GetDataSource(id, beid, with_secret=true) + dsReply, err := l.svcCtx.DataSourceRpc.GetDataSource(l.ctx, &dataSourceClient.GetDataSourceReq{ + Id: api.DatasourceId, + Beid: beid, + WithSecret: true, + }) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrDatasource, err) + } + if strings.TrimSpace(dsReply.GetDsn()) == "" { + return nil, fmt.Errorf("%w: 数据源 %d 未返回 DSN", ErrDatasource, api.DatasourceId) + } + //连接目标库:sqlx.NewMysql(dsn) 直连,连接池按 DSN 缓存复用 + conn, err := l.svcCtx.TargetConn(dsReply.Dsn) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrDatasource, err) + } + db, err := conn.RawDB() + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrDatasource, err) + } + + if isSelect { + //SELECT:结果集转 JSON + rows, err := QueryRowsAsMaps(l.ctx, db, sqlText, args) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrSqlExec, err) + } + rawJson, err := json.Marshal(rows) + if err != nil { + return nil, fmt.Errorf("%w: 结果集序列化失败: %v", ErrSqlExec, err) + } + //回填缓存(存原始结果,TTL=cache_ttl;写缓存失败不影响返回) + if cacheable { + if err := l.svcCtx.ResultCache.SetWithExpire(cacheKey, string(rawJson), + time.Duration(api.CacheTtl)*time.Second); err != nil { + l.Errorf("ExecuteApi 写缓存失败 api=%d err=%v", api.Id, err) + } + } + //h. 脱敏:grant.field_mask 非空时命中列名的值替换为 **** + MaskRows(rows, grant.FieldMask) + out, err := json.Marshal(rows) + if err != nil { + return nil, fmt.Errorf("%w: 结果集序列化失败: %v", ErrSqlExec, err) + } + //i. 返回 rows_json/cost_ms + return &datasvc.ExecuteReply{ + RowsJson: string(out), + CostMs: time.Since(start).Milliseconds(), + }, nil + } + + //写语句:返回受影响行数(不缓存) + affected, err := ExecWrite(l.ctx, db, sqlText, args) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrSqlExec, err) + } + return &datasvc.ExecuteReply{ + Affected: affected, + CostMs: time.Since(start).Milliseconds(), + }, nil } diff --git a/datasvc/rpc/internal/logic/executor.go b/datasvc/rpc/internal/logic/executor.go new file mode 100644 index 0000000..398d28d --- /dev/null +++ b/datasvc/rpc/internal/logic/executor.go @@ -0,0 +1,273 @@ +package logic + +import ( + "context" + "crypto/sha256" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "regexp" + "strings" +) + +// 参数类型取值(params_json 中 type 仅支持这三种) +const ( + TypeString = "string" + TypeNumber = "number" + TypeBool = "bool" +) + +// maskValue 脱敏后的替换值 +const maskValue = "****" + +// ParamDef API 参数定义(params_json 数组元素) +type ParamDef struct { + Name string `json:"name"` //参数名,对应 SQL 模板中的 :name 占位符 + Type string `json:"type"` //string/number/bool + Required bool `json:"required"` //是否必填 + Default interface{} `json:"default"` //缺省值(非必填且未传时补全) +} + +// paramNameRe SQL 模板中的占位符形如 :name(字母/下划线开头) +var paramNameRe = regexp.MustCompile(`:[A-Za-z_][A-Za-z0-9_]*`) + +// ParseParamDefs 解析并校验 params_json 参数定义(空串视为无参数) +func ParseParamDefs(paramsJson string) ([]ParamDef, error) { + paramsJson = strings.TrimSpace(paramsJson) + if paramsJson == "" { + return nil, nil + } + var defs []ParamDef + if err := json.Unmarshal([]byte(paramsJson), &defs); err != nil { + return nil, fmt.Errorf("params_json 不是合法的参数定义数组: %v", err) + } + seen := make(map[string]struct{}, len(defs)) + for i, d := range defs { + if strings.TrimSpace(d.Name) == "" { + return nil, fmt.Errorf("第 %d 个参数定义缺少 name", i+1) + } + switch d.Type { + case TypeString, TypeNumber, TypeBool: + default: + return nil, fmt.Errorf("参数 %s 的类型 %q 非法,仅支持 string/number/bool", d.Name, d.Type) + } + if _, ok := seen[d.Name]; ok { + return nil, fmt.Errorf("参数 %s 重复定义", d.Name) + } + seen[d.Name] = struct{}{} + } + return defs, nil +} + +// coerceValue 按参数定义类型收敛 JSON 值(业务参数与 default 共用),类型不符即拒绝 +func coerceValue(typ string, val interface{}) (interface{}, error) { + switch typ { + case TypeString: + s, ok := val.(string) + if !ok { + return nil, fmt.Errorf("期望 string 类型") + } + return s, nil + case TypeNumber: + n, ok := val.(float64) // encoding/json 的数字统一解析为 float64 + if !ok { + return nil, fmt.Errorf("期望 number 类型") + } + return n, nil + case TypeBool: + b, ok := val.(bool) + if !ok { + return nil, fmt.Errorf("期望 bool 类型") + } + return b, nil + default: + return nil, fmt.Errorf("不支持的参数类型 %q", typ) + } +} + +// ValidateParams 业务参数校验(纯函数): +// 1) 业务参数必须是 JSON 对象;2) 缺 required 参数报错; +// 3) 白名单之外的“多余参数”拒绝;4) 非必填缺省时补 default。 +// 返回 name -> 收敛后的参数值。 +func ValidateParams(defs []ParamDef, paramsJson string) (map[string]interface{}, error) { + provided := map[string]interface{}{} + paramsJson = strings.TrimSpace(paramsJson) + if paramsJson != "" { + if err := json.Unmarshal([]byte(paramsJson), &provided); err != nil { + return nil, fmt.Errorf("业务参数不是合法的 JSON 对象: %v", err) + } + } + //白名单索引 + defIdx := make(map[string]ParamDef, len(defs)) + for _, d := range defs { + defIdx[d.Name] = d + } + //多余参数拒绝(防注入、防误用) + for name := range provided { + if _, ok := defIdx[name]; !ok { + return nil, fmt.Errorf("多余参数: %s", name) + } + } + values := make(map[string]interface{}, len(defs)) + for _, d := range defs { + if raw, ok := provided[d.Name]; ok { + v, err := coerceValue(d.Type, raw) + if err != nil { + return nil, fmt.Errorf("参数 %s 类型错误: %v", d.Name, err) + } + values[d.Name] = v + continue + } + //缺省:必填报错;非必填补 default(default 为 null/未配置则不传入 SQL) + if d.Required { + return nil, fmt.Errorf("缺少必填参数: %s", d.Name) + } + if d.Default != nil { + v, err := coerceValue(d.Type, d.Default) + if err != nil { + return nil, fmt.Errorf("参数 %s 的 default 类型错误: %v", d.Name, err) + } + values[d.Name] = v + } + } + return values, nil +} + +// BuildSql 将 sql_text 中的 :name 占位符按出现顺序替换为 ? 并组装参数数组(纯函数): +// 仅允许替换参数白名单内的占位符,白名单之外的占位符直接拒绝(防注入)。 +// 注意:字符串字面量中的 :name 同样会被替换,属 :name 占位符方案的已知限制。 +func BuildSql(sqlText string, defs []ParamDef, values map[string]interface{}) (string, []interface{}, error) { + white := make(map[string]struct{}, len(defs)) + for _, d := range defs { + white[d.Name] = struct{}{} + } + //先校验全部占位符都在白名单内 + for _, m := range paramNameRe.FindAllString(sqlText, -1) { + if _, ok := white[m[1:]]; !ok { + return "", nil, fmt.Errorf("SQL 中存在未声明的占位符 %s,已拒绝执行", m) + } + } + //按出现顺序替换为 ? 并组装参数(未传值的可选参数以 NULL 传入) + args := make([]interface{}, 0, len(defs)) + built := paramNameRe.ReplaceAllStringFunc(sqlText, func(m string) string { + args = append(args, values[m[1:]]) + return "?" + }) + return built, args, nil +} + +// IsSelectStmt 判断语句是否以 SELECT 开头(剥掉前导括号后忽略大小写); +// 写语句(INSERT/UPDATE/DELETE/DROP 等)返回 false,执行结果不缓存 +func IsSelectStmt(sqlText string) bool { + s := strings.TrimSpace(sqlText) + s = strings.TrimSpace(strings.TrimPrefix(s, "(")) //支持 (select ...) 包装 + return len(s) >= 6 && strings.EqualFold(s[:6], "select") +} + +// CacheKey 结果缓存 key:dsvc#api#{api_id}#{参数摘要}, +// 参数摘要对收敛后的参数做 sha256(map 序列化按键排序,结果稳定) +func CacheKey(apiId int64, values map[string]interface{}) string { + raw, err := json.Marshal(values) + if err != nil { + raw = []byte(fmt.Sprintf("%v", values)) + } + sum := sha256.Sum256(raw) + return fmt.Sprintf("dsvc#api#%d#%s", apiId, hex.EncodeToString(sum[:])[:16]) +} + +// MaskRows 按授权 field_mask 脱敏(纯函数):结果集列名命中则值替换为 ****(列名忽略大小写) +func MaskRows(rows []map[string]interface{}, fieldMask string) { + fieldMask = strings.TrimSpace(fieldMask) + if fieldMask == "" || len(rows) == 0 { + return + } + mask := make(map[string]struct{}) + for _, f := range strings.Split(fieldMask, ",") { + f = strings.ToLower(strings.TrimSpace(f)) + if f != "" { + mask[f] = struct{}{} + } + } + if len(mask) == 0 { + return + } + for _, row := range rows { + for col := range row { + if _, ok := mask[strings.ToLower(col)]; ok { + row[col] = maskValue + } + } + } +} + +// normalizeWhitelist 规范化 IP 白名单:去首尾空白、丢弃空白项后逗号拼接 +func normalizeWhitelist(whitelist string) string { + items := strings.Split(whitelist, ",") + out := make([]string, 0, len(items)) + for _, item := range items { + if item = strings.TrimSpace(item); item != "" { + out = append(out, item) + } + } + return strings.Join(out, ",") +} + +// decodeRows 反序列化缓存中的结果集 JSON +func decodeRows(s string) ([]map[string]interface{}, error) { + var rows []map[string]interface{} + if err := json.Unmarshal([]byte(s), &rows); err != nil { + return nil, err + } + return rows, nil +} + +// QueryRowsAsMaps 在目标库执行 SELECT,把结果集转为通用行集; +// 驱动返回的 []byte(varchar/text/decimal)统一转 string,便于 JSON 输出 +func QueryRowsAsMaps(ctx context.Context, db *sql.DB, query string, args []interface{}) ([]map[string]interface{}, error) { + rs, err := db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rs.Close() + cols, err := rs.Columns() + if err != nil { + return nil, err + } + rows := make([]map[string]interface{}, 0, 16) + for rs.Next() { + container := make([]interface{}, len(cols)) + ptrs := make([]interface{}, len(cols)) + for i := range container { + ptrs[i] = &container[i] + } + if err := rs.Scan(ptrs...); err != nil { + return nil, err + } + row := make(map[string]interface{}, len(cols)) + for i, c := range cols { + row[c] = normalizeDbValue(container[i]) + } + rows = append(rows, row) + } + return rows, rs.Err() +} + +// normalizeDbValue 驱动返回值归一化(仅 []byte → string,其余保持原样) +func normalizeDbValue(v interface{}) interface{} { + switch t := v.(type) { + case []byte: + return string(t) + default: + return v + } +} + +// ExecWrite 在目标库执行写语句,返回受影响行数 +func ExecWrite(ctx context.Context, db *sql.DB, query string, args []interface{}) (int64, error) { + ret, err := db.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + return ret.RowsAffected() +} diff --git a/datasvc/rpc/internal/logic/executor_test.go b/datasvc/rpc/internal/logic/executor_test.go new file mode 100644 index 0000000..c0a547b --- /dev/null +++ b/datasvc/rpc/internal/logic/executor_test.go @@ -0,0 +1,256 @@ +package logic + +import ( + "errors" + "fmt" + "reflect" + "strings" + "testing" +) + +// 参数定义解析:正常 / 空 / 非法类型 / 重复参数 / 缺 name / 非数组 +func TestParseParamDefs(t *testing.T) { + // 空串视为无参数 + if defs, err := ParseParamDefs(""); err != nil || defs != nil { + t.Fatalf("空 params_json 应返回 nil, got %v %v", defs, err) + } + defs, err := ParseParamDefs(`[{"name":"id","type":"number","required":true},{"name":"name","type":"string","default":"tom"}]`) + if err != nil || len(defs) != 2 { + t.Fatalf("正常定义解析失败: %v %v", defs, err) + } + // 非法类型 + if _, err := ParseParamDefs(`[{"name":"a","type":"int"}]`); err == nil { + t.Fatal("非法类型应报错") + } + // 重复参数 + if _, err := ParseParamDefs(`[{"name":"a","type":"string"},{"name":"a","type":"string"}]`); err == nil { + t.Fatal("重复参数应报错") + } + // 缺 name + if _, err := ParseParamDefs(`[{"type":"string"}]`); err == nil { + t.Fatal("缺 name 应报错") + } + // 非法 JSON + if _, err := ParseParamDefs(`not-json`); err == nil { + t.Fatal("非法 JSON 应报错") + } +} + +// 业务参数校验:缺 required / 多余参数 / default 补全 / 类型收敛 +func TestValidateParams(t *testing.T) { + defs, err := ParseParamDefs(`[ + {"name":"id","type":"number","required":true}, + {"name":"name","type":"string","required":false,"default":"tom"}, + {"name":"vip","type":"bool","required":false} + ]`) + if err != nil { + t.Fatalf("定义解析失败: %v", err) + } + // 缺 required 报错 + if _, err := ValidateParams(defs, `{}`); err == nil || !strings.Contains(err.Error(), "缺少必填参数") { + t.Fatalf("缺 required 应报错, got: %v", err) + } + // 多余参数拒绝 + if _, err := ValidateParams(defs, `{"id":1,"hack":"1=1"}`); err == nil || !strings.Contains(err.Error(), "多余参数") { + t.Fatalf("多余参数应拒绝, got: %v", err) + } + // 非必填缺省补 default + vals, err := ValidateParams(defs, `{"id":1}`) + if err != nil { + t.Fatalf("合法参数不应报错: %v", err) + } + if vals["name"] != "tom" { + t.Fatalf("缺省应补 default \"tom\", got: %v", vals["name"]) + } + if _, ok := vals["vip"]; ok { + t.Fatal("无 default 的非必填缺省时不应出现在结果中") + } + // 类型收敛:number→float64、bool→bool + vals, err = ValidateParams(defs, `{"id":2,"vip":true,"name":"jerry"}`) + if err != nil { + t.Fatalf("合法参数不应报错: %v", err) + } + if n, ok := vals["id"].(float64); !ok || n != 2 { + t.Fatalf("number 参数应为 float64 2, got: %T %v", vals["id"], vals["id"]) + } + if vals["vip"] != true { + t.Fatalf("bool 参数应为 true, got: %v", vals["vip"]) + } + // 类型不符:number 传字符串 + if _, err := ValidateParams(defs, `{"id":"x"}`); err == nil || !strings.Contains(err.Error(), "类型错误") { + t.Fatalf("number 传字符串应报类型错误, got: %v", err) + } + // 类型不符:bool 传数字 + if _, err := ValidateParams(defs, `{"id":1,"vip":1}`); err == nil { + t.Fatalf("bool 传数字应报类型错误, got: %v", err) + } + // 业务参数不是对象 + if _, err := ValidateParams(defs, `[1,2]`); err == nil { + t.Fatalf("数组业务参数应报错, got: %v", err) + } + // 业务参数非法 JSON + if _, err := ValidateParams(defs, `{id:}`); err == nil { + t.Fatalf("非法 JSON 业务参数应报错, got: %v", err) + } + // 空参数且定义里 required=false:只补 default + vals, err = ValidateParams(nil, "") + if err != nil || len(vals) != 0 { + t.Fatalf("空定义+空参数应返回空 map, got: %v %v", vals, err) + } + // default 类型错误也要暴露 + badDefs, _ := ParseParamDefs(`[{"name":"n","type":"number","default":"oops"}]`) + if _, err := ValidateParams(badDefs, ``); err == nil || !strings.Contains(err.Error(), "default 类型错误") { + t.Fatalf("default 类型错误应报错, got: %v", err) + } +} + +// :name 占位符 → ? 替换与参数数组组装 +func TestBuildSql(t *testing.T) { + defs := []ParamDef{ + {Name: "id", Type: TypeNumber}, + {Name: "name", Type: TypeString}, + } + values := map[string]interface{}{"id": 1.0, "name": "tom"} + // 按出现顺序替换并组参数(含重复占位符) + got, args, err := BuildSql( + "select * from users where id = :id and name = :name and id > :id", defs, values) + if err != nil { + t.Fatalf("BuildSql 失败: %v", err) + } + want := "select * from users where id = ? and name = ? and id > ?" + if got != want { + t.Fatalf("替换结果不符:\nwant %q\ngot %q", want, got) + } + wantArgs := []interface{}{1.0, "tom", 1.0} + if !reflect.DeepEqual(args, wantArgs) { + t.Fatalf("参数数组不符: want %v got %v", wantArgs, args) + } + // 白名单之外的占位符拒绝(防注入) + if _, _, err := BuildSql("select * from t where a = :hack", defs, values); err == nil { + t.Fatal("未声明的占位符应拒绝执行") + } + // 未传值的可选占位符以 NULL 占位 + optDefs := []ParamDef{{Name: "opt", Type: TypeString}} + sql2, args2, err := BuildSql("select * from t where a = :opt", optDefs, map[string]interface{}{}) + if err != nil || sql2 != "select * from t where a = ?" || len(args2) != 1 || args2[0] != nil { + t.Fatalf("可选占位符应替换为 ? 且参数为 nil: %q %v %v", sql2, args2, err) + } +} + +// SELECT 判定:大小写 / 前导空白与括号 / 写语句 +func TestIsSelectStmt(t *testing.T) { + cases := []struct { + sql string + want bool + }{ + {"select 1", true}, + {" SELECT * FROM t", true}, + {"(select id from t limit 1)", true}, + {"insert into t values (1)", false}, + {"UPDATE t set a=1", false}, + {"delete from t", false}, + {"DROP table t", false}, + {"", false}, + {" ", false}, + } + for _, c := range cases { + if got := IsSelectStmt(c.sql); got != c.want { + t.Fatalf("IsSelectStmt(%q) = %v, want %v", c.sql, got, c.want) + } + } +} + +// field_mask 脱敏:命中列名(忽略大小写)值替换为 ****,其余不变 +func TestMaskRows(t *testing.T) { + rows := []map[string]interface{}{ + {"id": 1, "phone": "13800000001", "name": "a"}, + {"id": 2, "phone": "13800000002", "name": "b"}, + } + // 命中脱敏 + MaskRows(rows, "phone") + if rows[0]["phone"] != maskValue || rows[1]["phone"] != maskValue { + t.Fatalf("phone 列应被脱敏: %v", rows) + } + if rows[0]["id"] != 1 || rows[0]["name"] != "a" { + t.Fatal("未命中列不应被改动") + } + // 大小写不敏感命中(结果列 Phone vs mask phone) + rows2 := []map[string]interface{}{{"Phone": "13800000001", "ID": 3}} + MaskRows(rows2, "phone,id") + if rows2[0]["Phone"] != maskValue || rows2[0]["ID"] != maskValue { + t.Fatalf("大小写命中失败: %v", rows2) + } + // 空 mask 不改动 + rows3 := []map[string]interface{}{{"phone": "13800000001"}} + MaskRows(rows3, " ") + if rows3[0]["phone"] != "13800000001" { + t.Fatal("空 mask 不应改动结果") + } + // 空 rows 安全 + MaskRows(nil, "phone") +} + +// 缓存 key:同参数同 key(与传入顺序无关)、不同 api/参数不同 key +func TestCacheKey(t *testing.T) { + v1 := map[string]interface{}{"id": 1.0, "name": "tom"} + v2 := map[string]interface{}{"name": "tom", "id": 1.0} //仅顺序不同 + if CacheKey(7, v1) != CacheKey(7, v2) { + t.Fatal("相同参数(不同顺序)应生成相同 key") + } + if CacheKey(7, v1) == CacheKey(8, v1) { + t.Fatal("不同 api_id 应生成不同 key") + } + if CacheKey(7, v1) == CacheKey(7, map[string]interface{}{"id": 2.0, "name": "tom"}) { + t.Fatal("不同参数应生成不同 key") + } + if !strings.HasPrefix(CacheKey(7, v1), "dsvc#api#7#") { + t.Fatalf("key 应符合 dsvc#api#{api_id}# 前缀: %q", CacheKey(7, v1)) + } +} + +// IP 白名单规范化:去空白、丢空项 +func TestNormalizeWhitelist(t *testing.T) { + if got := normalizeWhitelist(" 10.0.0.1 , , 192.168.0.1 "); got != "10.0.0.1,192.168.0.1" { + t.Fatalf("规范化结果不符: %q", got) + } + if got := normalizeWhitelist(""); got != "" { + t.Fatalf("空白名单应为空串: %q", got) + } +} + +// 缓存结果集反序列化 +func TestDecodeRows(t *testing.T) { + rows, err := decodeRows(`[{"a":1},{"a":"x"}]`) + if err != nil || len(rows) != 2 { + t.Fatalf("decodeRows 失败: %v %v", rows, err) + } + if _, err := decodeRows(`not-json`); err == nil { + t.Fatal("非法 JSON 应报错") + } + if _, err := decodeRows(`{"a":1}`); err != nil { + // 对象形式非数组:json 可解到 []map 会失败 + t.Log("对象解到数组返回错误属预期") + } +} + +// 归纳校验:ExecuteApi 业务错误可用 %w 包装后保持 errors.Is 语义 +func TestSentinelErrors(t *testing.T) { + err := fmt.Errorf("%w: 授权已过期", ErrNotGranted) + if !errors.Is(err, ErrNotGranted) { + t.Fatal("包装后的哨兵错误应保持 errors.Is 语义") + } + err = fmt.Errorf("%w: ts 与服务器时间偏差超过 300 秒", ErrSignInvalid) + if !errors.Is(err, ErrSignInvalid) { + t.Fatal("包装后的 ErrSignInvalid 应保持 errors.Is 语义") + } +} + +// 目标库返回值归一化:[]byte → string(其余保持) +func TestNormalizeDbValue(t *testing.T) { + if got := normalizeDbValue([]byte("abc")); got != "abc" { + t.Fatalf("[]byte 应转 string, got %T %v", got, got) + } + if got := normalizeDbValue(1.5); got != 1.5 { + t.Fatalf("float64 应保持原样, got %v", got) + } +} diff --git a/datasvc/rpc/internal/logic/getApiLogic.go b/datasvc/rpc/internal/logic/getApiLogic.go index 01d5116..81f8599 100644 --- a/datasvc/rpc/internal/logic/getApiLogic.go +++ b/datasvc/rpc/internal/logic/getApiLogic.go @@ -2,7 +2,10 @@ package logic import ( "context" + "errors" + "strings" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -23,9 +26,14 @@ func NewGetApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApiLogi } } -// API详情 +// API详情(按 beid+path 查询) func (l *GetApiLogic) GetApi(in *datasvc.PathReq) (*datasvc.ApiReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.ApiReply{}, nil + api, err := l.svcCtx.ApiModel.FindByPath(in.Beid, strings.TrimSpace(in.Path)) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrApiNotFound + } + return nil, err + } + return &datasvc.ApiReply{Api: apiToPb(api)}, nil } diff --git a/datasvc/rpc/internal/logic/grantApiLogic.go b/datasvc/rpc/internal/logic/grantApiLogic.go index f4fe421..41c10b9 100644 --- a/datasvc/rpc/internal/logic/grantApiLogic.go +++ b/datasvc/rpc/internal/logic/grantApiLogic.go @@ -2,7 +2,12 @@ package logic import ( "context" + "database/sql" + "errors" + "strings" + "time" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -23,9 +28,54 @@ func NewGrantApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GrantApi } } -// 授权 API 给应用 +// 授权 API 给应用(同 api+app 重复授权幂等:刷新过期时间与脱敏字段) func (l *GrantApiLogic) GrantApi(in *datasvc.GrantApiReq) (*datasvc.OkReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.OkReply{}, nil + if in.ApiId <= 0 || in.AppId <= 0 { + return nil, ErrParamInvalid + } + //API 必须存在 + api, err := l.svcCtx.ApiModel.FindOne(in.ApiId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrApiNotFound + } + return nil, err + } + //多租户校验(beid 未传时回落 API 所属租户) + beid := in.Beid + if beid == 0 { + beid = api.Beid + } + if api.Beid != beid { + return nil, ErrApiNotFound + } + //应用必须存在且同租户 + app, err := l.svcCtx.AppModel.FindOne(in.AppId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrAppNotFound + } + return nil, err + } + if app.Beid != beid { + return nil, ErrAppNotFound + } + //expire_at:0 表示永久(NULL) + var expireAt sql.NullTime + if in.ExpireAt > 0 { + expireAt = sql.NullTime{Time: time.Unix(in.ExpireAt, 0), Valid: true} + } + grant := &model.ApiGrant{ + ApiId: in.ApiId, + AppId: in.AppId, + ExpireAt: expireAt, + FieldMask: strings.TrimSpace(in.FieldMask), + Beid: beid, + } + if err := l.svcCtx.GrantModel.Insert(grant); err != nil { + return nil, err + } + l.Infof("授权API成功 api=%d app=%d beid=%d expire_at=%v field_mask=%s", + grant.ApiId, grant.AppId, grant.Beid, grant.ExpireAt, grant.FieldMask) + return &datasvc.OkReply{Ok: true}, nil } diff --git a/datasvc/rpc/internal/logic/listApisLogic.go b/datasvc/rpc/internal/logic/listApisLogic.go index 1351eff..cb5a7fc 100644 --- a/datasvc/rpc/internal/logic/listApisLogic.go +++ b/datasvc/rpc/internal/logic/listApisLogic.go @@ -2,6 +2,7 @@ package logic import ( "context" + "strings" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -23,9 +24,15 @@ func NewListApisLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListApis } } -// API列表 +// API列表(beid+可选 status 分页,分页参数非法时在 model 层兜底) func (l *ListApisLogic) ListApis(in *datasvc.ApiListReq) (*datasvc.ApiListReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.ApiListReply{}, nil + list, total, err := l.svcCtx.ApiModel.List(in.Beid, strings.TrimSpace(in.Status), in.Page, in.Size) + if err != nil { + return nil, err + } + pbList := make([]*datasvc.Api, 0, len(list)) + for _, api := range list { + pbList = append(pbList, apiToPb(api)) + } + return &datasvc.ApiListReply{List: pbList, Total: total}, nil } diff --git a/datasvc/rpc/internal/logic/offlineApiLogic.go b/datasvc/rpc/internal/logic/offlineApiLogic.go index b85c4fa..4b66bda 100644 --- a/datasvc/rpc/internal/logic/offlineApiLogic.go +++ b/datasvc/rpc/internal/logic/offlineApiLogic.go @@ -2,7 +2,10 @@ package logic import ( "context" + "errors" + "fmt" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -23,8 +26,25 @@ func NewOfflineApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Offlin } } +// 下线API(offline 后 ExecuteApi 以 ErrNotPublished 拒绝执行) func (l *OfflineApiLogic) OfflineApi(in *datasvc.IdReq) (*datasvc.OkReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.OkReply{}, nil + if in.Id <= 0 { + return nil, fmt.Errorf("%w: id 非法", ErrParamInvalid) + } + api, err := l.svcCtx.ApiModel.FindOne(in.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrApiNotFound + } + return nil, err + } + //多租户校验 + if in.Beid != 0 && api.Beid != in.Beid { + return nil, ErrApiNotFound + } + if err := l.svcCtx.ApiModel.UpdateStatus(api.Id, "offline"); err != nil { + return nil, err + } + l.Infof("下线API成功 id=%d beid=%d path=%s", api.Id, api.Beid, api.Path) + return &datasvc.OkReply{Ok: true}, nil } diff --git a/datasvc/rpc/internal/logic/publishApiLogic.go b/datasvc/rpc/internal/logic/publishApiLogic.go index 5f09c72..522d1e6 100644 --- a/datasvc/rpc/internal/logic/publishApiLogic.go +++ b/datasvc/rpc/internal/logic/publishApiLogic.go @@ -2,7 +2,11 @@ package logic import ( "context" + "errors" + "fmt" + "strings" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -23,9 +27,33 @@ func NewPublishApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Publis } } -// 发布/下线 +// 发布API(前置校验:sql_text 非空、params_json 合法) func (l *PublishApiLogic) PublishApi(in *datasvc.IdReq) (*datasvc.OkReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.OkReply{}, nil + if in.Id <= 0 { + return nil, fmt.Errorf("%w: id 非法", ErrParamInvalid) + } + api, err := l.svcCtx.ApiModel.FindOne(in.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrApiNotFound + } + return nil, err + } + //多租户校验 + if in.Beid != 0 && api.Beid != in.Beid { + return nil, ErrApiNotFound + } + //发布前置校验:SQL 模板非空 + if strings.TrimSpace(api.SqlText) == "" { + return nil, fmt.Errorf("%w: api=%d 发布前需配置 SQL 模板", ErrSqlEmpty, api.Id) + } + //发布前置校验:参数定义必须合法,避免上线后执行链才发现 + if _, err := ParseParamDefs(api.ParamsJson); err != nil { + return nil, fmt.Errorf("%w: %v", ErrParamInvalid, err) + } + if err := l.svcCtx.ApiModel.UpdateStatus(api.Id, "published"); err != nil { + return nil, err + } + l.Infof("发布API成功 id=%d beid=%d path=%s", api.Id, api.Beid, api.Path) + return &datasvc.OkReply{Ok: true}, nil } diff --git a/datasvc/rpc/internal/logic/registerAppLogic.go b/datasvc/rpc/internal/logic/registerAppLogic.go index cfb46ef..47e81e1 100644 --- a/datasvc/rpc/internal/logic/registerAppLogic.go +++ b/datasvc/rpc/internal/logic/registerAppLogic.go @@ -2,13 +2,19 @@ package logic import ( "context" + "fmt" + "strings" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" "github.com/zeromicro/go-zero/core/logx" ) +// defaultAppQps 应用未显式配置限流时的默认 QPS(对齐 dc_api_app 表默认值) +const defaultAppQps int64 = 10 + type RegisterAppLogic struct { ctx context.Context svcCtx *svc.ServiceContext @@ -23,9 +29,47 @@ func NewRegisterAppLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Regis } } -// 注册应用(返回 app_key/app_secret) +// 注册应用:生成 app_key(app_+随机 hex16)、app_secret(随机 hex32), +// secret 经 AES-GCM 加密入库,明文仅本次响应返回一次 func (l *RegisterAppLogic) RegisterApp(in *datasvc.RegisterAppReq) (*datasvc.AppReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.AppReply{}, nil + name := strings.TrimSpace(in.Name) + if name == "" { + return nil, fmt.Errorf("%w: name 不能为空", ErrParamInvalid) + } + if in.RateLimitQps < 0 { + return nil, fmt.Errorf("%w: rate_limit_qps 不能为负数", ErrParamInvalid) + } + qps := in.RateLimitQps + if qps == 0 { + qps = defaultAppQps //未配置时取默认限流 + } + appKey, err := randomHex(8) //app_ + 16 位 hex + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrSecretCrypto, err) + } + appKey = "app_" + appKey + secret, err := randomHex(16) //32 位 hex + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrSecretCrypto, err) + } + //AES-GCM 加密入库,明文不落库 + enc, err := encryptAppSecret(l.svcCtx, secret) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrSecretCrypto, err) + } + data := &model.ApiApp{ + AppKey: appKey, + AppSecretEnc: enc, + Name: name, + RateLimitQps: qps, + IpWhitelist: normalizeWhitelist(in.IpWhitelist), + Beid: in.Beid, + } + id, err := l.svcCtx.AppModel.Insert(data) + if err != nil { + return nil, err + } + data.Id = id + l.Infof("注册应用成功 id=%d beid=%d app_key=%s", id, data.Beid, data.AppKey) + return &datasvc.AppReply{App: appToPb(data), AppSecret: secret}, nil } diff --git a/datasvc/rpc/internal/logic/updateApiLogic.go b/datasvc/rpc/internal/logic/updateApiLogic.go index d7f0f19..3c07846 100644 --- a/datasvc/rpc/internal/logic/updateApiLogic.go +++ b/datasvc/rpc/internal/logic/updateApiLogic.go @@ -2,7 +2,11 @@ package logic import ( "context" + "errors" + "fmt" + "strings" + "datacenter/datasvc/model" "datacenter/datasvc/rpc/datasvc" "datacenter/datasvc/rpc/internal/svc" @@ -23,9 +27,49 @@ func NewUpdateApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateA } } -// 更新API +// 更新API(空字符串字段不更新,保持原值;cache_ttl 以传入值为准,0 即关闭缓存) func (l *UpdateApiLogic) UpdateApi(in *datasvc.UpdateApiReq) (*datasvc.OkReply, error) { - // todo: add your logic here and delete this line - - return &datasvc.OkReply{}, nil + if in.Id <= 0 { + return nil, fmt.Errorf("%w: id 非法", ErrParamInvalid) + } + if in.CacheTtl < 0 { + return nil, fmt.Errorf("%w: cache_ttl 不能为负数", ErrParamInvalid) + } + api, err := l.svcCtx.ApiModel.FindOne(in.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrApiNotFound + } + return nil, err + } + //多租户校验 + if in.Beid != 0 && api.Beid != in.Beid { + return nil, ErrApiNotFound + } + //params_json 合法性校验 + if strings.TrimSpace(in.ParamsJson) != "" { + if _, err := ParseParamDefs(in.ParamsJson); err != nil { + return nil, fmt.Errorf("%w: %v", ErrParamInvalid, err) + } + api.ParamsJson = strings.TrimSpace(in.ParamsJson) + } + //sql_text:空串保持原值;wizard 模式仍强制 SELECT 开头 + if strings.TrimSpace(in.SqlText) != "" { + if api.Mode == "wizard" && !IsSelectStmt(in.SqlText) { + return nil, fmt.Errorf("%w: wizard 模式仅允许 SELECT 查询模板", ErrParamInvalid) + } + api.SqlText = in.SqlText + } + if name := strings.TrimSpace(in.Name); name != "" { + api.Name = name + } + if sample := strings.TrimSpace(in.RespSampleJson); sample != "" { + api.RespSampleJson = in.RespSampleJson + } + api.CacheTtl = in.CacheTtl + if err := l.svcCtx.ApiModel.Update(api); err != nil { + return nil, err + } + l.Infof("更新API成功 id=%d beid=%d", api.Id, api.Beid) + return &datasvc.OkReply{Ok: true}, nil } diff --git a/datasvc/rpc/internal/svc/serviceContext.go b/datasvc/rpc/internal/svc/serviceContext.go index 2d44a40..88674bf 100644 --- a/datasvc/rpc/internal/svc/serviceContext.go +++ b/datasvc/rpc/internal/svc/serviceContext.go @@ -1,13 +1,89 @@ package svc -import "datacenter/datasvc/rpc/internal/config" +import ( + "errors" + "sync" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/datasvc/model" + "datacenter/datasvc/rpc/internal/config" + + "github.com/zeromicro/go-zero/core/discov" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/syncx" + "github.com/zeromicro/go-zero/zrpc" + "golang.org/x/time/rate" +) + +// ErrCacheNotFound 结果缓存未命中(cache.Cache.Get 未命中时返回) +var ErrCacheNotFound = errors.New("cache not found") + +// dataSourceEtcdKey datasource rpc 的 etcd 服务 key(PHASE1 契约固定) +const dataSourceEtcdKey = "datasource.rpc" type ServiceContext struct { - Config config.Config + Config config.Config + ApiModel model.ApiModel + AppModel model.AppModel + GrantModel model.GrantModel + ResultCache cache.Cache //API 结果缓存(CacheRedis) + DataSourceRpc dataSourceClient.DataSource //数据源 rpc(ExecuteApi 获取目标库 DSN) + + limiterMu sync.Mutex + limiters map[int64]*rate.Limiter //应用限流器容器:key=app_id,进程内令牌桶 + + connMu sync.Mutex + targetCons map[string]sqlx.SqlConn //目标库连接池缓存:key=dsn,复用 *sqlx.SqlConn } func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) + //datasource rpc 客户端:config.go 不可改,参考网关 servicecontext 写法内联装配, + //etcd key 固定为 datasource.rpc;NonBlock 保证 datasource 未就绪时不阻塞本服务启动 + dsRpc := dataSourceClient.NewDataSource(zrpc.MustNewClient(zrpc.RpcClientConf{ + Etcd: discov.EtcdConf{ + Hosts: c.Etcd.Hosts, + Key: dataSourceEtcdKey, + }, + NonBlock: true, + })) return &ServiceContext{ - Config: c, + Config: c, + ApiModel: model.NewApiModel(conn), + AppModel: model.NewAppModel(conn), + GrantModel: model.NewGrantModel(conn), + ResultCache: cache.New(c.CacheRedis, syncx.NewSingleFlight(), cache.NewStat("dsvc"), ErrCacheNotFound), + DataSourceRpc: dsRpc, + limiters: make(map[int64]*rate.Limiter), + targetCons: make(map[string]sqlx.SqlConn), + } +} + +// Allow 应用级限流:每应用一个进程内令牌桶(map+锁保护), +// 限流配置变更时重建;qps<=0 视为不限流 +func (sc *ServiceContext) Allow(appId int64, qps int64) bool { + if qps <= 0 { + return true + } + sc.limiterMu.Lock() + lim, ok := sc.limiters[appId] + if !ok || int64(lim.Limit()) != qps { + lim = rate.NewLimiter(rate.Limit(qps), int(qps)) //桶容量=qps,允许 1 秒内小突发 + sc.limiters[appId] = lim + } + sc.limiterMu.Unlock() + return lim.Allow() +} + +// TargetConn 获取目标库连接(按 DSN 缓存 sqlx.SqlConn,复用底层连接池) +func (sc *ServiceContext) TargetConn(dsn string) (sqlx.SqlConn, error) { + sc.connMu.Lock() + defer sc.connMu.Unlock() + if conn, ok := sc.targetCons[dsn]; ok { + return conn, nil } + conn := sqlx.NewMysql(dsn) + sc.targetCons[dsn] = conn + return conn, nil } diff --git a/etc/datacenter-api.example.yaml b/etc/datacenter-api.example.yaml index da04b56..d73f535 100644 --- a/etc/datacenter-api.example.yaml +++ b/etc/datacenter-api.example.yaml @@ -34,4 +34,23 @@ QuestionsRpc: Etcd: Hosts: - 127.0.0.1:2379 - Key: questions.rpc \ No newline at end of file + Key: questions.rpc DataSourceRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: datasource.rpc +MetadataRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: metadata.rpc +DataSvcRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: datasvc.rpc +CatalogRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: catalog.rpc diff --git a/internal/config/config.go b/internal/config/config.go index cbf7702..daf4980 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,5 +19,11 @@ type Config struct { SearchRpc zrpc.RpcClientConf QuestionsRpc zrpc.RpcClientConf + //Phase 1 数据中台服务 + DataSourceRpc zrpc.RpcClientConf + MetadataRpc zrpc.RpcClientConf + DataSvcRpc zrpc.RpcClientConf + CatalogRpc zrpc.RpcClientConf + CacheRedis cache.ClusterConf } diff --git a/internal/handler/catalog/assetlisthandler.go b/internal/handler/catalog/assetlisthandler.go new file mode 100644 index 0000000..9c4e64e --- /dev/null +++ b/internal/handler/catalog/assetlisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package catalog + +import ( + "net/http" + + "datacenter/internal/logic/catalog" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 资产列表 +func AssetListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AssetListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := catalog.NewAssetListLogic(r.Context(), svcCtx) + resp, err := l.AssetList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/catalog/categorylisthandler.go b/internal/handler/catalog/categorylisthandler.go new file mode 100644 index 0000000..bc808b5 --- /dev/null +++ b/internal/handler/catalog/categorylisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package catalog + +import ( + "net/http" + + "datacenter/internal/logic/catalog" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 分类列表 +func CategoryListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CategoryListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := catalog.NewCategoryListLogic(r.Context(), svcCtx) + resp, err := l.CategoryList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/catalog/createcategoryhandler.go b/internal/handler/catalog/createcategoryhandler.go new file mode 100644 index 0000000..23ba37d --- /dev/null +++ b/internal/handler/catalog/createcategoryhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package catalog + +import ( + "net/http" + + "datacenter/internal/logic/catalog" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建资产分类 +func CreateCategoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateCategoryReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := catalog.NewCreateCategoryLogic(r.Context(), svcCtx) + resp, err := l.CreateCategory(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/catalog/tagassethandler.go b/internal/handler/catalog/tagassethandler.go new file mode 100644 index 0000000..6c52b63 --- /dev/null +++ b/internal/handler/catalog/tagassethandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package catalog + +import ( + "net/http" + + "datacenter/internal/logic/catalog" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 登记资产 +func TagAssetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.TagAssetReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := catalog.NewTagAssetLogic(r.Context(), svcCtx) + resp, err := l.TagAsset(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasource/createdatasourcehandler.go b/internal/handler/datasource/createdatasourcehandler.go new file mode 100644 index 0000000..2f15bed --- /dev/null +++ b/internal/handler/datasource/createdatasourcehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "net/http" + + "datacenter/internal/logic/datasource" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建数据源 +func CreateDataSourceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateDataSourceReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasource.NewCreateDataSourceLogic(r.Context(), svcCtx) + resp, err := l.CreateDataSource(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasource/datasourceinfohandler.go b/internal/handler/datasource/datasourceinfohandler.go new file mode 100644 index 0000000..f4df1bd --- /dev/null +++ b/internal/handler/datasource/datasourceinfohandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "net/http" + + "datacenter/internal/logic/datasource" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 数据源详情 +func DataSourceInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DataSourceIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasource.NewDataSourceInfoLogic(r.Context(), svcCtx) + resp, err := l.DataSourceInfo(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasource/datasourcelisthandler.go b/internal/handler/datasource/datasourcelisthandler.go new file mode 100644 index 0000000..7f57772 --- /dev/null +++ b/internal/handler/datasource/datasourcelisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "net/http" + + "datacenter/internal/logic/datasource" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 数据源列表 +func DataSourceListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DataSourceListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasource.NewDataSourceListLogic(r.Context(), svcCtx) + resp, err := l.DataSourceList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasource/datasourcetesthandler.go b/internal/handler/datasource/datasourcetesthandler.go new file mode 100644 index 0000000..a2a9356 --- /dev/null +++ b/internal/handler/datasource/datasourcetesthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "net/http" + + "datacenter/internal/logic/datasource" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 测试数据源连接 +func DataSourceTestHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DataSourceIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasource.NewDataSourceTestLogic(r.Context(), svcCtx) + resp, err := l.DataSourceTest(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasource/deletedatasourcehandler.go b/internal/handler/datasource/deletedatasourcehandler.go new file mode 100644 index 0000000..892b065 --- /dev/null +++ b/internal/handler/datasource/deletedatasourcehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "net/http" + + "datacenter/internal/logic/datasource" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除数据源 +func DeleteDataSourceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DataSourceIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasource.NewDeleteDataSourceLogic(r.Context(), svcCtx) + err := l.DeleteDataSource(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/datasource/updatedatasourcehandler.go b/internal/handler/datasource/updatedatasourcehandler.go new file mode 100644 index 0000000..231c5f6 --- /dev/null +++ b/internal/handler/datasource/updatedatasourcehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "net/http" + + "datacenter/internal/logic/datasource" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新数据源 +func UpdateDataSourceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateDataSourceReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasource.NewUpdateDataSourceLogic(r.Context(), svcCtx) + err := l.UpdateDataSource(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/datasvc/apiinfohandler.go b/internal/handler/datasvc/apiinfohandler.go new file mode 100644 index 0000000..5bcd6dd --- /dev/null +++ b/internal/handler/datasvc/apiinfohandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// API详情 +func ApiInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ApiPathReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewApiInfoLogic(r.Context(), svcCtx) + resp, err := l.ApiInfo(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasvc/apilisthandler.go b/internal/handler/datasvc/apilisthandler.go new file mode 100644 index 0000000..2f20473 --- /dev/null +++ b/internal/handler/datasvc/apilisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// API列表 +func ApiListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ApiListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewApiListLogic(r.Context(), svcCtx) + resp, err := l.ApiList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasvc/createapihandler.go b/internal/handler/datasvc/createapihandler.go new file mode 100644 index 0000000..9f12d76 --- /dev/null +++ b/internal/handler/datasvc/createapihandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建数据API +func CreateApiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateApiReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewCreateApiLogic(r.Context(), svcCtx) + resp, err := l.CreateApi(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasvc/deleteapihandler.go b/internal/handler/datasvc/deleteapihandler.go new file mode 100644 index 0000000..ec829e2 --- /dev/null +++ b/internal/handler/datasvc/deleteapihandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除数据API +func DeleteApiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DsIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewDeleteApiLogic(r.Context(), svcCtx) + err := l.DeleteApi(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/datasvc/executeapihandler.go b/internal/handler/datasvc/executeapihandler.go new file mode 100644 index 0000000..b127670 --- /dev/null +++ b/internal/handler/datasvc/executeapihandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 执行数据API(外部统一入口) +func ExecuteApiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ExecuteApiReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewExecuteApiLogic(r.Context(), svcCtx) + resp, err := l.ExecuteApi(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasvc/grantapihandler.go b/internal/handler/datasvc/grantapihandler.go new file mode 100644 index 0000000..9b6f301 --- /dev/null +++ b/internal/handler/datasvc/grantapihandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 授权API给应用 +func GrantApiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GrantApiReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewGrantApiLogic(r.Context(), svcCtx) + err := l.GrantApi(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/datasvc/offlineapihandler.go b/internal/handler/datasvc/offlineapihandler.go new file mode 100644 index 0000000..a9795d5 --- /dev/null +++ b/internal/handler/datasvc/offlineapihandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 下线API +func OfflineApiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DsIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewOfflineApiLogic(r.Context(), svcCtx) + err := l.OfflineApi(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/datasvc/publishapihandler.go b/internal/handler/datasvc/publishapihandler.go new file mode 100644 index 0000000..14fa69d --- /dev/null +++ b/internal/handler/datasvc/publishapihandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 发布API +func PublishApiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DsIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewPublishApiLogic(r.Context(), svcCtx) + err := l.PublishApi(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/datasvc/registerapphandler.go b/internal/handler/datasvc/registerapphandler.go new file mode 100644 index 0000000..3664d15 --- /dev/null +++ b/internal/handler/datasvc/registerapphandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 注册调用应用 +func RegisterAppHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RegisterAppReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewRegisterAppLogic(r.Context(), svcCtx) + resp, err := l.RegisterApp(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/datasvc/updateapihandler.go b/internal/handler/datasvc/updateapihandler.go new file mode 100644 index 0000000..653c363 --- /dev/null +++ b/internal/handler/datasvc/updateapihandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "net/http" + + "datacenter/internal/logic/datasvc" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新数据API +func UpdateApiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateApiReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := datasvc.NewUpdateApiLogic(r.Context(), svcCtx) + err := l.UpdateApi(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/metadata/addlineagehandler.go b/internal/handler/metadata/addlineagehandler.go new file mode 100644 index 0000000..7dd5af2 --- /dev/null +++ b/internal/handler/metadata/addlineagehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "net/http" + + "datacenter/internal/logic/metadata" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 登记血缘 +func AddLineageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AddLineageReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metadata.NewAddLineageLogic(r.Context(), svcCtx) + err := l.AddLineage(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/metadata/collectdatabasehandler.go b/internal/handler/metadata/collectdatabasehandler.go new file mode 100644 index 0000000..ec40209 --- /dev/null +++ b/internal/handler/metadata/collectdatabasehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "net/http" + + "datacenter/internal/logic/metadata" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 采集库表元数据 +func CollectDatabaseHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CollectDatabaseReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metadata.NewCollectDatabaseLogic(r.Context(), svcCtx) + resp, err := l.CollectDatabase(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metadata/createversionhandler.go b/internal/handler/metadata/createversionhandler.go new file mode 100644 index 0000000..d3a531a --- /dev/null +++ b/internal/handler/metadata/createversionhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "net/http" + + "datacenter/internal/logic/metadata" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 定版快照 +func CreateVersionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MetaEntityIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metadata.NewCreateVersionLogic(r.Context(), svcCtx) + resp, err := l.CreateVersion(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metadata/lineagehandler.go b/internal/handler/metadata/lineagehandler.go new file mode 100644 index 0000000..451b323 --- /dev/null +++ b/internal/handler/metadata/lineagehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "net/http" + + "datacenter/internal/logic/metadata" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 血缘图 +func LineageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.LineageReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metadata.NewLineageLogic(r.Context(), svcCtx) + resp, err := l.Lineage(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metadata/metaentityinfohandler.go b/internal/handler/metadata/metaentityinfohandler.go new file mode 100644 index 0000000..cc0a69c --- /dev/null +++ b/internal/handler/metadata/metaentityinfohandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "net/http" + + "datacenter/internal/logic/metadata" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 实体详情 +func MetaEntityInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MetaEntityIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metadata.NewMetaEntityInfoLogic(r.Context(), svcCtx) + resp, err := l.MetaEntityInfo(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metadata/metaentitylisthandler.go b/internal/handler/metadata/metaentitylisthandler.go new file mode 100644 index 0000000..6861111 --- /dev/null +++ b/internal/handler/metadata/metaentitylisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "net/http" + + "datacenter/internal/logic/metadata" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 实体列表 +func MetaEntityListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MetaEntityListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metadata.NewMetaEntityListLogic(r.Context(), svcCtx) + resp, err := l.MetaEntityList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metadata/versioncomparehandler.go b/internal/handler/metadata/versioncomparehandler.go new file mode 100644 index 0000000..48590b4 --- /dev/null +++ b/internal/handler/metadata/versioncomparehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "net/http" + + "datacenter/internal/logic/metadata" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 版本对比 +func VersionCompareHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CompareReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metadata.NewVersionCompareLogic(r.Context(), svcCtx) + resp, err := l.VersionCompare(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metadata/versionlisthandler.go b/internal/handler/metadata/versionlisthandler.go new file mode 100644 index 0000000..ea61958 --- /dev/null +++ b/internal/handler/metadata/versionlisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "net/http" + + "datacenter/internal/logic/metadata" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 版本列表 +func VersionListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MetaEntityIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metadata.NewVersionListLogic(r.Context(), svcCtx) + resp, err := l.VersionList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 79f89e1..9305347 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -1,10 +1,16 @@ // Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 + package handler import ( "net/http" + catalog "datacenter/internal/handler/catalog" common "datacenter/internal/handler/common" + datasource "datacenter/internal/handler/datasource" + datasvc "datacenter/internal/handler/datasvc" + metadata "datacenter/internal/handler/metadata" questions "datacenter/internal/handler/questions" search "datacenter/internal/handler/search" user "datacenter/internal/handler/user" @@ -14,15 +20,46 @@ import ( "github.com/zeromicro/go-zero/rest" ) -func RegisterHandlers(engine *rest.Server, serverCtx *svc.ServiceContext) { - engine.AddRoutes( +func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( []rest.Route{ { + // 资产列表 + Method: http.MethodGet, + Path: "/catalog/asset/list", + Handler: catalog.AssetListHandler(serverCtx), + }, + { + // 登记资产 + Method: http.MethodPost, + Path: "/catalog/asset/tag", + Handler: catalog.TagAssetHandler(serverCtx), + }, + { + // 创建资产分类 + Method: http.MethodPost, + Path: "/catalog/category/create", + Handler: catalog.CreateCategoryHandler(serverCtx), + }, + { + // 分类列表 + Method: http.MethodGet, + Path: "/catalog/category/list", + Handler: catalog.CategoryListHandler(serverCtx), + }, + }, + ) + + server.AddRoutes( + []rest.Route{ + { + // 获取站点的信息 Method: http.MethodGet, Path: "/common/appinfo", Handler: common.AppInfoHandler(serverCtx), }, { + // 获取站点的社交属性信息 Method: http.MethodPost, Path: "/common/snsinfo", Handler: common.SnsInfoHandler(serverCtx), @@ -35,9 +72,10 @@ func RegisterHandlers(engine *rest.Server, serverCtx *svc.ServiceContext) { }, ) - engine.AddRoutes( + server.AddRoutes( []rest.Route{ { + // 七牛上传凭证 Method: http.MethodPost, Path: "/common/qiuniu/token", Handler: common.QiuniuTokenHandler(serverCtx), @@ -46,103 +84,230 @@ func RegisterHandlers(engine *rest.Server, serverCtx *svc.ServiceContext) { rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) - engine.AddRoutes( + server.AddRoutes( []rest.Route{ { + // 创建数据源 Method: http.MethodPost, - Path: "/user/ping", - Handler: user.PingHandler(serverCtx), + Path: "/datasource/create", + Handler: datasource.CreateDataSourceHandler(serverCtx), }, { + // 删除数据源 Method: http.MethodPost, - Path: "/user/register", - Handler: user.RegisterHandler(serverCtx), + Path: "/datasource/delete", + Handler: datasource.DeleteDataSourceHandler(serverCtx), }, { - Method: http.MethodPost, - Path: "/user/login", - Handler: user.LoginHandler(serverCtx), + // 数据源详情 + Method: http.MethodGet, + Path: "/datasource/info", + Handler: datasource.DataSourceInfoHandler(serverCtx), }, { - Method: http.MethodPost, - Path: "/user/wx/login", - Handler: user.WxloginHandler(serverCtx), + // 数据源列表 + Method: http.MethodGet, + Path: "/datasource/list", + Handler: datasource.DataSourceListHandler(serverCtx), }, { + // 测试数据源连接 Method: http.MethodGet, - Path: "/user/wx/login", - Handler: user.Code2SessionHandler(serverCtx), + Path: "/datasource/test", + Handler: datasource.DataSourceTestHandler(serverCtx), + }, + { + // 更新数据源 + Method: http.MethodPost, + Path: "/datasource/update", + Handler: datasource.UpdateDataSourceHandler(serverCtx), }, }, ) - engine.AddRoutes( - rest.WithMiddlewares( - []rest.Middleware{serverCtx.Usercheck}, - []rest.Route{ - { - Method: http.MethodGet, - Path: "/user/dc/info", - Handler: user.UserInfoHandler(serverCtx), - }, - }..., - ), - rest.WithJwt(serverCtx.Config.Auth.AccessSecret), + server.AddRoutes( + []rest.Route{ + { + // 创建数据API + Method: http.MethodPost, + Path: "/datasvc/api/create", + Handler: datasvc.CreateApiHandler(serverCtx), + }, + { + // 删除数据API + Method: http.MethodPost, + Path: "/datasvc/api/delete", + Handler: datasvc.DeleteApiHandler(serverCtx), + }, + { + // API详情 + Method: http.MethodGet, + Path: "/datasvc/api/info", + Handler: datasvc.ApiInfoHandler(serverCtx), + }, + { + // API列表 + Method: http.MethodGet, + Path: "/datasvc/api/list", + Handler: datasvc.ApiListHandler(serverCtx), + }, + { + // 下线API + Method: http.MethodPost, + Path: "/datasvc/api/offline", + Handler: datasvc.OfflineApiHandler(serverCtx), + }, + { + // 发布API + Method: http.MethodPost, + Path: "/datasvc/api/publish", + Handler: datasvc.PublishApiHandler(serverCtx), + }, + { + // 更新数据API + Method: http.MethodPost, + Path: "/datasvc/api/update", + Handler: datasvc.UpdateApiHandler(serverCtx), + }, + { + // 注册调用应用 + Method: http.MethodPost, + Path: "/datasvc/app/register", + Handler: datasvc.RegisterAppHandler(serverCtx), + }, + { + // 执行数据API(外部统一入口) + Method: http.MethodPost, + Path: "/datasvc/execute", + Handler: datasvc.ExecuteApiHandler(serverCtx), + }, + { + // 授权API给应用 + Method: http.MethodPost, + Path: "/datasvc/grant", + Handler: datasvc.GrantApiHandler(serverCtx), + }, + }, ) - engine.AddRoutes( + server.AddRoutes( []rest.Route{ { + // 采集库表元数据 + Method: http.MethodPost, + Path: "/metadata/collect", + Handler: metadata.CollectDatabaseHandler(serverCtx), + }, + { + // 实体详情 Method: http.MethodGet, - Path: "/votes/activity/info", - Handler: votes.ActivityInfoHandler(serverCtx), + Path: "/metadata/entity/info", + Handler: metadata.MetaEntityInfoHandler(serverCtx), }, { + // 实体列表 Method: http.MethodGet, - Path: "/votes/activity/view", - Handler: votes.ActivityIcrViewHandler(serverCtx), + Path: "/metadata/entity/list", + Handler: metadata.MetaEntityListHandler(serverCtx), }, { + // 血缘图 Method: http.MethodGet, - Path: "/votes/enroll/info", - Handler: votes.EnrollInfoHandler(serverCtx), + Path: "/metadata/lineage", + Handler: metadata.LineageHandler(serverCtx), }, { + // 登记血缘 + Method: http.MethodPost, + Path: "/metadata/lineage/add", + Handler: metadata.AddLineageHandler(serverCtx), + }, + { + // 版本对比 Method: http.MethodGet, - Path: "/votes/enroll/lists", - Handler: votes.EnrollListsHandler(serverCtx), + Path: "/metadata/version/compare", + Handler: metadata.VersionCompareHandler(serverCtx), + }, + { + // 定版快照 + Method: http.MethodPost, + Path: "/metadata/version/create", + Handler: metadata.CreateVersionHandler(serverCtx), + }, + { + // 版本列表 + Method: http.MethodGet, + Path: "/metadata/version/list", + Handler: metadata.VersionListHandler(serverCtx), }, }, ) - engine.AddRoutes( + server.AddRoutes( + []rest.Route{ + { + // 获取活动的信息 + Method: http.MethodGet, + Path: "/questions/activities/info", + Handler: questions.ActivitiesInfoHandler(serverCtx), + }, + { + // 获取奖品信息 + Method: http.MethodGet, + Path: "/questions/award/info", + Handler: questions.AwardInfoHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/questions/award/list", + Handler: questions.AwardListHandler(serverCtx), + }, + }, + ) + + server.AddRoutes( rest.WithMiddlewares( []rest.Middleware{serverCtx.Usercheck}, []rest.Route{ { + // 提交答案 Method: http.MethodPost, - Path: "/votes/vote", - Handler: votes.VoteHandler(serverCtx), + Path: "/questions/change", + Handler: questions.ChangeHandler(serverCtx), + }, + { + // 获取分数 + Method: http.MethodGet, + Path: "/questions/grade", + Handler: questions.GradeHandler(serverCtx), + }, + { + // 获取题目 + Method: http.MethodGet, + Path: "/questions/lists", + Handler: questions.ListsHandler(serverCtx), }, { + // 填写中奖信息人 Method: http.MethodPost, - Path: "/votes/enroll", - Handler: votes.EnrollHandler(serverCtx), + Path: "/questions/lottery/convert", + Handler: questions.LotteryHandler(serverCtx), + }, + { + // 开始转盘 + Method: http.MethodPost, + Path: "/questions/lottery/turntable", + Handler: questions.TurntableHandler(serverCtx), }, }..., ), rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) - engine.AddRoutes( + server.AddRoutes( rest.WithMiddlewares( []rest.Middleware{serverCtx.Admincheck}, []rest.Route{ - { - Method: http.MethodGet, - Path: "/search/article", - Handler: search.ArticleHandler(serverCtx), - }, { Method: http.MethodGet, Path: "/search/articel/init", @@ -153,58 +318,103 @@ func RegisterHandlers(engine *rest.Server, serverCtx *svc.ServiceContext) { Path: "/search/articel/store", Handler: search.ArticleStoreHandler(serverCtx), }, + { + // 搜索 + Method: http.MethodGet, + Path: "/search/article", + Handler: search.ArticleHandler(serverCtx), + }, }..., ), ) - engine.AddRoutes( + server.AddRoutes( []rest.Route{ { - Method: http.MethodGet, - Path: "/questions/activities/info", - Handler: questions.ActivitiesInfoHandler(serverCtx), + Method: http.MethodPost, + Path: "/user/login", + Handler: user.LoginHandler(serverCtx), }, { - Method: http.MethodGet, - Path: "/questions/award/info", - Handler: questions.AwardInfoHandler(serverCtx), + Method: http.MethodPost, + Path: "/user/ping", + Handler: user.PingHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/user/register", + Handler: user.RegisterHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/user/wx/login", + Handler: user.WxloginHandler(serverCtx), }, { Method: http.MethodGet, - Path: "/questions/award/list", - Handler: questions.AwardListHandler(serverCtx), + Path: "/user/wx/login", + Handler: user.Code2SessionHandler(serverCtx), }, }, ) - engine.AddRoutes( + server.AddRoutes( rest.WithMiddlewares( []rest.Middleware{serverCtx.Usercheck}, []rest.Route{ { Method: http.MethodGet, - Path: "/questions/lists", - Handler: questions.ListsHandler(serverCtx), - }, - { - Method: http.MethodPost, - Path: "/questions/change", - Handler: questions.ChangeHandler(serverCtx), - }, - { - Method: http.MethodGet, - Path: "/questions/grade", - Handler: questions.GradeHandler(serverCtx), + Path: "/user/dc/info", + Handler: user.UserInfoHandler(serverCtx), }, + }..., + ), + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), + ) + + server.AddRoutes( + []rest.Route{ + { + // 获取活动的信息 + Method: http.MethodGet, + Path: "/votes/activity/info", + Handler: votes.ActivityInfoHandler(serverCtx), + }, + { + // 活动访问+1 + Method: http.MethodGet, + Path: "/votes/activity/view", + Handler: votes.ActivityIcrViewHandler(serverCtx), + }, + { + // 获取报名的投票作品信息 + Method: http.MethodGet, + Path: "/votes/enroll/info", + Handler: votes.EnrollInfoHandler(serverCtx), + }, + { + // 获取报名的投票作品列表 + Method: http.MethodGet, + Path: "/votes/enroll/lists", + Handler: votes.EnrollListsHandler(serverCtx), + }, + }, + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.Usercheck}, + []rest.Route{ { Method: http.MethodPost, - Path: "/questions/lottery/turntable", - Handler: questions.TurntableHandler(serverCtx), + Path: "/votes/enroll", + Handler: votes.EnrollHandler(serverCtx), }, { + // 投票 Method: http.MethodPost, - Path: "/questions/lottery/convert", - Handler: questions.LotteryHandler(serverCtx), + Path: "/votes/vote", + Handler: votes.VoteHandler(serverCtx), }, }..., ), diff --git a/internal/logic/catalog/assetlistlogic.go b/internal/logic/catalog/assetlistlogic.go new file mode 100644 index 0000000..c573b3d --- /dev/null +++ b/internal/logic/catalog/assetlistlogic.go @@ -0,0 +1,76 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package catalog + +import ( + "context" + + "datacenter/catalog/rpc/catalogClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AssetListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 资产列表 +func NewAssetListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AssetListLogic { + return &AssetListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// AssetList 转发 catalog rpc 查询资产分页列表 +func (l *AssetListLogic) AssetList(req *types.AssetListReq) (resp *types.AssetListResp, err error) { + // 分页默认值:page<=0 取 1,size<=0 取 10 + page := req.Page + if page <= 0 { + page = 1 + } + size := req.Size + if size <= 0 { + size = 10 + } + + // 逐字段映射为 rpc 请求 + rpcResp, err := l.svcCtx.CatalogRpc.ListAssets(l.ctx, &catalogClient.AssetListReq{ + Beid: req.Beid, + CategoryId: req.CategoryId, + Keyword: req.Keyword, + Page: page, + Size: size, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // 逐条映射资产列表(make 保证空列表序列化为 [] 而非 null) + list := make([]types.Asset, 0, len(rpcResp.GetList())) + for _, a := range rpcResp.GetList() { + list = append(list, types.Asset{ + Id: a.GetId(), + EntityId: a.GetEntityId(), + CategoryId: a.GetCategoryId(), + Name: a.GetName(), + EntityType: a.GetEntityType(), + Comment: a.GetComment(), + Classification: a.GetClassification(), + AssetTagsJson: a.GetAssetTagsJson(), + Visibility: a.GetVisibility(), + }) + } + + return &types.AssetListResp{ + List: list, + Total: rpcResp.GetTotal(), + }, nil +} diff --git a/internal/logic/catalog/categorylistlogic.go b/internal/logic/catalog/categorylistlogic.go new file mode 100644 index 0000000..d228608 --- /dev/null +++ b/internal/logic/catalog/categorylistlogic.go @@ -0,0 +1,50 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package catalog + +import ( + "context" + + "datacenter/catalog/rpc/catalogClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CategoryListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 分类列表 +func NewCategoryListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CategoryListLogic { + return &CategoryListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// CategoryList 转发 catalog rpc 查询分类列表 +func (l *CategoryListLogic) CategoryList(req *types.CategoryListReq) (resp *types.CategoryListResp, err error) { + // rpc ListCategories 入参为 IdReq:网关只传 beid 做租户过滤, + // id 留 0 表示取该 beid 下的全部分类(分类树) + rpcResp, err := l.svcCtx.CatalogRpc.ListCategories(l.ctx, &catalogClient.IdReq{ + Beid: req.Beid, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // 逐条映射分类列表(make 保证空列表序列化为 [] 而非 null) + list := make([]types.AssetCategory, 0, len(rpcResp.GetList())) + for _, c := range rpcResp.GetList() { + list = append(list, toCategory(c)) + } + + return &types.CategoryListResp{List: list}, nil +} diff --git a/internal/logic/catalog/convert.go b/internal/logic/catalog/convert.go new file mode 100644 index 0000000..7977e66 --- /dev/null +++ b/internal/logic/catalog/convert.go @@ -0,0 +1,17 @@ +package catalog + +import ( + "datacenter/catalog/rpc/catalogClient" + "datacenter/internal/types" +) + +// toCategory 将 rpc 侧的 AssetCategory 逐字段映射为网关侧的 AssetCategory。 +// 使用 pb 生成的 Get* 方法取值,对 nil 指针安全。 +func toCategory(c *catalogClient.AssetCategory) types.AssetCategory { + return types.AssetCategory{ + Id: c.GetId(), + ParentId: c.GetParentId(), + Name: c.GetName(), + Beid: c.GetBeid(), + } +} diff --git a/internal/logic/catalog/createcategorylogic.go b/internal/logic/catalog/createcategorylogic.go new file mode 100644 index 0000000..cd7c5ec --- /dev/null +++ b/internal/logic/catalog/createcategorylogic.go @@ -0,0 +1,47 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package catalog + +import ( + "context" + + "datacenter/catalog/rpc/catalogClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateCategoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建资产分类 +func NewCreateCategoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateCategoryLogic { + return &CreateCategoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// CreateCategory 转发 catalog rpc 创建分类,返回含 id 的 AssetCategory +func (l *CreateCategoryLogic) CreateCategory(req *types.CreateCategoryReq) (resp *types.AssetCategory, err error) { + // rpc 入参即 AssetCategory:逐字段映射(网关契约字段是它的子集) + rpcResp, err := l.svcCtx.CatalogRpc.CreateCategory(l.ctx, &catalogClient.AssetCategory{ + ParentId: req.ParentId, + Name: req.Name, + Beid: req.Beid, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // 逐字段映射 rpc 返回的分类(含生成的 id) + category := toCategory(rpcResp.GetCategory()) + return &category, nil +} diff --git a/internal/logic/catalog/tagassetlogic.go b/internal/logic/catalog/tagassetlogic.go new file mode 100644 index 0000000..f82a923 --- /dev/null +++ b/internal/logic/catalog/tagassetlogic.go @@ -0,0 +1,74 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package catalog + +import ( + "context" + + "datacenter/catalog/rpc/catalogClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TagAssetLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 登记资产 +func NewTagAssetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TagAssetLogic { + return &TagAssetLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// TagAsset 转发 catalog rpc 登记资产(元数据实体入目录),返回登记后的 Asset +func (l *TagAssetLogic) TagAsset(req *types.TagAssetReq) (resp *types.Asset, err error) { + // classification 未传时默认 L2(分级:L1/L2/L3) + classification := req.Classification + if classification == "" { + classification = "L2" + } + // visibility 未传时默认 private(public/private) + visibility := req.Visibility + if visibility == "" { + visibility = "private" + } + + // 逐字段映射为 rpc 请求 + rpcResp, err := l.svcCtx.CatalogRpc.TagAsset(l.ctx, &catalogClient.TagAssetReq{ + EntityId: req.EntityId, + CategoryId: req.CategoryId, + Name: req.Name, + EntityType: req.EntityType, + Comment: req.Comment, + Classification: classification, + AssetTagsJson: req.AssetTagsJson, + Visibility: visibility, + Beid: req.Beid, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // 逐字段映射 rpc 返回的资产(网关 Asset 契约无 beid 字段,不透出) + a := rpcResp.GetAsset() + return &types.Asset{ + Id: a.GetId(), + EntityId: a.GetEntityId(), + CategoryId: a.GetCategoryId(), + Name: a.GetName(), + EntityType: a.GetEntityType(), + Comment: a.GetComment(), + Classification: a.GetClassification(), + AssetTagsJson: a.GetAssetTagsJson(), + Visibility: a.GetVisibility(), + }, nil +} diff --git a/internal/logic/datasource/convert.go b/internal/logic/datasource/convert.go new file mode 100644 index 0000000..050301b --- /dev/null +++ b/internal/logic/datasource/convert.go @@ -0,0 +1,27 @@ +package datasource + +import ( + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/internal/types" +) + +// dataSourceInfoToTypes 将 rpc 的 DataSourceInfo 逐字段转换为网关类型。 +// rpc 侧的 PasswordEnc/ParamsJson 为敏感或内部字段, 网关对外不回传, 故不做映射。 +func dataSourceInfoToTypes(info *dataSourceClient.DataSourceInfo) *types.DataSourceInfo { + if info == nil { + return &types.DataSourceInfo{} + } + return &types.DataSourceInfo{ + Id: info.Id, + Name: info.Name, + Type: info.Type, + Host: info.Host, + Port: info.Port, + DbName: info.DbName, + Username: info.Username, + Status: info.Status, + Owner: info.Owner, + CreatedAt: info.CreatedAt, + UpdatedAt: info.UpdatedAt, + } +} diff --git a/internal/logic/datasource/createdatasourcelogic.go b/internal/logic/datasource/createdatasourcelogic.go new file mode 100644 index 0000000..fada3e5 --- /dev/null +++ b/internal/logic/datasource/createdatasourcelogic.go @@ -0,0 +1,50 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "context" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateDataSourceLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建数据源 +func NewCreateDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateDataSourceLogic { + return &CreateDataSourceLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 创建数据源: 网关请求逐字段映射为 rpc 请求, 转发后把 DataSourceInfo 转为网关类型返回 +func (l *CreateDataSourceLogic) CreateDataSource(req *types.CreateDataSourceReq) (resp *types.DataSourceInfo, err error) { + reply, err := l.svcCtx.DataSourceRpc.CreateDataSource(l.ctx, &dataSourceClient.CreateDataSourceReq{ + Name: req.Name, + Type: req.Type, + Host: req.Host, + Port: req.Port, + DbName: req.DbName, + Username: req.Username, + Password: req.Password, + ParamsJson: req.ParamsJson, + Owner: req.Owner, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return dataSourceInfoToTypes(reply.Datasource), nil +} diff --git a/internal/logic/datasource/datasourceinfologic.go b/internal/logic/datasource/datasourceinfologic.go new file mode 100644 index 0000000..2a5e12f --- /dev/null +++ b/internal/logic/datasource/datasourceinfologic.go @@ -0,0 +1,43 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "context" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DataSourceInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 数据源详情 +func NewDataSourceInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DataSourceInfoLogic { + return &DataSourceInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 数据源详情: 网关对外不回传密钥, with_secret 固定为 false +func (l *DataSourceInfoLogic) DataSourceInfo(req *types.DataSourceIdReq) (resp *types.DataSourceInfo, err error) { + reply, err := l.svcCtx.DataSourceRpc.GetDataSource(l.ctx, &dataSourceClient.GetDataSourceReq{ + Id: req.Id, + Beid: req.Beid, + WithSecret: false, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return dataSourceInfoToTypes(reply.Datasource), nil +} diff --git a/internal/logic/datasource/datasourcelistlogic.go b/internal/logic/datasource/datasourcelistlogic.go new file mode 100644 index 0000000..d31d024 --- /dev/null +++ b/internal/logic/datasource/datasourcelistlogic.go @@ -0,0 +1,58 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "context" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DataSourceListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 数据源列表 +func NewDataSourceListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DataSourceListLogic { + return &DataSourceListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 数据源列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *DataSourceListLogic) DataSourceList(req *types.DataSourceListReq) (resp *types.DataSourceListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.DataSourceRpc.ListDataSources(l.ctx, &dataSourceClient.ListReq{ + Beid: req.Beid, + Type: req.Type, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.DataSourceInfo, 0, len(reply.List)) + for _, item := range reply.List { + list = append(list, *dataSourceInfoToTypes(item)) + } + return &types.DataSourceListResp{ + List: list, + Total: reply.Total, + }, nil +} diff --git a/internal/logic/datasource/datasourcetestlogic.go b/internal/logic/datasource/datasourcetestlogic.go new file mode 100644 index 0000000..2934b72 --- /dev/null +++ b/internal/logic/datasource/datasourcetestlogic.go @@ -0,0 +1,46 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "context" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DataSourceTestLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 测试数据源连接 +func NewDataSourceTestLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DataSourceTestLogic { + return &DataSourceTestLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 测试数据源连接: 网关请求逐字段映射为 rpc 请求后转发 TestConnection, 业务错误直接透传 +func (l *DataSourceTestLogic) DataSourceTest(req *types.DataSourceIdReq) (resp *types.TestConnResp, err error) { + reply, err := l.svcCtx.DataSourceRpc.TestConnection(l.ctx, &dataSourceClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.TestConnResp{ + Ok: reply.Ok, + LatencyMs: reply.LatencyMs, + ErrMsg: reply.ErrMsg, + }, nil +} diff --git a/internal/logic/datasource/deletedatasourcelogic.go b/internal/logic/datasource/deletedatasourcelogic.go new file mode 100644 index 0000000..8fe435d --- /dev/null +++ b/internal/logic/datasource/deletedatasourcelogic.go @@ -0,0 +1,38 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "context" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteDataSourceLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除数据源 +func NewDeleteDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteDataSourceLogic { + return &DeleteDataSourceLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 删除数据源: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传 +func (l *DeleteDataSourceLogic) DeleteDataSource(req *types.DataSourceIdReq) error { + _, err := l.svcCtx.DataSourceRpc.DeleteDataSource(l.ctx, &dataSourceClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + return err +} diff --git a/internal/logic/datasource/updatedatasourcelogic.go b/internal/logic/datasource/updatedatasourcelogic.go new file mode 100644 index 0000000..14805b9 --- /dev/null +++ b/internal/logic/datasource/updatedatasourcelogic.go @@ -0,0 +1,48 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasource + +import ( + "context" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateDataSourceLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新数据源 +func NewUpdateDataSourceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateDataSourceLogic { + return &UpdateDataSourceLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 更新数据源: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传 +func (l *UpdateDataSourceLogic) UpdateDataSource(req *types.UpdateDataSourceReq) error { + _, err := l.svcCtx.DataSourceRpc.UpdateDataSource(l.ctx, &dataSourceClient.UpdateDataSourceReq{ + Id: req.Id, + Beid: req.Beid, + Name: req.Name, + Type: req.Type, + Host: req.Host, + Port: req.Port, + DbName: req.DbName, + Username: req.Username, + Password: req.Password, + ParamsJson: req.ParamsJson, + Status: req.Status, + Owner: req.Owner, + }) + return err +} diff --git a/internal/logic/datasvc/apiinfologic.go b/internal/logic/datasvc/apiinfologic.go new file mode 100644 index 0000000..a502564 --- /dev/null +++ b/internal/logic/datasvc/apiinfologic.go @@ -0,0 +1,46 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ApiInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// API详情 +func NewApiInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApiInfoLogic { + return &ApiInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// ApiInfo 按执行路径转发 datasvc rpc 查询 API 详情 +func (l *ApiInfoLogic) ApiInfo(req *types.ApiPathReq) (resp *types.ApiInfo, err error) { + // 逐字段映射为 rpc 请求(按 path + beid 定位) + rpcResp, err := l.svcCtx.DataSvcRpc.GetApi(l.ctx, &dataSvcClient.PathReq{ + Path: req.Path, + Beid: req.Beid, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // 逐字段映射 rpc 返回的 Api + info := apiToInfo(rpcResp.GetApi()) + return &info, nil +} diff --git a/internal/logic/datasvc/apilistlogic.go b/internal/logic/datasvc/apilistlogic.go new file mode 100644 index 0000000..6bd329d --- /dev/null +++ b/internal/logic/datasvc/apilistlogic.go @@ -0,0 +1,65 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ApiListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// API列表 +func NewApiListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApiListLogic { + return &ApiListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// ApiList 转发 datasvc rpc 查询 API 分页列表 +func (l *ApiListLogic) ApiList(req *types.ApiListReq) (resp *types.ApiListResp, err error) { + // 分页默认值:page<=0 取 1,size<=0 取 10 + page := req.Page + if page <= 0 { + page = 1 + } + size := req.Size + if size <= 0 { + size = 10 + } + + // 逐字段映射为 rpc 请求 + rpcResp, err := l.svcCtx.DataSvcRpc.ListApis(l.ctx, &dataSvcClient.ApiListReq{ + Beid: req.Beid, + Status: req.Status, + Page: page, + Size: size, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // 逐条映射 Api 列表(make 保证空列表序列化为 [] 而非 null) + list := make([]types.ApiInfo, 0, len(rpcResp.GetList())) + for _, api := range rpcResp.GetList() { + list = append(list, apiToInfo(api)) + } + + return &types.ApiListResp{ + List: list, + Total: rpcResp.GetTotal(), + }, nil +} diff --git a/internal/logic/datasvc/convert.go b/internal/logic/datasvc/convert.go new file mode 100644 index 0000000..bffad61 --- /dev/null +++ b/internal/logic/datasvc/convert.go @@ -0,0 +1,24 @@ +package datasvc + +import ( + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/types" +) + +// apiToInfo 将 rpc 侧的 Api 逐字段映射为网关侧的 ApiInfo。 +// 网关契约(types.ApiInfo)没有 beid / resp_sample_json 字段,故不透出。 +// 使用 pb 生成的 Get* 方法取值,对 nil 指针安全。 +func apiToInfo(api *dataSvcClient.Api) types.ApiInfo { + return types.ApiInfo{ + Id: api.GetId(), + Name: api.GetName(), + Path: api.GetPath(), + Mode: api.GetMode(), + DatasourceId: api.GetDatasourceId(), + SqlText: api.GetSqlText(), + ParamsJson: api.GetParamsJson(), + CacheTtl: api.GetCacheTtl(), + Status: api.GetStatus(), + Owner: api.GetOwner(), + } +} diff --git a/internal/logic/datasvc/createapilogic.go b/internal/logic/datasvc/createapilogic.go new file mode 100644 index 0000000..bab6d7e --- /dev/null +++ b/internal/logic/datasvc/createapilogic.go @@ -0,0 +1,59 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateApiLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建数据API +func NewCreateApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateApiLogic { + return &CreateApiLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// CreateApi 转发 datasvc rpc 创建数据 API,返回含 id 的 ApiInfo +func (l *CreateApiLogic) CreateApi(req *types.CreateApiReq) (resp *types.ApiInfo, err error) { + // mode 未传时默认 sql(proto 约定:wizard/sql/register 三种模式) + mode := req.Mode + if mode == "" { + mode = "sql" + } + + // 逐字段映射为 rpc 请求(网关契约无 resp_sample_json,置空由 rpc 侧处理) + rpcResp, err := l.svcCtx.DataSvcRpc.CreateApi(l.ctx, &dataSvcClient.CreateApiReq{ + Name: req.Name, + Path: req.Path, + Mode: mode, + DatasourceId: req.DatasourceId, + SqlText: req.SqlText, + ParamsJson: req.ParamsJson, + CacheTtl: req.CacheTtl, + Owner: req.Owner, + Beid: req.Beid, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // 返回创建结果(含 id) + info := apiToInfo(rpcResp.GetApi()) + return &info, nil +} diff --git a/internal/logic/datasvc/deleteapilogic.go b/internal/logic/datasvc/deleteapilogic.go new file mode 100644 index 0000000..390240e --- /dev/null +++ b/internal/logic/datasvc/deleteapilogic.go @@ -0,0 +1,40 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteApiLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除数据API +func NewDeleteApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteApiLogic { + return &DeleteApiLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// DeleteApi 转发 datasvc rpc 删除数据 API +func (l *DeleteApiLogic) DeleteApi(req *types.DsIdReq) error { + // 逐字段映射为 rpc 请求 + _, err := l.svcCtx.DataSvcRpc.DeleteApi(l.ctx, &dataSvcClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + // rpc 业务错误原样透传 + return err +} diff --git a/internal/logic/datasvc/executeapilogic.go b/internal/logic/datasvc/executeapilogic.go new file mode 100644 index 0000000..0e64723 --- /dev/null +++ b/internal/logic/datasvc/executeapilogic.go @@ -0,0 +1,54 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ExecuteApiLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 执行数据API(外部统一入口) +func NewExecuteApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ExecuteApiLogic { + return &ExecuteApiLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// ExecuteApi 转发 datasvc rpc 执行 API(rpc 侧完成鉴权→限流→缓存→执行→脱敏) +func (l *ExecuteApiLogic) ExecuteApi(req *types.ExecuteApiReq) (resp *types.ExecuteApiResp, err error) { + // 逐字段映射为 rpc 请求;网关契约无 ip / beid 字段, + // beid 传 0(proto 零值),ip 传空串,由 rpc 侧按 path 归属处理 + rpcResp, err := l.svcCtx.DataSvcRpc.ExecuteApi(l.ctx, &dataSvcClient.ExecuteReq{ + AppKey: req.AppKey, + Sign: req.Sign, + Ts: req.Ts, + Path: req.Path, + ParamsJson: req.ParamsJson, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // 逐字段映射执行结果 + return &types.ExecuteApiResp{ + RowsJson: rpcResp.GetRowsJson(), + CostMs: rpcResp.GetCostMs(), + CacheHit: rpcResp.GetCacheHit(), + Affected: rpcResp.GetAffected(), + }, nil +} diff --git a/internal/logic/datasvc/grantapilogic.go b/internal/logic/datasvc/grantapilogic.go new file mode 100644 index 0000000..918bbc7 --- /dev/null +++ b/internal/logic/datasvc/grantapilogic.go @@ -0,0 +1,44 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GrantApiLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 授权API给应用 +func NewGrantApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GrantApiLogic { + return &GrantApiLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// GrantApi 转发 datasvc rpc 授权 API 给应用 +func (l *GrantApiLogic) GrantApi(req *types.GrantApiReq) error { + // 逐字段映射为 rpc 请求;expire_at 为 unix 秒, + // proto 约定 0 表示永久授权,故网关未传时直接透传 0 即可 + _, err := l.svcCtx.DataSvcRpc.GrantApi(l.ctx, &dataSvcClient.GrantApiReq{ + ApiId: req.ApiId, + AppId: req.AppId, + ExpireAt: req.ExpireAt, + FieldMask: req.FieldMask, + Beid: req.Beid, + }) + // rpc 业务错误原样透传 + return err +} diff --git a/internal/logic/datasvc/offlineapilogic.go b/internal/logic/datasvc/offlineapilogic.go new file mode 100644 index 0000000..c1a8d29 --- /dev/null +++ b/internal/logic/datasvc/offlineapilogic.go @@ -0,0 +1,40 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type OfflineApiLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 下线API +func NewOfflineApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OfflineApiLogic { + return &OfflineApiLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// OfflineApi 转发 datasvc rpc 下线 API +func (l *OfflineApiLogic) OfflineApi(req *types.DsIdReq) error { + // 逐字段映射为 rpc 请求 + _, err := l.svcCtx.DataSvcRpc.OfflineApi(l.ctx, &dataSvcClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + // rpc 业务错误原样透传 + return err +} diff --git a/internal/logic/datasvc/publishapilogic.go b/internal/logic/datasvc/publishapilogic.go new file mode 100644 index 0000000..8d61021 --- /dev/null +++ b/internal/logic/datasvc/publishapilogic.go @@ -0,0 +1,40 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type PublishApiLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 发布API +func NewPublishApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PublishApiLogic { + return &PublishApiLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// PublishApi 转发 datasvc rpc 发布 API +func (l *PublishApiLogic) PublishApi(req *types.DsIdReq) error { + // 逐字段映射为 rpc 请求 + _, err := l.svcCtx.DataSvcRpc.PublishApi(l.ctx, &dataSvcClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + // rpc 业务错误原样透传 + return err +} diff --git a/internal/logic/datasvc/registerapplogic.go b/internal/logic/datasvc/registerapplogic.go new file mode 100644 index 0000000..ddb57b9 --- /dev/null +++ b/internal/logic/datasvc/registerapplogic.go @@ -0,0 +1,52 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RegisterAppLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 注册调用应用 +func NewRegisterAppLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterAppLogic { + return &RegisterAppLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// RegisterApp 转发 datasvc rpc 注册调用应用, +// 把返回的 app_id / app_key / app_secret 映射为网关 AppResp(secret 仅此一次) +func (l *RegisterAppLogic) RegisterApp(req *types.RegisterAppReq) (resp *types.AppResp, err error) { + // 逐字段映射为 rpc 请求 + rpcResp, err := l.svcCtx.DataSvcRpc.RegisterApp(l.ctx, &dataSvcClient.RegisterAppReq{ + Name: req.Name, + RateLimitQps: req.RateLimitQps, + IpWhitelist: req.IpWhitelist, + Beid: req.Beid, + }) + if err != nil { + // rpc 业务错误原样透传 + return nil, err + } + + // rpc 返回 ApiApp(含 id/app_key)+ AppSecret,映射为网关 AppResp + return &types.AppResp{ + AppId: rpcResp.GetApp().GetId(), + AppKey: rpcResp.GetApp().GetAppKey(), + AppSecret: rpcResp.GetAppSecret(), + }, nil +} diff --git a/internal/logic/datasvc/updateapilogic.go b/internal/logic/datasvc/updateapilogic.go new file mode 100644 index 0000000..ae96473 --- /dev/null +++ b/internal/logic/datasvc/updateapilogic.go @@ -0,0 +1,44 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package datasvc + +import ( + "context" + + "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateApiLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新数据API +func NewUpdateApiLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApiLogic { + return &UpdateApiLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// UpdateApi 转发 datasvc rpc 更新数据 API +func (l *UpdateApiLogic) UpdateApi(req *types.UpdateApiReq) error { + // 逐字段映射为 rpc 请求(网关契约无 resp_sample_json,置空表示不修改时由 rpc 侧判断) + _, err := l.svcCtx.DataSvcRpc.UpdateApi(l.ctx, &dataSvcClient.UpdateApiReq{ + Id: req.Id, + Beid: req.Beid, + Name: req.Name, + SqlText: req.SqlText, + ParamsJson: req.ParamsJson, + CacheTtl: req.CacheTtl, + }) + // rpc 业务错误原样透传 + return err +} diff --git a/internal/logic/metadata/addlineagelogic.go b/internal/logic/metadata/addlineagelogic.go new file mode 100644 index 0000000..c5eb58e --- /dev/null +++ b/internal/logic/metadata/addlineagelogic.go @@ -0,0 +1,40 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AddLineageLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 登记血缘 +func NewAddLineageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddLineageLogic { + return &AddLineageLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 登记血缘: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传 +func (l *AddLineageLogic) AddLineage(req *types.AddLineageReq) error { + _, err := l.svcCtx.MetadataRpc.AddLineage(l.ctx, &metadataClient.AddLineageReq{ + FromId: req.FromId, + ToId: req.ToId, + PropsJson: req.PropsJson, + Beid: req.Beid, + }) + return err +} diff --git a/internal/logic/metadata/collectdatabaselogic.go b/internal/logic/metadata/collectdatabaselogic.go new file mode 100644 index 0000000..60af9a1 --- /dev/null +++ b/internal/logic/metadata/collectdatabaselogic.go @@ -0,0 +1,68 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "context" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CollectDatabaseLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 采集库表元数据 +func NewCollectDatabaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CollectDatabaseLogic { + return &CollectDatabaseLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 采集库表元数据: dsn 为空且指定了数据源时, 先经数据源服务(with_secret=true)换取 dsn, 再转发元数据采集 +func (l *CollectDatabaseLogic) CollectDatabase(req *types.CollectDatabaseReq) (resp *types.CollectResp, err error) { + dsn := req.Dsn + if dsn == "" && req.DsId > 0 { + // 内部服务调用, 携带 with_secret 解密密码并拼 dsn + dsReply, err := l.svcCtx.DataSourceRpc.GetDataSource(l.ctx, &dataSourceClient.GetDataSourceReq{ + Id: req.DsId, + Beid: req.Beid, + WithSecret: true, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + dsn = dsReply.Dsn + } + // db_type 缺省按 mysql 处理 + dbType := req.DbType + if dbType == "" { + dbType = "mysql" + } + reply, err := l.svcCtx.MetadataRpc.CollectDatabase(l.ctx, &metadataClient.CollectDatabaseReq{ + DsId: req.DsId, + Beid: req.Beid, + DbType: dbType, + Dsn: dsn, + DbName: req.DbName, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.CollectResp{ + TablesCreated: reply.TablesCreated, + ColumnsCreated: reply.ColumnsCreated, + }, nil +} diff --git a/internal/logic/metadata/convert.go b/internal/logic/metadata/convert.go new file mode 100644 index 0000000..6f99e7b --- /dev/null +++ b/internal/logic/metadata/convert.go @@ -0,0 +1,68 @@ +package metadata + +import ( + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" +) + +// entityToTypes 将 rpc 的 MetaEntity 逐字段转换为网关类型 +func entityToTypes(entity *metadataClient.MetaEntity) *types.MetaEntity { + if entity == nil { + return &types.MetaEntity{} + } + return &types.MetaEntity{ + Id: entity.Id, + EntityType: entity.EntityType, + Fqn: entity.Fqn, + Name: entity.Name, + Comment: entity.Comment, + Engine: entity.Engine, + RowsCnt: entity.RowsCnt, + PropsJson: entity.PropsJson, + Classification: entity.Classification, + Owner: entity.Owner, + DsId: entity.DsId, + Beid: entity.Beid, + } +} + +// versionToTypes 将 rpc 的 MetaVersion 逐字段转换为网关类型 +func versionToTypes(version *metadataClient.MetaVersion) *types.MetaVersion { + if version == nil { + return &types.MetaVersion{} + } + return &types.MetaVersion{ + Id: version.Id, + EntityId: version.EntityId, + Version: version.Version, + SnapshotJson: version.SnapshotJson, + Operator: version.Operator, + PublishedAt: version.PublishedAt, + } +} + +// lineageNodesToTypes 将 rpc 的血缘节点列表逐字段转换为网关类型 +func lineageNodesToTypes(nodes []*metadataClient.LineageNode) []types.LineageNode { + list := make([]types.LineageNode, 0, len(nodes)) + for _, node := range nodes { + list = append(list, types.LineageNode{ + Id: node.GetId(), + Fqn: node.GetFqn(), + Name: node.GetName(), + }) + } + return list +} + +// lineageEdgesToTypes 将 rpc 的血缘边列表逐字段转换为网关类型 +func lineageEdgesToTypes(edges []*metadataClient.LineageEdge) []types.LineageEdge { + list := make([]types.LineageEdge, 0, len(edges)) + for _, edge := range edges { + list = append(list, types.LineageEdge{ + FromId: edge.GetFromId(), + ToId: edge.GetToId(), + PropsJson: edge.GetPropsJson(), + }) + } + return list +} diff --git a/internal/logic/metadata/createversionlogic.go b/internal/logic/metadata/createversionlogic.go new file mode 100644 index 0000000..bbc0743 --- /dev/null +++ b/internal/logic/metadata/createversionlogic.go @@ -0,0 +1,45 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateVersionLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 定版快照 +func NewCreateVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateVersionLogic { + return &CreateVersionLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 定版快照: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传。 +// 网关与 rpc 的请求结构均无 operator 字段, 操作人兜底 "admin" 由 rpc 侧负责。 +func (l *CreateVersionLogic) CreateVersion(req *types.MetaEntityIdReq) (resp *types.VersionResp, err error) { + reply, err := l.svcCtx.MetadataRpc.CreateVersion(l.ctx, &metadataClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.VersionResp{ + Version: *versionToTypes(reply.Version), + }, nil +} diff --git a/internal/logic/metadata/lineagelogic.go b/internal/logic/metadata/lineagelogic.go new file mode 100644 index 0000000..38f17eb --- /dev/null +++ b/internal/logic/metadata/lineagelogic.go @@ -0,0 +1,54 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type LineageLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 血缘图 +func NewLineageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LineageLogic { + return &LineageLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 血缘图: direction 缺省 downstream, depth 缺省 2, 转发后转换节点与边, 业务错误直接透传 +func (l *LineageLogic) Lineage(req *types.LineageReq) (resp *types.LineageResp, err error) { + direction, depth := req.Direction, req.Depth + if direction == "" { + direction = "downstream" + } + if depth <= 0 { + depth = 2 + } + reply, err := l.svcCtx.MetadataRpc.GetLineage(l.ctx, &metadataClient.LineageReq{ + EntityId: req.EntityId, + Direction: direction, + Depth: depth, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.LineageResp{ + Nodes: lineageNodesToTypes(reply.Nodes), + Edges: lineageEdgesToTypes(reply.Edges), + }, nil +} diff --git a/internal/logic/metadata/metaentityinfologic.go b/internal/logic/metadata/metaentityinfologic.go new file mode 100644 index 0000000..6f5b69c --- /dev/null +++ b/internal/logic/metadata/metaentityinfologic.go @@ -0,0 +1,42 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type MetaEntityInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 实体详情 +func NewMetaEntityInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MetaEntityInfoLogic { + return &MetaEntityInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 实体详情: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传 +func (l *MetaEntityInfoLogic) MetaEntityInfo(req *types.MetaEntityIdReq) (resp *types.MetaEntity, err error) { + reply, err := l.svcCtx.MetadataRpc.GetEntity(l.ctx, &metadataClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return entityToTypes(reply.Entity), nil +} diff --git a/internal/logic/metadata/metaentitylistlogic.go b/internal/logic/metadata/metaentitylistlogic.go new file mode 100644 index 0000000..cf07b9c --- /dev/null +++ b/internal/logic/metadata/metaentitylistlogic.go @@ -0,0 +1,60 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type MetaEntityListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 实体列表 +func NewMetaEntityListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MetaEntityListLogic { + return &MetaEntityListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 实体列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *MetaEntityListLogic) MetaEntityList(req *types.MetaEntityListReq) (resp *types.MetaEntityListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.MetadataRpc.ListEntities(l.ctx, &metadataClient.EntityListReq{ + Beid: req.Beid, + EntityType: req.EntityType, + Keyword: req.Keyword, + DsId: req.DsId, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.MetaEntity, 0, len(reply.List)) + for _, item := range reply.List { + list = append(list, *entityToTypes(item)) + } + return &types.MetaEntityListResp{ + List: list, + Total: reply.Total, + }, nil +} diff --git a/internal/logic/metadata/versioncomparelogic.go b/internal/logic/metadata/versioncomparelogic.go new file mode 100644 index 0000000..1803fe9 --- /dev/null +++ b/internal/logic/metadata/versioncomparelogic.go @@ -0,0 +1,44 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type VersionCompareLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 版本对比 +func NewVersionCompareLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VersionCompareLogic { + return &VersionCompareLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 版本对比: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传 +func (l *VersionCompareLogic) VersionCompare(req *types.CompareReq) (resp *types.DiffResp, err error) { + reply, err := l.svcCtx.MetadataRpc.CompareVersions(l.ctx, &metadataClient.CompareReq{ + VersionA: req.VersionA, + VersionB: req.VersionB, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.DiffResp{ + DiffJson: reply.DiffJson, + }, nil +} diff --git a/internal/logic/metadata/versionlistlogic.go b/internal/logic/metadata/versionlistlogic.go new file mode 100644 index 0000000..0a960d3 --- /dev/null +++ b/internal/logic/metadata/versionlistlogic.go @@ -0,0 +1,48 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metadata + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metadata/rpc/metadataClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type VersionListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 版本列表 +func NewVersionListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VersionListLogic { + return &VersionListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 版本列表: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传 +func (l *VersionListLogic) VersionList(req *types.MetaEntityIdReq) (resp *types.VersionListResp, err error) { + reply, err := l.svcCtx.MetadataRpc.ListVersions(l.ctx, &metadataClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.MetaVersion, 0, len(reply.List)) + for _, item := range reply.List { + list = append(list, *versionToTypes(item)) + } + return &types.VersionListResp{ + List: list, + }, nil +} diff --git a/internal/svc/servicecontext.go b/internal/svc/servicecontext.go index c3fa99c..0219cf7 100644 --- a/internal/svc/servicecontext.go +++ b/internal/svc/servicecontext.go @@ -2,9 +2,13 @@ package svc import ( "context" + "datacenter/catalog/rpc/catalogClient" "datacenter/common/rpc/commonclient" + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/datasvc/rpc/dataSvcClient" "datacenter/internal/config" "datacenter/internal/middleware" + "datacenter/metadata/rpc/metadataClient" "datacenter/questions/rpc/questionsclient" "datacenter/search/rpc/searchclient" "datacenter/shared" @@ -29,11 +33,15 @@ type ServiceContext struct { GreetMiddleware2 rest.Middleware Usercheck rest.Middleware Admincheck rest.Middleware - UserRpc userclient.User //用户 - CommonRpc commonclient.Common //公共 - VotesRpc votesclient.Votes //投票 - SearchRpc searchclient.Search //搜索 - QuestionsRpc questionsclient.Questions //问答抽奖 + UserRpc userclient.User //用户 + CommonRpc commonclient.Common //公共 + VotesRpc votesclient.Votes //投票 + SearchRpc searchclient.Search //搜索 + QuestionsRpc questionsclient.Questions //问答抽奖 + DataSourceRpc dataSourceClient.DataSource //数据源(Phase 1) + MetadataRpc metadataClient.Metadata //元数据(Phase 1) + DataSvcRpc dataSvcClient.DataSvc //数据服务(Phase 1) + CatalogRpc catalogClient.Catalog //资产目录(Phase 1) Cache cache.Cache RedisConn *redis.Redis } @@ -55,6 +63,10 @@ func NewServiceContext(c config.Config) *ServiceContext { vr := votesclient.NewVotes(zrpc.MustNewClient(c.VotesRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) sr := searchclient.NewSearch(zrpc.MustNewClient(c.SearchRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) qr := questionsclient.NewQuestions(zrpc.MustNewClient(c.QuestionsRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + dsr := dataSourceClient.NewDataSource(zrpc.MustNewClient(c.DataSourceRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + mr := metadataClient.NewMetadata(zrpc.MustNewClient(c.MetadataRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + dvr := dataSvcClient.NewDataSvc(zrpc.MustNewClient(c.DataSvcRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + cr2 := catalogClient.NewCatalog(zrpc.MustNewClient(c.CatalogRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) //缓存 ca := cache.New(c.CacheRedis, syncx.NewSingleFlight(), cache.NewStat("dc"), shared.ErrNotFound) rcon := redis.MustNewRedis(c.CacheRedis[0].RedisConf) @@ -69,6 +81,10 @@ func NewServiceContext(c config.Config) *ServiceContext { VotesRpc: vr, SearchRpc: sr, QuestionsRpc: qr, + DataSourceRpc: dsr, + MetadataRpc: mr, + DataSvcRpc: dvr, + CatalogRpc: cr2, Cache: ca, RedisConn: rcon, } diff --git a/internal/types/types.go b/internal/types/types.go index dffe84b..725c5ba 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,75 +1,77 @@ // Code generated by goctl. DO NOT EDIT. -package types +// goctl 1.10.2 -type Beid struct { - Beid int64 `json:"beid,optional" form:"beid,optional"` -} +package types -type Token struct { - Token string `json:"token"` +type Actid struct { + Actid int64 `json:"actid,optional" form:"actid,optional"` //活动id } -type WxTicket struct { - Ticket string `json:"ticket"` +type ActivityResp struct { + Actid int64 `json:"actid"` + Title string `json:"title"` //活动名称 + Descr string `json:"descr"` //活动描述 + StartDate int64 `json:"start_date"` //活动时间 + EnrollDate int64 `json:"enroll_date"` //投票时间 + EndDate int64 `json:"end_date"` //活动结束时间 + Votecount int64 `json:"votecount"` //当前活动的总票数 + Viewcount int64 `json:"viewcount"` //当前活动的总浏览数 + Type int64 `json:"type"` //投票方式 + Num int64 `json:"num"` //投票几票 } -type Application struct { - Sname string `json:"Sname"` //名称 - Logo string `json:"logo"` // login - Isclose int64 `json:"isclose"` //是否关闭 - Fullwebsite string `json:"fullwebsite"` // 全站名称 +type AddLineageReq struct { + FromId int64 `json:"from_id"` + ToId int64 `json:"to_id"` + PropsJson string `json:"props_json,optional"` + Beid int64 `json:"beid,optional"` } -type SnsReq struct { - Beid - Ptyid int64 `json:"ptyid"` //对应平台 - BackUrl string `json:"back_url"` //登陆返回的地址 +type AnswerReq struct { + ActivityId int64 `json:"actid"` + Answers string `json:"answers"` + Score string `json:"score"` } -type SnsResp struct { - Beid - Ptyid int64 `json:"ptyid"` //对应平台 - Appid string `json:"appid"` //sns 平台的id - Title string `json:"title"` //名称 - LoginUrl string `json:"login_url"` //微信登陆的地址 +type AnswerResp struct { + Answers string `json:"answers"` + Score string `json:"score"` } -type WxShareResp struct { - Appid string `json:"appid"` - Timestamp int64 `json:"timestamp"` - Noncestr string `json:"noncestr"` - Signature string `json:"signature"` +type ApiInfo struct { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + Path string `json:"path,optional"` + Mode string `json:"mode,optional"` + DatasourceId int64 `json:"datasource_id,optional"` + SqlText string `json:"sql_text,optional"` + ParamsJson string `json:"params_json,optional"` + CacheTtl int64 `json:"cache_ttl,optional"` + Status string `json:"status,optional"` + Owner string `json:"owner,optional"` } -type RegisterReq struct { - Mobile string `json:"mobile"` //基本一个手机号码就完事 - Password string `json:"password"` - Smscode string `json:"smscode"` //短信码 +type ApiListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Status string `json:"status,optional" form:"status,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` } -type LoginReq struct { - Mobile string `json:"mobile"` - Type int64 `json:"type"` //1.密码登陆,2.短信登陆 - Password string `json:"password"` +type ApiListResp struct { + List []ApiInfo `json:"list"` + Total int64 `json:"total"` } -type WxLoginReq struct { - Beid int64 `json:"beid"` //应用id - Code string `json:"code"` //微信登陆密钥 - Ptyid int64 `json:"ptyid"` //对应平台 +type ApiPathReq struct { + Path string `json:"path,optional" form:"path,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` } -type UserReply struct { - Auid int64 `json:"auid"` - Uid int64 `json:"uid"` - Beid int64 `json:"beid"` //应用id - Ptyid int64 `json:"ptyid"` //对应平台 - Username string `json:"username"` - Mobile string `json:"mobile"` - Nickname string `json:"nickname"` - Openid string `json:"openid"` - Avator string `json:"avator"` - JwtToken +type AppResp struct { + AppId int64 `json:"app_id"` + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` //仅注册时返回一次 } type AppUser struct { @@ -82,64 +84,156 @@ type AppUser struct { Avator string `json:"avator"` } -type LoginAppUser struct { - Uid int64 `json:"uid"` - Auid int64 `json:"auid"` - Beid int64 `json:"beid"` //应用id - Ptyid int64 `json:"ptyid"` //对应平台 - Nickname string `json:"nickname"` - Openid string `json:"openid"` - Avator string `json:"avator"` - JwtToken +type Application struct { + Sname string `json:"Sname"` //名称 + Logo string `json:"logo"` // login + Isclose int64 `json:"isclose"` //是否关闭 + Fullwebsite string `json:"fullwebsite"` // 全站名称 } -type JwtToken struct { - AccessToken string `json:"access_token,omitempty"` - AccessExpire int64 `json:"access_expire,omitempty"` - RefreshAfter int64 `json:"refresh_after,omitempty"` +type ArticleReq struct { + NewsId string `json:"NewsId"` + NewsTitle string `json:"NewsTitle"` + ImageUrl string `json:"ImageUrl"` } -type UserReq struct { - Auid int64 `json:"auid,optional" form:"auid,optional"` - Uid int64 `json:"uid,optional" form:"uid,optional"` - Beid int64 `json:"beid,optional" form:"beid,optional"` //应用id - Ptyid int64 `json:"ptyid,optional" form:"ptyid,optional"` +type Asset struct { + Id int64 `json:"id,optional"` + EntityId int64 `json:"entity_id,optional"` + CategoryId int64 `json:"category_id,optional"` + Name string `json:"name,optional"` + EntityType string `json:"entity_type,optional"` + Comment string `json:"comment,optional"` + Classification string `json:"classification,optional"` + AssetTagsJson string `json:"asset_tags_json,optional"` + Visibility string `json:"visibility,optional"` } -type Request struct { - Name string `path:"name,options=you|me"` +type AssetCategory struct { + Id int64 `json:"id,optional"` + ParentId int64 `json:"parent_id,optional"` + Name string `json:"name,optional"` + Beid int64 `json:"beid,optional"` } -type Response struct { - Message string `json:"message"` +type AssetListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + CategoryId int64 `json:"category_id,optional" form:"category_id,optional"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` } -type Actid struct { - Actid int64 `json:"actid,optional" form:"actid,optional"` //活动id +type AssetListResp struct { + List []Asset `json:"list"` + Total int64 `json:"total"` } -type VoteReq struct { - Aeid int64 `json:"aeid,optional" form:"aeid,optional"` // 作品id - Actid +type AwardConvertReq struct { + UserName string `json:"username"` + Phone string `json:"phone"` + LotteryId int64 `json:"lotteryid"` } -type VoteResp struct { - VoteReq - Votecount int64 `json:"votecount"` //投票票数 - Viewcount int64 `json:"viewcount"` //浏览数 +type Beid struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` } -type ActivityResp struct { - Actid int64 `json:"actid"` - Title string `json:"title"` //活动名称 - Descr string `json:"descr"` //活动描述 - StartDate int64 `json:"start_date"` //活动时间 - EnrollDate int64 `json:"enroll_date"` //投票时间 - EndDate int64 `json:"end_date"` //活动结束时间 - Votecount int64 `json:"votecount"` //当前活动的总票数 - Viewcount int64 `json:"viewcount"` //当前活动的总浏览数 - Type int64 `json:"type"` //投票方式 - Num int64 `json:"num"` //投票几票 +type CategoryListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type CategoryListResp struct { + List []AssetCategory `json:"list"` +} + +type CollectDatabaseReq struct { + DsId int64 `json:"ds_id,optional"` + Beid int64 `json:"beid,optional"` + DbType string `json:"db_type,optional"` + Dsn string `json:"dsn,optional"` //为空则按 ds_id 从数据源服务取 + DbName string `json:"db_name,optional"` +} + +type CollectResp struct { + TablesCreated int64 `json:"tables_created"` + ColumnsCreated int64 `json:"columns_created"` +} + +type CompareReq struct { + VersionA int64 `json:"version_a,optional" form:"version_a,optional"` + VersionB int64 `json:"version_b,optional" form:"version_b,optional"` +} + +type CreateApiReq struct { + Name string `json:"name"` + Path string `json:"path"` + Mode string `json:"mode,optional"` + DatasourceId int64 `json:"datasource_id"` + SqlText string `json:"sql_text"` + ParamsJson string `json:"params_json,optional"` + CacheTtl int64 `json:"cache_ttl,optional"` + Owner string `json:"owner,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateCategoryReq struct { + ParentId int64 `json:"parent_id,optional"` + Name string `json:"name"` + Beid int64 `json:"beid,optional"` +} + +type CreateDataSourceReq struct { + Name string `json:"name"` + Type string `json:"type"` + Host string `json:"host,optional"` + Port int32 `json:"port,optional"` + DbName string `json:"db_name,optional"` + Username string `json:"username,optional"` + Password string `json:"password,optional"` + ParamsJson string `json:"params_json,optional"` + Owner string `json:"owner,optional"` + Beid int64 `json:"beid,optional"` +} + +type DataSourceIdReq struct { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type DataSourceInfo struct { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + Type string `json:"type,optional"` + Host string `json:"host,optional"` + Port int32 `json:"port,optional"` + DbName string `json:"db_name,optional"` + Username string `json:"username,optional"` + Status int32 `json:"status,optional"` + Owner string `json:"owner,optional"` + CreatedAt int64 `json:"created_at,optional"` + UpdatedAt int64 `json:"updated_at,optional"` +} + +type DataSourceListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Type string `json:"type,optional" form:"type,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type DataSourceListResp struct { + List []DataSourceInfo `json:"list"` + Total int64 `json:"total"` +} + +type DiffResp struct { + DiffJson string `json:"diff_json"` +} + +type DsIdReq struct { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` } type EnrollReq struct { @@ -161,6 +255,145 @@ type EnrollResp struct { Viewcount int64 `json:"viewcount"` //当前活动的总浏览数 } +type ExecuteApiReq struct { + AppKey string `json:"app_key"` + Sign string `json:"sign"` + Ts string `json:"ts"` + Path string `json:"path"` + ParamsJson string `json:"params_json,optional"` +} + +type ExecuteApiResp struct { + RowsJson string `json:"rows_json"` + CostMs int64 `json:"cost_ms"` + CacheHit bool `json:"cache_hit"` + Affected int64 `json:"affected"` +} + +type GrantApiReq struct { + ApiId int64 `json:"api_id"` + AppId int64 `json:"app_id"` + ExpireAt int64 `json:"expire_at,optional"` //unix 秒,0 永久 + FieldMask string `json:"field_mask,optional"` + Beid int64 `json:"beid,optional"` +} + +type JwtToken struct { + AccessToken string `json:"access_token,omitempty"` + AccessExpire int64 `json:"access_expire,omitempty"` + RefreshAfter int64 `json:"refresh_after,omitempty"` +} + +type LineageEdge struct { + FromId int64 `json:"from_id,optional"` + ToId int64 `json:"to_id,optional"` + PropsJson string `json:"props_json,optional"` +} + +type LineageNode struct { + Id int64 `json:"id,optional"` + Fqn string `json:"fqn,optional"` + Name string `json:"name,optional"` +} + +type LineageReq struct { + EntityId int64 `json:"entity_id,optional" form:"entity_id,optional"` + Direction string `json:"direction,optional" form:"direction,optional"` + Depth int32 `json:"depth,optional" form:"depth,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type LineageResp struct { + Nodes []LineageNode `json:"nodes"` + Edges []LineageEdge `json:"edges"` +} + +type LoginAppUser struct { + Uid int64 `json:"uid"` + Auid int64 `json:"auid"` + Beid int64 `json:"beid"` //应用id + Ptyid int64 `json:"ptyid"` //对应平台 + Nickname string `json:"nickname"` + Openid string `json:"openid"` + Avator string `json:"avator"` + JwtToken +} + +type LoginReq struct { + Mobile string `json:"mobile"` + Type int64 `json:"type"` //1.密码登陆,2.短信登陆 + Password string `json:"password"` +} + +type MetaEntity struct { + Id int64 `json:"id,optional"` + EntityType string `json:"entity_type,optional"` + Fqn string `json:"fqn,optional"` + Name string `json:"name,optional"` + Comment string `json:"comment,optional"` + Engine string `json:"engine,optional"` + RowsCnt int64 `json:"rows_cnt,optional"` + PropsJson string `json:"props_json,optional"` + Classification string `json:"classification,optional"` + Owner string `json:"owner,optional"` + DsId int64 `json:"ds_id,optional"` + Beid int64 `json:"beid,optional"` +} + +type MetaEntityIdReq struct { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type MetaEntityListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + EntityType string `json:"entity_type,optional" form:"entity_type,optional"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + DsId int64 `json:"ds_id,optional" form:"ds_id,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type MetaEntityListResp struct { + List []MetaEntity `json:"list"` + Total int64 `json:"total"` +} + +type MetaVersion struct { + Id int64 `json:"id,optional"` + EntityId int64 `json:"entity_id,optional"` + Version int64 `json:"version,optional"` + SnapshotJson string `json:"snapshot_json,optional"` + Operator string `json:"operator,optional"` + PublishedAt int64 `json:"published_at,optional"` +} + +type QuestionsAwardReq struct { + ActivityId int64 `json:"actid"` + AnswerId int64 `json:"answerid"` +} + +type RegisterAppReq struct { + Name string `json:"name"` + RateLimitQps int64 `json:"rate_limit_qps,optional"` + IpWhitelist string `json:"ip_whitelist,optional"` + Beid int64 `json:"beid,optional"` +} + +type RegisterReq struct { + Mobile string `json:"mobile"` //基本一个手机号码就完事 + Password string `json:"password"` + Smscode string `json:"smscode"` //短信码 +} + +type Request struct { + Name string `path:"name,options=you|me"` +} + +type Response struct { + Message string `json:"message"` +} + type SearchReq struct { Keyword string `json:"keyword,optional" form:"keyword,optional"` Page string `json:"page,optional" form:"page,optional"` @@ -171,30 +404,118 @@ type SearchResp struct { Data []ArticleReq `json:"data"` } -type ArticleReq struct { - NewsId string `json:"NewsId"` - NewsTitle string `json:"NewsTitle"` - ImageUrl string `json:"ImageUrl"` +type SnsReq struct { + Beid + Ptyid int64 `json:"ptyid"` //对应平台 + BackUrl string `json:"back_url"` //登陆返回的地址 } -type AnswerReq struct { - ActivityId int64 `json:"actid"` - Answers string `json:"answers"` - Score string `json:"score"` +type SnsResp struct { + Beid + Ptyid int64 `json:"ptyid"` //对应平台 + Appid string `json:"appid"` //sns 平台的id + Title string `json:"title"` //名称 + LoginUrl string `json:"login_url"` //微信登陆的地址 } -type QuestionsAwardReq struct { - ActivityId int64 `json:"actid"` - AnswerId int64 `json:"answerid"` +type TagAssetReq struct { + EntityId int64 `json:"entity_id"` + CategoryId int64 `json:"category_id,optional"` + Name string `json:"name"` + EntityType string `json:"entity_type,optional"` + Comment string `json:"comment,optional"` + Classification string `json:"classification,optional"` + AssetTagsJson string `json:"asset_tags_json,optional"` + Visibility string `json:"visibility,optional"` + Beid int64 `json:"beid,optional"` } -type AnswerResp struct { - Answers string `json:"answers"` - Score string `json:"score"` +type TestConnResp struct { + Ok bool `json:"ok"` + LatencyMs int64 `json:"latency_ms"` + ErrMsg string `json:"err_msg,optional"` } -type AwardConvertReq struct { - UserName string `json:"username"` - Phone string `json:"phone"` - LotteryId int64 `json:"lotteryid"` +type Token struct { + Token string `json:"token"` +} + +type UpdateApiReq struct { + Id int64 `json:"id"` + Beid int64 `json:"beid,optional"` + Name string `json:"name,optional"` + SqlText string `json:"sql_text,optional"` + ParamsJson string `json:"params_json,optional"` + CacheTtl int64 `json:"cache_ttl,optional"` +} + +type UpdateDataSourceReq struct { + Id int64 `json:"id"` + Beid int64 `json:"beid,optional"` + Name string `json:"name,optional"` + Type string `json:"type,optional"` + Host string `json:"host,optional"` + Port int32 `json:"port,optional"` + DbName string `json:"db_name,optional"` + Username string `json:"username,optional"` + Password string `json:"password,optional"` + ParamsJson string `json:"params_json,optional"` + Status int32 `json:"status,optional"` + Owner string `json:"owner,optional"` +} + +type UserReply struct { + Auid int64 `json:"auid"` + Uid int64 `json:"uid"` + Beid int64 `json:"beid"` //应用id + Ptyid int64 `json:"ptyid"` //对应平台 + Username string `json:"username"` + Mobile string `json:"mobile"` + Nickname string `json:"nickname"` + Openid string `json:"openid"` + Avator string `json:"avator"` + JwtToken +} + +type UserReq struct { + Auid int64 `json:"auid,optional" form:"auid,optional"` + Uid int64 `json:"uid,optional" form:"uid,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` //应用id + Ptyid int64 `json:"ptyid,optional" form:"ptyid,optional"` //对应平台 +} + +type VersionListResp struct { + List []MetaVersion `json:"list"` +} + +type VersionResp struct { + Version MetaVersion `json:"version"` +} + +type VoteReq struct { + Aeid int64 `json:"aeid,optional" form:"aeid,optional"` // 作品id + Actid +} + +type VoteResp struct { + VoteReq + Votecount int64 `json:"votecount"` //投票票数 + Viewcount int64 `json:"viewcount"` //浏览数 +} + +type WxLoginReq struct { + Beid int64 `json:"beid"` //应用id + Code string `json:"code"` //微信登陆密钥 + Ptyid int64 `json:"ptyid"` //对应平台 +} + +type WxShareResp struct { + Appid string `json:"appid"` + Timestamp int64 `json:"timestamp"` + Noncestr string `json:"noncestr"` + Signature string `json:"signature"` +} + +type WxTicket struct { + Ticket string `json:"ticket"` } diff --git a/metadata.api b/metadata.api new file mode 100644 index 0000000..85a0db3 --- /dev/null +++ b/metadata.api @@ -0,0 +1,141 @@ +syntax = "v1" + +//元数据管理(Phase 1) +type MetaEntity { + Id int64 `json:"id,optional"` + EntityType string `json:"entity_type,optional"` + Fqn string `json:"fqn,optional"` + Name string `json:"name,optional"` + Comment string `json:"comment,optional"` + Engine string `json:"engine,optional"` + RowsCnt int64 `json:"rows_cnt,optional"` + PropsJson string `json:"props_json,optional"` + Classification string `json:"classification,optional"` + Owner string `json:"owner,optional"` + DsId int64 `json:"ds_id,optional"` + Beid int64 `json:"beid,optional"` +} + +type CollectDatabaseReq { + DsId int64 `json:"ds_id,optional"` + Beid int64 `json:"beid,optional"` + DbType string `json:"db_type,optional"` + Dsn string `json:"dsn,optional"` //为空则按 ds_id 从数据源服务取 + DbName string `json:"db_name,optional"` +} + +type CollectResp { + TablesCreated int64 `json:"tables_created"` + ColumnsCreated int64 `json:"columns_created"` +} + +type MetaEntityIdReq { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type MetaEntityListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + EntityType string `json:"entity_type,optional" form:"entity_type,optional"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + DsId int64 `json:"ds_id,optional" form:"ds_id,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type MetaEntityListResp { + List []MetaEntity `json:"list"` + Total int64 `json:"total"` +} + +type AddLineageReq { + FromId int64 `json:"from_id"` + ToId int64 `json:"to_id"` + PropsJson string `json:"props_json,optional"` + Beid int64 `json:"beid,optional"` +} + +type LineageReq { + EntityId int64 `json:"entity_id,optional" form:"entity_id,optional"` + Direction string `json:"direction,optional" form:"direction,optional"` + Depth int32 `json:"depth,optional" form:"depth,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type LineageNode { + Id int64 `json:"id,optional"` + Fqn string `json:"fqn,optional"` + Name string `json:"name,optional"` +} +type LineageEdge { + FromId int64 `json:"from_id,optional"` + ToId int64 `json:"to_id,optional"` + PropsJson string `json:"props_json,optional"` +} +type LineageResp { + Nodes []LineageNode `json:"nodes"` + Edges []LineageEdge `json:"edges"` +} + +type MetaVersion { + Id int64 `json:"id,optional"` + EntityId int64 `json:"entity_id,optional"` + Version int64 `json:"version,optional"` + SnapshotJson string `json:"snapshot_json,optional"` + Operator string `json:"operator,optional"` + PublishedAt int64 `json:"published_at,optional"` +} +type VersionResp { Version MetaVersion `json:"version"` } +type VersionListResp { List []MetaVersion `json:"list"` } + +type CompareReq { + VersionA int64 `json:"version_a,optional" form:"version_a,optional"` + VersionB int64 `json:"version_b,optional" form:"version_b,optional"` +} +type DiffResp { DiffJson string `json:"diff_json"` } + +@server( + group: metadata +) +service datacenter-api { + @doc( + summary: "采集库表元数据" + ) + @handler collectDatabase + post /metadata/collect (CollectDatabaseReq) returns (CollectResp) + @doc( + summary: "实体详情" + ) + @handler metaEntityInfo + get /metadata/entity/info (MetaEntityIdReq) returns (MetaEntity) + @doc( + summary: "实体列表" + ) + @handler metaEntityList + get /metadata/entity/list (MetaEntityListReq) returns (MetaEntityListResp) + @doc( + summary: "登记血缘" + ) + @handler addLineage + post /metadata/lineage/add (AddLineageReq) + @doc( + summary: "血缘图" + ) + @handler lineage + get /metadata/lineage (LineageReq) returns (LineageResp) + @doc( + summary: "定版快照" + ) + @handler createVersion + post /metadata/version/create (MetaEntityIdReq) returns (VersionResp) + @doc( + summary: "版本列表" + ) + @handler versionList + get /metadata/version/list (MetaEntityIdReq) returns (VersionListResp) + @doc( + summary: "版本对比" + ) + @handler versionCompare + get /metadata/version/compare (CompareReq) returns (DiffResp) +} diff --git a/metadata/model/entitymodel.go b/metadata/model/entitymodel.go new file mode 100644 index 0000000..5ac78a7 --- /dev/null +++ b/metadata/model/entitymodel.go @@ -0,0 +1,300 @@ +package model + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// 实体类型 +const ( + EntityTypeDatabase = "database" + EntityTypeTable = "table" + EntityTypeColumn = "column" +) + +const ( + entityTable = "`dc_meta_entity`" + entityInsertFieldSize = 11 // 插入字段数(与 entityInsertColumns 对应) + entityInsertChunk = 200 // 批量插入分片大小 + fqnQueryChunk = 500 // fqn in 查询分片大小 + idQueryChunk = 500 // id in 查询分片大小 +) + +// entityInsertColumns 插入字段(不含自增 id 与时间列) +const entityInsertColumns = "entity_type,fqn,name,`comment`,engine,rows_cnt,props_json,classification,owner,ds_id,beid" + +// entityRows 查询列(props_json 允许 NULL,用 ifnull 兜底避免扫描报错) +const entityRows = "id,entity_type,fqn,name,`comment`,engine,rows_cnt,ifnull(props_json,'') as props_json,classification,owner,ds_id,beid,created_at,updated_at" + +// entityUpsertClause uk_beid_fqn 冲突时的更新子句: +// 覆盖采集可得的基础字段;classification/owner 为人工维护字段,不做覆盖 +const entityUpsertClause = " on duplicate key update name = values(name), `comment` = values(`comment`), " + + "engine = values(engine), rows_cnt = values(rows_cnt), props_json = values(props_json), ds_id = values(ds_id)" + +// 元数据实体(database/table/column) +type ( + // Entity dc_meta_entity 行 + Entity struct { + Id int64 `db:"id"` + EntityType string `db:"entity_type"` + Fqn string `db:"fqn"` + Name string `db:"name"` + Comment string `db:"comment"` + Engine string `db:"engine"` + RowsCnt int64 `db:"rows_cnt"` + PropsJson string `db:"props_json"` + Classification string `db:"classification"` + Owner string `db:"owner"` + DsId int64 `db:"ds_id"` + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + EntityModel interface { + // Insert 批量写入实体(uk_beid_fqn 冲突时按 upsert 语义更新基础字段) + Insert(ctx context.Context, session sqlx.Session, entities []*Entity) error + // FindOne 按主键查询(beid>0 时追加多租户过滤) + FindOne(ctx context.Context, id, beid int64) (*Entity, error) + // FindOneByFqn 按 beid + 全限定名查询 + FindOneByFqn(ctx context.Context, beid int64, fqn string) (*Entity, error) + // FindList 条件分页查询(entity_type/keyword/ds_id 均可选,keyword 对 fqn/name 模糊匹配) + FindList(ctx context.Context, beid int64, entityType, keyword string, dsId, page, size int64) ([]*Entity, int64, error) + // FindFqnMap 批量查询 fqn -> id 映射(采集建边用) + FindFqnMap(ctx context.Context, session sqlx.Session, beid int64, fqns []string) (map[string]int64, error) + // FindListByIds 按主键批量查询(血缘节点信息回填用,beid>0 时追加多租户过滤) + FindListByIds(ctx context.Context, beid int64, ids []int64) ([]*Entity, error) + // UpdateProps 更新实体扩展属性 + UpdateProps(ctx context.Context, id int64, propsJson string) error + } + + defaultEntityModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewEntityModel 创建元数据实体 model +func NewEntityModel(conn sqlx.SqlConn) EntityModel { + return &defaultEntityModel{ + conn: conn, + table: entityTable, + } +} + +// Insert 批量写入实体:分片拼接多行 values,uk_beid_fqn 冲突时走 upsert 更新而非报错 +func (m *defaultEntityModel) Insert(ctx context.Context, session sqlx.Session, entities []*Entity) error { + if len(entities) == 0 { + return nil + } + prefix := fmt.Sprintf("insert into %s (%s) values ", m.table, entityInsertColumns) + for start := 0; start < len(entities); start += entityInsertChunk { + end := start + entityInsertChunk + if end > len(entities) { + end = len(entities) + } + batch := entities[start:end] + rows := strings.TrimSuffix(strings.Repeat("("+placeholders(entityInsertFieldSize)+"),", len(batch)), ",") + query := prefix + rows + entityUpsertClause + args := make([]any, 0, len(batch)*entityInsertFieldSize) + for _, e := range batch { + args = append(args, e.EntityType, e.Fqn, e.Name, e.Comment, e.Engine, + e.RowsCnt, e.PropsJson, e.Classification, e.Owner, e.DsId, e.Beid) + } + if _, err := execCtx(ctx, m.conn, session, query, args...); err != nil { + return fmt.Errorf("批量写入实体失败: %w", err) + } + } + return nil +} + +// FindOne 按主键查询实体 +func (m *defaultEntityModel) FindOne(ctx context.Context, id, beid int64) (*Entity, error) { + query := fmt.Sprintf("select %s from %s where id = ?", entityRows, m.table) + args := []any{id} + if beid > 0 { + query += " and beid = ?" + args = append(args, beid) + } + var resp Entity + err := m.conn.QueryRowCtx(ctx, &resp, query+" limit 1", args...) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindOneByFqn 按 beid + 全限定名查询实体 +func (m *defaultEntityModel) FindOneByFqn(ctx context.Context, beid int64, fqn string) (*Entity, error) { + if beid <= 0 { + return nil, ErrBeidRequired + } + query := fmt.Sprintf("select %s from %s where beid = ? and fqn = ? limit 1", entityRows, m.table) + var resp Entity + err := m.conn.QueryRowCtx(ctx, &resp, query, beid, fqn) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindList 条件分页查询实体,返回列表与总数 +func (m *defaultEntityModel) FindList(ctx context.Context, beid int64, entityType, keyword string, + dsId, page, size int64) ([]*Entity, int64, error) { + if beid <= 0 { + return nil, 0, ErrBeidRequired + } + where := "beid = ?" + args := []any{beid} + if entityType != "" { + where += " and entity_type = ?" + args = append(args, entityType) + } + if kw := strings.TrimSpace(keyword); kw != "" { + where += " and (fqn like ? or name like ?)" + like := "%" + escapeLike(kw) + "%" + args = append(args, like, like) + } + if dsId > 0 { + where += " and ds_id = ?" + args = append(args, dsId) + } + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) as total from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + listQuery := fmt.Sprintf("select %s from %s where %s order by id desc limit ?, ?", entityRows, m.table, where) + args = append(args, (page-1)*size, size) + var list []*Entity + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} + +// FindFqnMap 批量查询 fqn -> id 映射,fqns 自动去重并分片 +func (m *defaultEntityModel) FindFqnMap(ctx context.Context, session sqlx.Session, + beid int64, fqns []string) (map[string]int64, error) { + if beid <= 0 { + return nil, ErrBeidRequired + } + result := make(map[string]int64, len(fqns)) + if len(fqns) == 0 { + return result, nil + } + + // 去重,避免 in 条件里出现重复占位 + seen := make(map[string]struct{}, len(fqns)) + uniq := make([]string, 0, len(fqns)) + for _, f := range fqns { + if f == "" { + continue + } + if _, ok := seen[f]; ok { + continue + } + seen[f] = struct{}{} + uniq = append(uniq, f) + } + + for start := 0; start < len(uniq); start += fqnQueryChunk { + end := start + fqnQueryChunk + if end > len(uniq) { + end = len(uniq) + } + part := uniq[start:end] + query := fmt.Sprintf("select id,fqn from %s where beid = ? and fqn in (%s)", + m.table, placeholders(len(part))) + args := make([]any, 0, len(part)+1) + args = append(args, beid) + for _, f := range part { + args = append(args, f) + } + // 扫描目标必须是"列数与字段数一致"的轻量结构(go-zero 对少列查询扫宽 struct 会报 not matching destination) + var rows []*idFqnRow + if err := queryRowsCtx(ctx, m.conn, session, &rows, query, args...); err != nil { + return nil, err + } + for _, r := range rows { + result[r.Fqn] = r.Id + } + } + return result, nil +} + +// idFqnRow FindFqnMap 专用扫描结构(与 select id,fqn 列数对齐) +type idFqnRow struct { + Id int64 `db:"id"` + Fqn string `db:"fqn"` +} + +// FindListByIds 按主键批量查询实体(自动去重并分片) +func (m *defaultEntityModel) FindListByIds(ctx context.Context, beid int64, ids []int64) ([]*Entity, error) { + if len(ids) == 0 { + return []*Entity{}, nil + } + + seen := make(map[int64]struct{}, len(ids)) + uniq := make([]int64, 0, len(ids)) + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + uniq = append(uniq, id) + } + + list := make([]*Entity, 0, len(uniq)) + for start := 0; start < len(uniq); start += idQueryChunk { + end := start + idQueryChunk + if end > len(uniq) { + end = len(uniq) + } + part := uniq[start:end] + query := fmt.Sprintf("select %s from %s where id in (%s)", entityRows, m.table, placeholders(len(part))) + args := make([]any, 0, len(part)+1) + for _, id := range part { + args = append(args, id) + } + if beid > 0 { + query += " and beid = ?" + args = append(args, beid) + } + var rows []*Entity + if err := m.conn.QueryRowsCtx(ctx, &rows, query, args...); err != nil { + return nil, err + } + list = append(list, rows...) + } + return list, nil +} + +// UpdateProps 更新实体扩展属性 +func (m *defaultEntityModel) UpdateProps(ctx context.Context, id int64, propsJson string) error { + query := fmt.Sprintf("update %s set props_json = ? where id = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, propsJson, id) + return err +} diff --git a/metadata/model/relationmodel.go b/metadata/model/relationmodel.go new file mode 100644 index 0000000..ca2d5e4 --- /dev/null +++ b/metadata/model/relationmodel.go @@ -0,0 +1,147 @@ +package model + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + relationTable = "`dc_meta_relation`" + relationInsertChunk = 500 // 批量插入分片大小 +) + +// relationInsertColumns 插入字段(不含自增 id 与 created_at) +const relationInsertColumns = "rel_type,from_id,to_id,props_json,beid" + +// relationRows 查询列(props_json 允许 NULL,用 ifnull 兜底避免扫描报错) +const relationRows = "id,rel_type,from_id,to_id,ifnull(props_json,'') as props_json,beid,created_at" + +// 元数据关系(contains/lineage/depends) +type ( + // Relation dc_meta_relation 行 + Relation struct { + Id int64 `db:"id"` + RelType string `db:"rel_type"` + FromId int64 `db:"from_id"` + ToId int64 `db:"to_id"` + PropsJson string `db:"props_json"` + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + } + + RelationModel interface { + // Insert 幂等写入单条关系(uk_edge 冲突时忽略) + Insert(ctx context.Context, session sqlx.Session, rel *Relation) error + // BatchInsert 幂等批量写入关系 + BatchInsert(ctx context.Context, session sqlx.Session, rels []*Relation) error + // FindNeighbors 查询与 entityId 直连的边 + // upstream:以 entityId 为终点(to_id)的边,邻居在 from_id 侧 + // downstream:以 entityId 为起点(from_id)的边,邻居在 to_id 侧 + FindNeighbors(ctx context.Context, relType string, entityId int64, direction string) ([]*Relation, error) + // ListByFrom 按 from_id 查询(relType 为空查全部类型) + ListByFrom(ctx context.Context, relType string, fromId int64) ([]*Relation, error) + // ListByTo 按 to_id 查询(relType 为空查全部类型) + ListByTo(ctx context.Context, relType string, toId int64) ([]*Relation, error) + } + + defaultRelationModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewRelationModel 创建元数据关系 model +func NewRelationModel(conn sqlx.SqlConn) RelationModel { + return &defaultRelationModel{ + conn: conn, + table: relationTable, + } +} + +// Insert 幂等写入单条关系:uk_edge(rel_type,from_id,to_id) 冲突时 insert ignore 跳过 +func (m *defaultRelationModel) Insert(ctx context.Context, session sqlx.Session, rel *Relation) error { + query := fmt.Sprintf("insert ignore into %s (%s) values (?,?,?,?,?)", m.table, relationInsertColumns) + _, err := execCtx(ctx, m.conn, session, query, rel.RelType, rel.FromId, rel.ToId, rel.PropsJson, rel.Beid) + return err +} + +// BatchInsert 幂等批量写入关系:分片拼接多行 values,uk_edge 冲突时忽略 +func (m *defaultRelationModel) BatchInsert(ctx context.Context, session sqlx.Session, rels []*Relation) error { + if len(rels) == 0 { + return nil + } + prefix := fmt.Sprintf("insert ignore into %s (%s) values ", m.table, relationInsertColumns) + for start := 0; start < len(rels); start += relationInsertChunk { + end := start + relationInsertChunk + if end > len(rels) { + end = len(rels) + } + batch := rels[start:end] + rows := strings.TrimSuffix(strings.Repeat("("+placeholders(5)+"),", len(batch)), ",") + query := prefix + rows + args := make([]any, 0, len(batch)*5) + for _, r := range batch { + args = append(args, r.RelType, r.FromId, r.ToId, r.PropsJson, r.Beid) + } + if _, err := execCtx(ctx, m.conn, session, query, args...); err != nil { + return fmt.Errorf("批量写入关系失败: %w", err) + } + } + return nil +} + +// FindNeighbors 查询与 entityId 直连的边(血缘 BFS 逐层展开用) +func (m *defaultRelationModel) FindNeighbors(ctx context.Context, relType string, + entityId int64, direction string) ([]*Relation, error) { + var where string + switch direction { + case DirectionUpstream: + // 上游:当前实体是边的终点,邻居在 from_id 侧 + where = "to_id = ?" + case DirectionDownstream: + // 下游:当前实体是边的起点,邻居在 to_id 侧 + where = "from_id = ?" + default: + return nil, ErrInvalidDirection + } + query := fmt.Sprintf("select %s from %s where rel_type = ? and %s", relationRows, m.table, where) + var list []*Relation + if err := m.conn.QueryRowsCtx(ctx, &list, query, relType, entityId); err != nil { + return nil, err + } + return list, nil +} + +// ListByFrom 按 from_id 查询出边(relType 为空查全部类型) +func (m *defaultRelationModel) ListByFrom(ctx context.Context, relType string, fromId int64) ([]*Relation, error) { + query := fmt.Sprintf("select %s from %s where from_id = ?", relationRows, m.table) + args := []any{fromId} + if relType != "" { + query += " and rel_type = ?" + args = append(args, relType) + } + var list []*Relation + if err := m.conn.QueryRowsCtx(ctx, &list, query, args...); err != nil { + return nil, err + } + return list, nil +} + +// ListByTo 按 to_id 查询入边(relType 为空查全部类型) +func (m *defaultRelationModel) ListByTo(ctx context.Context, relType string, toId int64) ([]*Relation, error) { + query := fmt.Sprintf("select %s from %s where to_id = ?", relationRows, m.table) + args := []any{toId} + if relType != "" { + query += " and rel_type = ?" + args = append(args, relType) + } + var list []*Relation + if err := m.conn.QueryRowsCtx(ctx, &list, query, args...); err != nil { + return nil, err + } + return list, nil +} diff --git a/metadata/model/vars.go b/metadata/model/vars.go new file mode 100644 index 0000000..89c6651 --- /dev/null +++ b/metadata/model/vars.go @@ -0,0 +1,65 @@ +package model + +import ( + "context" + "database/sql" + "errors" + "strings" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// 通用错误 +var ( + // ErrNotFound 记录不存在 + ErrNotFound = sqlx.ErrNotFound + // ErrBeidRequired 多租户 beid 缺失 + ErrBeidRequired = errors.New("beid 不能为空") + // ErrInvalidDirection 血缘遍历方向非法(仅支持 upstream/downstream) + ErrInvalidDirection = errors.New("direction 仅支持 upstream/downstream") +) + +// 关系类型 +const ( + RelTypeContains = "contains" // 包含关系:database->table、table->column + RelTypeLineage = "lineage" // 血缘关系 + RelTypeDepends = "depends" // 依赖关系 +) + +// 血缘遍历方向 +const ( + DirectionUpstream = "upstream" // 上游:沿 from_id -> to_id 反向找 + DirectionDownstream = "downstream" // 下游:沿 from_id -> to_id 正向找 +) + +// DefaultClassification 实体默认分级(L2 内部) +const DefaultClassification = "L2" + +// execCtx 优先在事务 session 上执行写语句,session 为空时退回普通连接 +func execCtx(ctx context.Context, conn sqlx.SqlConn, session sqlx.Session, query string, args ...any) (sql.Result, error) { + if session != nil { + return session.ExecCtx(ctx, query, args...) + } + return conn.ExecCtx(ctx, query, args...) +} + +// queryRowsCtx 优先在事务 session 上执行列表查询,session 为空时退回普通连接 +func queryRowsCtx(ctx context.Context, conn sqlx.SqlConn, session sqlx.Session, v any, query string, args ...any) error { + if session != nil { + return session.QueryRowsCtx(ctx, v, query, args...) + } + return conn.QueryRowsCtx(ctx, v, query, args...) +} + +// placeholders 生成 n 个 ? 占位符(逗号分隔) +func placeholders(n int) string { + return strings.TrimSuffix(strings.Repeat("?,", n), ",") +} + +// escapeLike 转义 like 通配符(%/_/\),防止用户输入干扰模糊匹配 +func escapeLike(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `%`, `\%`) + s = strings.ReplaceAll(s, `_`, `\_`) + return s +} diff --git a/metadata/model/versionmodel.go b/metadata/model/versionmodel.go new file mode 100644 index 0000000..b377fa0 --- /dev/null +++ b/metadata/model/versionmodel.go @@ -0,0 +1,106 @@ +package model + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + versionTable = "`dc_meta_version`" +) + +// versionInsertColumns 插入字段 +const versionInsertColumns = "entity_id,version,snapshot_json,operator,published_at" + +// versionRows 查询列(snapshot_json 允许 NULL,用 ifnull 兜底避免扫描报错) +const versionRows = "id,entity_id,version,ifnull(snapshot_json,'') as snapshot_json,operator,published_at" + +// 元数据版本(定版快照) +type ( + // Version dc_meta_version 行 + Version struct { + Id int64 `db:"id"` + EntityId int64 `db:"entity_id"` + Version int64 `db:"version"` + SnapshotJson string `db:"snapshot_json"` + Operator string `db:"operator"` + PublishedAt time.Time `db:"published_at"` + } + + VersionModel interface { + // Insert 写入版本快照,返回新版本 id + Insert(ctx context.Context, v *Version) (int64, error) + // FindOne 按主键查询版本 + FindOne(ctx context.Context, id int64) (*Version, error) + // FindList 按实体查询版本列表(版本号倒序) + FindList(ctx context.Context, entityId int64) ([]*Version, error) + // MaxVersion 查询实体当前最大版本号(无版本时返回 0) + MaxVersion(ctx context.Context, entityId int64) (int64, error) + } + + defaultVersionModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewVersionModel 创建元数据版本 model +func NewVersionModel(conn sqlx.SqlConn) VersionModel { + return &defaultVersionModel{ + conn: conn, + table: versionTable, + } +} + +// Insert 写入版本快照 +func (m *defaultVersionModel) Insert(ctx context.Context, v *Version) (int64, error) { + query := fmt.Sprintf("insert into %s (%s) values (?,?,?,?,?)", m.table, versionInsertColumns) + ret, err := m.conn.ExecCtx(ctx, query, v.EntityId, v.Version, v.SnapshotJson, v.Operator, v.PublishedAt) + if err != nil { + return 0, err + } + id, err := ret.LastInsertId() + if err != nil { + return 0, err + } + return id, nil +} + +// FindOne 按主键查询版本 +func (m *defaultVersionModel) FindOne(ctx context.Context, id int64) (*Version, error) { + query := fmt.Sprintf("select %s from %s where id = ? limit 1", versionRows, m.table) + var resp Version + err := m.conn.QueryRowCtx(ctx, &resp, query, id) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindList 按实体查询版本列表(版本号倒序) +func (m *defaultVersionModel) FindList(ctx context.Context, entityId int64) ([]*Version, error) { + query := fmt.Sprintf("select %s from %s where entity_id = ? order by version desc", versionRows, m.table) + var list []*Version + if err := m.conn.QueryRowsCtx(ctx, &list, query, entityId); err != nil { + return nil, err + } + return list, nil +} + +// MaxVersion 查询实体当前最大版本号,无版本时返回 0 +func (m *defaultVersionModel) MaxVersion(ctx context.Context, entityId int64) (int64, error) { + query := fmt.Sprintf("select ifnull(max(version),0) as max_version from %s where entity_id = ?", m.table) + var maxVersion int64 + if err := m.conn.QueryRowCtx(ctx, &maxVersion, query, entityId); err != nil { + return 0, err + } + return maxVersion, nil +} diff --git a/metadata/rpc/internal/logic/addLineageLogic.go b/metadata/rpc/internal/logic/addLineageLogic.go index 7f46c8b..b92fec4 100644 --- a/metadata/rpc/internal/logic/addLineageLogic.go +++ b/metadata/rpc/internal/logic/addLineageLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/metadata/model" "datacenter/metadata/rpc/internal/svc" "datacenter/metadata/rpc/metadata" @@ -25,7 +26,19 @@ func NewAddLineageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddLin // 登记血缘 func (l *AddLineageLogic) AddLineage(in *metadata.AddLineageReq) (*metadata.OkReply, error) { - // todo: add your logic here and delete this line - - return &metadata.OkReply{}, nil + if in.FromId <= 0 || in.ToId <= 0 { + return nil, ErrInvalidLineageParams + } + // rel_type=lineage,uk_edge(rel_type,from_id,to_id) 冲突忽略,重复登记幂等 + err := l.svcCtx.RelationModel.Insert(l.ctx, nil, &model.Relation{ + RelType: model.RelTypeLineage, + FromId: in.FromId, + ToId: in.ToId, + PropsJson: in.PropsJson, + Beid: in.Beid, + }) + if err != nil { + return nil, err + } + return &metadata.OkReply{Ok: true}, nil } diff --git a/metadata/rpc/internal/logic/collectDatabaseLogic.go b/metadata/rpc/internal/logic/collectDatabaseLogic.go index 6613f83..a2e3f23 100644 --- a/metadata/rpc/internal/logic/collectDatabaseLogic.go +++ b/metadata/rpc/internal/logic/collectDatabaseLogic.go @@ -2,13 +2,46 @@ package logic import ( "context" + "encoding/json" + "fmt" + "datacenter/metadata/model" "datacenter/metadata/rpc/internal/svc" "datacenter/metadata/rpc/metadata" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) +// infoTable information_schema.TABLES 查询行 +type infoTable struct { + TableName string `db:"TABLE_NAME"` + TableComment string `db:"TABLE_COMMENT"` + Engine string `db:"ENGINE"` + TableRows int64 `db:"TABLE_ROWS"` +} + +// infoColumn information_schema.COLUMNS 查询行 +type infoColumn struct { + TableName string `db:"TABLE_NAME"` + ColumnName string `db:"COLUMN_NAME"` + ColumnComment string `db:"COLUMN_COMMENT"` + ColumnType string `db:"COLUMN_TYPE"` + IsNullable string `db:"IS_NULLABLE"` +} + +// columnProps 列实体的扩展属性 +type columnProps struct { + ColumnType string `json:"column_type"` + Nullable bool `json:"nullable"` +} + +// columnItem 列实体及其所属表的 fqn(建 table->column contains 边用) +type columnItem struct { + entity *model.Entity + tableFqn string +} + type CollectDatabaseLogic struct { ctx context.Context svcCtx *svc.ServiceContext @@ -25,7 +58,185 @@ func NewCollectDatabaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *C // 采集库表元数据(含 contains 关系) func (l *CollectDatabaseLogic) CollectDatabase(in *metadata.CollectDatabaseReq) (*metadata.CollectReply, error) { - // todo: add your logic here and delete this line + // 1. 参数校验(DSN 由网关编排解密后传入,本服务不做加解密) + if in.DbType == "" || in.Dsn == "" || in.DbName == "" || in.Beid <= 0 { + return nil, ErrInvalidCollectParams + } + + // 2. 直连目标库读取 information_schema + tables, columns, err := readInformationSchema(l.ctx, in.Dsn, in.DbName) + if err != nil { + return nil, err + } + + // 3. 组装实体(fqn 规则:{db_type}://{db}[.{table}[.{column}]]) + dbFqn := fmt.Sprintf("%s://%s", in.DbType, in.DbName) + dbEntity := &model.Entity{ + EntityType: model.EntityTypeDatabase, + Fqn: dbFqn, + Name: in.DbName, + Classification: model.DefaultClassification, + DsId: in.DsId, + Beid: in.Beid, + } + tableEntities := make([]*model.Entity, 0, len(tables)) + for _, tb := range tables { + tableEntities = append(tableEntities, &model.Entity{ + EntityType: model.EntityTypeTable, + Fqn: fmt.Sprintf("%s.%s", dbFqn, tb.TableName), + Name: tb.TableName, + Comment: tb.TableComment, + Engine: tb.Engine, + RowsCnt: tb.TableRows, + Classification: model.DefaultClassification, + DsId: in.DsId, + Beid: in.Beid, + }) + } + columnItems := make([]columnItem, 0, len(columns)) + for _, c := range columns { + tableFqn := fmt.Sprintf("%s.%s", dbFqn, c.TableName) + props, err := json.Marshal(columnProps{ColumnType: c.ColumnType, Nullable: c.IsNullable == "YES"}) + if err != nil { + return nil, fmt.Errorf("列属性序列化失败: %w", err) + } + columnItems = append(columnItems, columnItem{ + tableFqn: tableFqn, + entity: &model.Entity{ + EntityType: model.EntityTypeColumn, + Fqn: fmt.Sprintf("%s.%s", tableFqn, c.ColumnName), + Name: c.ColumnName, + Comment: c.ColumnComment, + PropsJson: string(props), + Classification: model.DefaultClassification, + DsId: in.DsId, + Beid: in.Beid, + }, + }) + } + + // 4. 单事务写入:实体 upsert + contains 边幂等写入 + var tablesCreated, columnsCreated int64 + err = l.svcCtx.Conn.TransactCtx(l.ctx, func(txCtx context.Context, session sqlx.Session) error { + columnEntities := make([]*model.Entity, len(columnItems)) + allFqns := make([]string, 0, 1+len(tableEntities)+len(columnItems)) + allFqns = append(allFqns, dbFqn) + for _, te := range tableEntities { + allFqns = append(allFqns, te.Fqn) + } + for i, item := range columnItems { + columnEntities[i] = item.entity + allFqns = append(allFqns, item.entity.Fqn) + } + + // 4.1 预取写前已存在的实体,用于统计本次"新增"数 + before, err := l.svcCtx.EntityModel.FindFqnMap(txCtx, session, in.Beid, allFqns) + if err != nil { + return err + } + + // 4.2 写入实体:database 1 条 + table/column 批量 + // uk_beid_fqn 冲突时按 upsert 语义更新 comment/rows_cnt 等基础字段,不报错 + if err := l.svcCtx.EntityModel.Insert(txCtx, session, []*model.Entity{dbEntity}); err != nil { + return err + } + if len(tableEntities) > 0 { + if err := l.svcCtx.EntityModel.Insert(txCtx, session, tableEntities); err != nil { + return err + } + } + if len(columnEntities) > 0 { + if err := l.svcCtx.EntityModel.Insert(txCtx, session, columnEntities); err != nil { + return err + } + } + + // 4.3 回读 id 映射,构造 contains 边:database->table、table->column + after, err := l.svcCtx.EntityModel.FindFqnMap(txCtx, session, in.Beid, allFqns) + if err != nil { + return err + } + dbId, ok := after[dbFqn] + if !ok { + return fmt.Errorf("database 实体写入后未找到: %s", dbFqn) + } + edges := make([]*model.Relation, 0, len(tableEntities)+len(columnItems)) + for _, te := range tableEntities { + if id, ok := after[te.Fqn]; ok { + edges = append(edges, &model.Relation{ + RelType: model.RelTypeContains, + FromId: dbId, + ToId: id, + Beid: in.Beid, + }) + } + } + for _, item := range columnItems { + id, ok := after[item.entity.Fqn] + if !ok { + continue + } + if tableId, ok := after[item.tableFqn]; ok { + edges = append(edges, &model.Relation{ + RelType: model.RelTypeContains, + FromId: tableId, + ToId: id, + Beid: in.Beid, + }) + } + } + if len(edges) > 0 { + // uk_edge 冲突忽略,重复采集幂等 + if err := l.svcCtx.RelationModel.BatchInsert(txCtx, session, edges); err != nil { + return err + } + } + + // 4.4 统计本次新增数(写前不存在、写后存在的 fqn 即为新建) + for _, te := range tableEntities { + if _, ok := before[te.Fqn]; !ok { + tablesCreated++ + } + } + for _, ce := range columnEntities { + if _, ok := before[ce.Fqn]; !ok { + columnsCreated++ + } + } + return nil + }) + if err != nil { + return nil, err + } - return &metadata.CollectReply{}, nil + l.Infof("[CollectDatabase] beid=%d ds_id=%d db_type=%s db=%s 表=%d(新增%d) 列=%d(新增%d)", + in.Beid, in.DsId, in.DbType, in.DbName, len(tables), tablesCreated, len(columns), columnsCreated) + + return &metadata.CollectReply{ + TablesCreated: tablesCreated, + ColumnsCreated: columnsCreated, + }, nil +} + +// readInformationSchema 直连目标库读取库下全部表与列的元信息 +func readInformationSchema(ctx context.Context, dsn, dbName string) ([]*infoTable, []*infoColumn, error) { + conn := sqlx.NewMysql(dsn) + + var tables []*infoTable + // NULL 值兜底:视图行 ENGINE/TABLE_ROWS 可能为 NULL + tableQuery := "select TABLE_NAME, ifnull(TABLE_COMMENT,'') as TABLE_COMMENT, " + + "ifnull(ENGINE,'') as ENGINE, ifnull(TABLE_ROWS,0) as TABLE_ROWS " + + "from information_schema.TABLES where TABLE_SCHEMA = ?" + if err := conn.QueryRowsPartialCtx(ctx, &tables, tableQuery, dbName); err != nil { + return nil, nil, fmt.Errorf("查询 information_schema.TABLES 失败: %w", err) + } + + var columns []*infoColumn + columnQuery := "select TABLE_NAME, COLUMN_NAME, ifnull(COLUMN_COMMENT,'') as COLUMN_COMMENT, " + + "COLUMN_TYPE, ifnull(IS_NULLABLE,'') as IS_NULLABLE " + + "from information_schema.COLUMNS where TABLE_SCHEMA = ? order by TABLE_NAME, ORDINAL_POSITION" + if err := conn.QueryRowsPartialCtx(ctx, &columns, columnQuery, dbName); err != nil { + return nil, nil, fmt.Errorf("查询 information_schema.COLUMNS 失败: %w", err) + } + return tables, columns, nil } diff --git a/metadata/rpc/internal/logic/compareVersionsLogic.go b/metadata/rpc/internal/logic/compareVersionsLogic.go index 46e2617..8fd381f 100644 --- a/metadata/rpc/internal/logic/compareVersionsLogic.go +++ b/metadata/rpc/internal/logic/compareVersionsLogic.go @@ -2,7 +2,11 @@ package logic import ( "context" + "encoding/json" + "errors" + "fmt" + "datacenter/metadata/model" "datacenter/metadata/rpc/internal/svc" "datacenter/metadata/rpc/metadata" @@ -25,7 +29,37 @@ func NewCompareVersionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *C // 版本对比 func (l *CompareVersionsLogic) CompareVersions(in *metadata.CompareReq) (*metadata.DiffReply, error) { - // todo: add your logic here and delete this line + // 1. 加载两版快照(version_a 为 before,version_b 为 after) + va, err := l.svcCtx.VersionModel.FindOne(l.ctx, in.VersionA) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrVersionNotFound + } + return nil, err + } + vb, err := l.svcCtx.VersionModel.FindOne(l.ctx, in.VersionB) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrVersionNotFound + } + return nil, err + } + + // 2. 解析快照 JSON + sa, err := parseEntitySnapshot(va.SnapshotJson) + if err != nil { + return nil, fmt.Errorf("版本 %d 快照解析失败: %w", va.Id, err) + } + sb, err := parseEntitySnapshot(vb.SnapshotJson) + if err != nil { + return nil, fmt.Errorf("版本 %d 快照解析失败: %w", vb.Id, err) + } - return &metadata.DiffReply{}, nil + // 3. 逐字段对比(name/comment/engine/rows_cnt/props_json/classification,纯函数见 snapshot.go) + diffs := diffEntitySnapshot(sa, sb) + data, err := json.Marshal(diffs) + if err != nil { + return nil, err + } + return &metadata.DiffReply{DiffJson: string(data)}, nil } diff --git a/metadata/rpc/internal/logic/convert.go b/metadata/rpc/internal/logic/convert.go new file mode 100644 index 0000000..4feb089 --- /dev/null +++ b/metadata/rpc/internal/logic/convert.go @@ -0,0 +1,36 @@ +package logic + +import ( + "datacenter/metadata/model" + "datacenter/metadata/rpc/metadata" +) + +// entityToPb 实体 model -> pb +func entityToPb(e *model.Entity) *metadata.MetaEntity { + return &metadata.MetaEntity{ + Id: e.Id, + EntityType: e.EntityType, + Fqn: e.Fqn, + Name: e.Name, + Comment: e.Comment, + Engine: e.Engine, + RowsCnt: e.RowsCnt, + PropsJson: e.PropsJson, + Classification: e.Classification, + Owner: e.Owner, + DsId: e.DsId, + Beid: e.Beid, + } +} + +// versionToPb 版本 model -> pb(published_at 转毫秒时间戳) +func versionToPb(v *model.Version) *metadata.MetaVersion { + return &metadata.MetaVersion{ + Id: v.Id, + EntityId: v.EntityId, + Version: v.Version, + SnapshotJson: v.SnapshotJson, + Operator: v.Operator, + PublishedAt: v.PublishedAt.UnixMilli(), + } +} diff --git a/metadata/rpc/internal/logic/createVersionLogic.go b/metadata/rpc/internal/logic/createVersionLogic.go index dd4ed1c..d87c41b 100644 --- a/metadata/rpc/internal/logic/createVersionLogic.go +++ b/metadata/rpc/internal/logic/createVersionLogic.go @@ -2,7 +2,11 @@ package logic import ( "context" + "encoding/json" + "errors" + "time" + "datacenter/metadata/model" "datacenter/metadata/rpc/internal/svc" "datacenter/metadata/rpc/metadata" @@ -25,7 +29,41 @@ func NewCreateVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Cre // 定版快照 func (l *CreateVersionLogic) CreateVersion(in *metadata.IdReq) (*metadata.VersionReply, error) { - // todo: add your logic here and delete this line + if in.Id <= 0 { + return nil, ErrEntityNotFound + } + // 1. 加载实体当前状态 + entity, err := l.svcCtx.EntityModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrEntityNotFound + } + return nil, err + } + + // 2. 版本号 = 当前最大版本 + 1(实体内自增,uk_entity_version 保证唯一) + maxVersion, err := l.svcCtx.VersionModel.MaxVersion(l.ctx, entity.Id) + if err != nil { + return nil, err + } + + // 3. 快照:实体当前字段 + props 序列化为 JSON + snapshot, err := json.Marshal(newEntitySnapshot(entity)) + if err != nil { + return nil, err + } + v := &model.Version{ + EntityId: entity.Id, + Version: maxVersion + 1, + SnapshotJson: string(snapshot), + PublishedAt: time.Now(), + } + id, err := l.svcCtx.VersionModel.Insert(l.ctx, v) + if err != nil { + return nil, err + } + v.Id = id - return &metadata.VersionReply{}, nil + l.Infof("[CreateVersion] entity_id=%d version=%d", v.EntityId, v.Version) + return &metadata.VersionReply{Version: versionToPb(v)}, nil } diff --git a/metadata/rpc/internal/logic/error.go b/metadata/rpc/internal/logic/error.go new file mode 100644 index 0000000..1444e72 --- /dev/null +++ b/metadata/rpc/internal/logic/error.go @@ -0,0 +1,17 @@ +package logic + +import "errors" + +// 业务错误(对齐 votes 服务写法:errors.New 中文语义错误) +var ( + // ErrInvalidCollectParams 采集入参缺失(db_type/dsn/db_name/beid) + ErrInvalidCollectParams = errors.New("采集参数不完整:db_type/dsn/db_name/beid 均不能为空") + // ErrEntityNotFound 实体不存在 + ErrEntityNotFound = errors.New("实体不存在") + // ErrInvalidLineageParams 血缘起点/终点非法 + ErrInvalidLineageParams = errors.New("血缘起点/终点实体id不能为空") + // ErrInvalidDirection 血缘方向非法 + ErrInvalidDirection = errors.New("direction 仅支持 upstream/downstream") + // ErrVersionNotFound 版本不存在 + ErrVersionNotFound = errors.New("版本不存在") +) diff --git a/metadata/rpc/internal/logic/getEntityLogic.go b/metadata/rpc/internal/logic/getEntityLogic.go index 98f9a12..7c51c4d 100644 --- a/metadata/rpc/internal/logic/getEntityLogic.go +++ b/metadata/rpc/internal/logic/getEntityLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "errors" + "datacenter/metadata/model" "datacenter/metadata/rpc/internal/svc" "datacenter/metadata/rpc/metadata" @@ -25,7 +27,15 @@ func NewGetEntityLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetEnti // 实体详情 func (l *GetEntityLogic) GetEntity(in *metadata.IdReq) (*metadata.EntityReply, error) { - // todo: add your logic here and delete this line - - return &metadata.EntityReply{}, nil + if in.Id <= 0 { + return nil, ErrEntityNotFound + } + entity, err := l.svcCtx.EntityModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrEntityNotFound + } + return nil, err + } + return &metadata.EntityReply{Entity: entityToPb(entity)}, nil } diff --git a/metadata/rpc/internal/logic/getLineageLogic.go b/metadata/rpc/internal/logic/getLineageLogic.go index 582fb40..99794b8 100644 --- a/metadata/rpc/internal/logic/getLineageLogic.go +++ b/metadata/rpc/internal/logic/getLineageLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/metadata/model" "datacenter/metadata/rpc/internal/svc" "datacenter/metadata/rpc/metadata" @@ -25,7 +26,54 @@ func NewGetLineageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLin // 血缘图(上下游 N 跳) func (l *GetLineageLogic) GetLineage(in *metadata.LineageReq) (*metadata.LineageReply, error) { - // todo: add your logic here and delete this line + // 1. 参数校验 + if in.EntityId <= 0 { + return nil, ErrInvalidLineageParams + } + direction := in.Direction + if direction != model.DirectionUpstream && direction != model.DirectionDownstream { + return nil, ErrInvalidDirection + } + depth := int(in.Depth) + + // 2. BFS 逐层展开(遍历逻辑抽为纯函数 traverseLineage,见 lineage.go 及其单测) + next := func(id int64) ([]LineageEdgeData, error) { + rels, err := l.svcCtx.RelationModel.FindNeighbors(l.ctx, model.RelTypeLineage, id, direction) + if err != nil { + return nil, err + } + edges := make([]LineageEdgeData, 0, len(rels)) + for _, r := range rels { + edges = append(edges, LineageEdgeData{FromId: r.FromId, ToId: r.ToId, PropsJson: r.PropsJson}) + } + return edges, nil + } + nodeIds, edgeData, err := traverseLineage(in.EntityId, direction, depth, next) + if err != nil { + return nil, err + } - return &metadata.LineageReply{}, nil + // 3. 回填节点展示信息(fqn/name),不存在的实体(如跨租户)跳过 + entities, err := l.svcCtx.EntityModel.FindListByIds(l.ctx, in.Beid, nodeIds) + if err != nil { + return nil, err + } + entityMap := make(map[int64]*model.Entity, len(entities)) + for _, e := range entities { + entityMap[e.Id] = e + } + nodes := make([]*metadata.LineageNode, 0, len(nodeIds)) + for _, id := range nodeIds { + e, ok := entityMap[id] + if !ok { + continue + } + nodes = append(nodes, &metadata.LineageNode{Id: e.Id, Fqn: e.Fqn, Name: e.Name}) + } + + edges := make([]*metadata.LineageEdge, 0, len(edgeData)) + for _, ed := range edgeData { + edges = append(edges, &metadata.LineageEdge{FromId: ed.FromId, ToId: ed.ToId, PropsJson: ed.PropsJson}) + } + return &metadata.LineageReply{Nodes: nodes, Edges: edges}, nil } diff --git a/metadata/rpc/internal/logic/lineage.go b/metadata/rpc/internal/logic/lineage.go new file mode 100644 index 0000000..87b3a6d --- /dev/null +++ b/metadata/rpc/internal/logic/lineage.go @@ -0,0 +1,78 @@ +package logic + +import ( + "strconv" + + "datacenter/metadata/model" +) + +// LineageEdgeData 血缘边的最小数据结构(与存储层解耦,便于纯函数单测) +type LineageEdgeData struct { + FromId int64 + ToId int64 + PropsJson string +} + +// defaultLineageDepth 血缘遍历默认深度(depth<=0 时按 1 跳处理) +const defaultLineageDepth = 1 + +// traverseLineage 从 start 出发按 direction 做 BFS,最多 depth 层。 +// 参数 next(id) 返回与 id 直连的边: +// - upstream:以 id 为终点(to_id)的边,沿 from_id 反向继续; +// - downstream:以 id 为起点(from_id)的边,沿 to_id 正向继续。 +// +// 返回: +// - nodes:去重后的节点 id(含起点,按访问顺序); +// - edges:途经的边(同一条边只输出一次); +// +// visited 集合天然完成环检测,环路不会导致死循环,也不会重复输出节点/边。 +func traverseLineage(start int64, direction string, depth int, + next func(int64) ([]LineageEdgeData, error)) ([]int64, []LineageEdgeData, error) { + if depth <= 0 { + depth = defaultLineageDepth + } + visited := map[int64]struct{}{start: {}} + nodes := []int64{start} + edges := make([]LineageEdgeData, 0) + edgeSeen := make(map[string]struct{}) + frontier := []int64{start} + + for level := 0; level < depth && len(frontier) > 0; level++ { + nextFrontier := make([]int64, 0, len(frontier)) + for _, cur := range frontier { + neighbors, err := next(cur) + if err != nil { + return nil, nil, err + } + for _, e := range neighbors { + // 同一条边只处理一次(防止邻接数据重复输出) + key := edgeKey(e.FromId, e.ToId) + if _, ok := edgeSeen[key]; ok { + continue + } + edgeSeen[key] = struct{}{} + edges = append(edges, e) + + // 计算下一跳节点:upstream 取边的起点,downstream 取边的终点 + nextId := e.ToId + if direction == model.DirectionUpstream { + nextId = e.FromId + } + // 环检测:已访问节点不再入队 + if _, ok := visited[nextId]; ok { + continue + } + visited[nextId] = struct{}{} + nodes = append(nodes, nextId) + nextFrontier = append(nextFrontier, nextId) + } + } + frontier = nextFrontier + } + return nodes, edges, nil +} + +// edgeKey 边去重键:from->to +func edgeKey(from, to int64) string { + return strconv.FormatInt(from, 10) + "->" + strconv.FormatInt(to, 10) +} diff --git a/metadata/rpc/internal/logic/lineage_test.go b/metadata/rpc/internal/logic/lineage_test.go new file mode 100644 index 0000000..487cae2 --- /dev/null +++ b/metadata/rpc/internal/logic/lineage_test.go @@ -0,0 +1,183 @@ +package logic + +import ( + "errors" + "reflect" + "testing" + + "datacenter/metadata/model" +) + +// memoryGraph 内存边集合(单测用,替代存储层) +type memoryGraph struct { + edges []LineageEdgeData +} + +// next 构造与 traverseLineage 匹配的邻居查询闭包(模拟 RelationModel.FindNeighbors) +func (g *memoryGraph) next(direction string) func(int64) ([]LineageEdgeData, error) { + return func(id int64) ([]LineageEdgeData, error) { + out := make([]LineageEdgeData, 0) + for _, e := range g.edges { + switch direction { + case model.DirectionDownstream: + if e.FromId == id { + out = append(out, e) + } + case model.DirectionUpstream: + if e.ToId == id { + out = append(out, e) + } + } + } + return out, nil + } +} + +// mkEdge 构造一条血缘边 +func mkEdge(from, to int64) LineageEdgeData { + return LineageEdgeData{FromId: from, ToId: to, PropsJson: ""} +} + +// collectEdgeKeys 把边列表转成 "from->to" 键集合,便于断言 +func collectEdgeKeys(edges []LineageEdgeData) map[string]struct{} { + keys := make(map[string]struct{}, len(edges)) + for _, e := range edges { + keys[edgeKey(e.FromId, e.ToId)] = struct{}{} + } + return keys +} + +// 测试下游多跳:1->2->3->4,depth=2 时只展开到第 2 跳 +func TestTraverseLineageDownstreamMultiHop(t *testing.T) { + g := &memoryGraph{edges: []LineageEdgeData{mkEdge(1, 2), mkEdge(2, 3), mkEdge(3, 4)}} + + nodes, edges, err := traverseLineage(1, model.DirectionDownstream, 2, g.next(model.DirectionDownstream)) + if err != nil { + t.Fatalf("traverseLineage error: %v", err) + } + if !reflect.DeepEqual(nodes, []int64{1, 2, 3}) { + t.Fatalf("nodes = %v, want [1 2 3]", nodes) + } + want := map[string]struct{}{"1->2": {}, "2->3": {}} + if !reflect.DeepEqual(collectEdgeKeys(edges), want) { + t.Fatalf("edges = %v, want %v", edges, want) + } +} + +// 测试深度足够时走完整条链 +func TestTraverseLineageDownstreamFullChain(t *testing.T) { + g := &memoryGraph{edges: []LineageEdgeData{mkEdge(1, 2), mkEdge(2, 3), mkEdge(3, 4)}} + + nodes, _, err := traverseLineage(1, model.DirectionDownstream, 10, g.next(model.DirectionDownstream)) + if err != nil { + t.Fatalf("traverseLineage error: %v", err) + } + if !reflect.DeepEqual(nodes, []int64{1, 2, 3, 4}) { + t.Fatalf("nodes = %v, want [1 2 3 4]", nodes) + } +} + +// 测试上游反向遍历:1->2->3->4,从 4 出发沿 from_id 反向找 +func TestTraverseLineageUpstreamMultiHop(t *testing.T) { + g := &memoryGraph{edges: []LineageEdgeData{mkEdge(1, 2), mkEdge(2, 3), mkEdge(3, 4)}} + + nodes, edges, err := traverseLineage(4, model.DirectionUpstream, 2, g.next(model.DirectionUpstream)) + if err != nil { + t.Fatalf("traverseLineage error: %v", err) + } + if !reflect.DeepEqual(nodes, []int64{4, 3, 2}) { + t.Fatalf("nodes = %v, want [4 3 2]", nodes) + } + want := map[string]struct{}{"3->4": {}, "2->3": {}} + if !reflect.DeepEqual(collectEdgeKeys(edges), want) { + t.Fatalf("edges = %v, want %v", edges, want) + } +} + +// 测试环检测:1->2->3->1 成环时不能死循环,且节点/边不重复输出 +func TestTraverseLineageCycle(t *testing.T) { + g := &memoryGraph{edges: []LineageEdgeData{mkEdge(1, 2), mkEdge(2, 3), mkEdge(3, 1)}} + + nodes, edges, err := traverseLineage(1, model.DirectionDownstream, 10, g.next(model.DirectionDownstream)) + if err != nil { + t.Fatalf("traverseLineage error: %v", err) + } + if !reflect.DeepEqual(nodes, []int64{1, 2, 3}) { + t.Fatalf("nodes = %v, want [1 2 3]", nodes) + } + if len(edges) != 3 { + t.Fatalf("edges = %v, want 3 edges", edges) + } +} + +// 测试菱形去重:1->2、1->3、2->4、3->4,节点 4 只出现一次 +func TestTraverseLineageDiamondDedup(t *testing.T) { + g := &memoryGraph{edges: []LineageEdgeData{mkEdge(1, 2), mkEdge(1, 3), mkEdge(2, 4), mkEdge(3, 4)}} + + nodes, edges, err := traverseLineage(1, model.DirectionDownstream, 3, g.next(model.DirectionDownstream)) + if err != nil { + t.Fatalf("traverseLineage error: %v", err) + } + if !reflect.DeepEqual(nodes, []int64{1, 2, 3, 4}) { + t.Fatalf("nodes = %v, want [1 2 3 4]", nodes) + } + if len(edges) != 4 { + t.Fatalf("edges = %v, want 4 edges", edges) + } +} + +// 测试重复边去重:邻接数据里出现两条相同边时只输出一次 +func TestTraverseLineageDuplicateEdge(t *testing.T) { + g := &memoryGraph{edges: []LineageEdgeData{mkEdge(1, 2), mkEdge(1, 2), mkEdge(2, 3)}} + + nodes, edges, err := traverseLineage(1, model.DirectionDownstream, 2, g.next(model.DirectionDownstream)) + if err != nil { + t.Fatalf("traverseLineage error: %v", err) + } + if !reflect.DeepEqual(nodes, []int64{1, 2, 3}) { + t.Fatalf("nodes = %v, want [1 2 3]", nodes) + } + if len(edges) != 2 { + t.Fatalf("edges = %v, want 2 edges", edges) + } +} + +// 测试深度缺省:depth<=0 时按 1 跳处理 +func TestTraverseLineageDepthDefault(t *testing.T) { + g := &memoryGraph{edges: []LineageEdgeData{mkEdge(1, 2), mkEdge(2, 3)}} + + nodes, _, err := traverseLineage(1, model.DirectionDownstream, 0, g.next(model.DirectionDownstream)) + if err != nil { + t.Fatalf("traverseLineage error: %v", err) + } + if !reflect.DeepEqual(nodes, []int64{1, 2}) { + t.Fatalf("nodes = %v, want [1 2]", nodes) + } +} + +// 测试孤立节点:无边可达时只返回起点自身 +func TestTraverseLineageIsolatedNode(t *testing.T) { + g := &memoryGraph{edges: []LineageEdgeData{mkEdge(2, 3)}} + + nodes, edges, err := traverseLineage(1, model.DirectionDownstream, 3, g.next(model.DirectionDownstream)) + if err != nil { + t.Fatalf("traverseLineage error: %v", err) + } + if !reflect.DeepEqual(nodes, []int64{1}) { + t.Fatalf("nodes = %v, want [1]", nodes) + } + if len(edges) != 0 { + t.Fatalf("edges = %v, want empty", edges) + } +} + +// 测试邻居查询失败时错误向上传递 +func TestTraverseLineageNextError(t *testing.T) { + wantErr := errors.New("db down") + next := func(int64) ([]LineageEdgeData, error) { return nil, wantErr } + + _, _, err := traverseLineage(1, model.DirectionDownstream, 2, next) + if !errors.Is(err, wantErr) { + t.Fatalf("err = %v, want %v", err, wantErr) + } +} diff --git a/metadata/rpc/internal/logic/listEntitiesLogic.go b/metadata/rpc/internal/logic/listEntitiesLogic.go index 4d58cca..54460f7 100644 --- a/metadata/rpc/internal/logic/listEntitiesLogic.go +++ b/metadata/rpc/internal/logic/listEntitiesLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/metadata/model" "datacenter/metadata/rpc/internal/svc" "datacenter/metadata/rpc/metadata" @@ -25,7 +26,17 @@ func NewListEntitiesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *List // 实体列表 func (l *ListEntitiesLogic) ListEntities(in *metadata.EntityListReq) (*metadata.EntityListReply, error) { - // todo: add your logic here and delete this line - - return &metadata.EntityListReply{}, nil + // 多租户条件必填 + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + list, total, err := l.svcCtx.EntityModel.FindList(l.ctx, in.Beid, in.EntityType, in.Keyword, in.DsId, in.Page, in.Size) + if err != nil { + return nil, err + } + entities := make([]*metadata.MetaEntity, 0, len(list)) + for _, e := range list { + entities = append(entities, entityToPb(e)) + } + return &metadata.EntityListReply{List: entities, Total: total}, nil } diff --git a/metadata/rpc/internal/logic/listVersionsLogic.go b/metadata/rpc/internal/logic/listVersionsLogic.go index 0b202c8..5e8068a 100644 --- a/metadata/rpc/internal/logic/listVersionsLogic.go +++ b/metadata/rpc/internal/logic/listVersionsLogic.go @@ -25,7 +25,13 @@ func NewListVersionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *List // 版本列表 func (l *ListVersionsLogic) ListVersions(in *metadata.IdReq) (*metadata.VersionListReply, error) { - // todo: add your logic here and delete this line - - return &metadata.VersionListReply{}, nil + list, err := l.svcCtx.VersionModel.FindList(l.ctx, in.Id) + if err != nil { + return nil, err + } + versions := make([]*metadata.MetaVersion, 0, len(list)) + for _, v := range list { + versions = append(versions, versionToPb(v)) + } + return &metadata.VersionListReply{List: versions}, nil } diff --git a/metadata/rpc/internal/logic/snapshot.go b/metadata/rpc/internal/logic/snapshot.go new file mode 100644 index 0000000..551abbb --- /dev/null +++ b/metadata/rpc/internal/logic/snapshot.go @@ -0,0 +1,72 @@ +package logic + +import ( + "encoding/json" + "strconv" + + "datacenter/metadata/model" +) + +// entitySnapshot 实体定版快照结构(dc_meta_version.snapshot_json) +type entitySnapshot struct { + EntityType string `json:"entity_type"` + Fqn string `json:"fqn"` + Name string `json:"name"` + Comment string `json:"comment"` + Engine string `json:"engine"` + RowsCnt int64 `json:"rows_cnt"` + PropsJson string `json:"props_json"` + Classification string `json:"classification"` + Owner string `json:"owner"` + DsId int64 `json:"ds_id"` +} + +// newEntitySnapshot 从实体行构造定版快照(记录当前字段 + props) +func newEntitySnapshot(e *model.Entity) *entitySnapshot { + return &entitySnapshot{ + EntityType: e.EntityType, + Fqn: e.Fqn, + Name: e.Name, + Comment: e.Comment, + Engine: e.Engine, + RowsCnt: e.RowsCnt, + PropsJson: e.PropsJson, + Classification: e.Classification, + Owner: e.Owner, + DsId: e.DsId, + } +} + +// parseEntitySnapshot 解析快照 JSON +func parseEntitySnapshot(data string) (*entitySnapshot, error) { + var s entitySnapshot + if err := json.Unmarshal([]byte(data), &s); err != nil { + return nil, err + } + return &s, nil +} + +// FieldDiff 单字段差异 +type FieldDiff struct { + Field string `json:"field"` + Before string `json:"before"` + After string `json:"after"` +} + +// diffEntitySnapshot 逐字段对比两版快照,返回差异字段数组(before=旧版,after=新版)。 +// 对比范围:name/comment/engine/rows_cnt/props_json/classification,字段顺序固定,便于前端渲染。 +func diffEntitySnapshot(before, after *entitySnapshot) []FieldDiff { + diffs := make([]FieldDiff, 0) + add := func(field, beforeVal, afterVal string) { + if beforeVal != afterVal { + diffs = append(diffs, FieldDiff{Field: field, Before: beforeVal, After: afterVal}) + } + } + add("name", before.Name, after.Name) + add("comment", before.Comment, after.Comment) + add("engine", before.Engine, after.Engine) + add("rows_cnt", strconv.FormatInt(before.RowsCnt, 10), strconv.FormatInt(after.RowsCnt, 10)) + add("props_json", before.PropsJson, after.PropsJson) + add("classification", before.Classification, after.Classification) + return diffs +} diff --git a/metadata/rpc/internal/logic/snapshot_test.go b/metadata/rpc/internal/logic/snapshot_test.go new file mode 100644 index 0000000..f79d0b4 --- /dev/null +++ b/metadata/rpc/internal/logic/snapshot_test.go @@ -0,0 +1,93 @@ +package logic + +import ( + "encoding/json" + "reflect" + "testing" + + "datacenter/metadata/model" +) + +// 测试快照无差异:字段全相同时返回空数组 +func TestDiffEntitySnapshotNoDiff(t *testing.T) { + before := &entitySnapshot{Name: "t1", Comment: "c", Engine: "InnoDB", RowsCnt: 10, + PropsJson: "{}", Classification: "L2"} + after := &entitySnapshot{Name: "t1", Comment: "c", Engine: "InnoDB", RowsCnt: 10, + PropsJson: "{}", Classification: "L2"} + + diffs := diffEntitySnapshot(before, after) + if len(diffs) != 0 { + t.Fatalf("diffs = %v, want empty", diffs) + } +} + +// 测试全字段差异:六个对比字段全部变化时按固定顺序输出 +func TestDiffEntitySnapshotAllDiff(t *testing.T) { + before := &entitySnapshot{Name: "t1", Comment: "旧注释", Engine: "MyISAM", RowsCnt: 10, + PropsJson: `{"a":1}`, Classification: "L2"} + after := &entitySnapshot{Name: "t2", Comment: "新注释", Engine: "InnoDB", RowsCnt: 20, + PropsJson: `{"a":2}`, Classification: "L3"} + + diffs := diffEntitySnapshot(before, after) + want := []FieldDiff{ + {Field: "name", Before: "t1", After: "t2"}, + {Field: "comment", Before: "旧注释", After: "新注释"}, + {Field: "engine", Before: "MyISAM", After: "InnoDB"}, + {Field: "rows_cnt", Before: "10", After: "20"}, + {Field: "props_json", Before: `{"a":1}`, After: `{"a":2}`}, + {Field: "classification", Before: "L2", After: "L3"}, + } + if !reflect.DeepEqual(diffs, want) { + t.Fatalf("diffs = %+v, want %+v", diffs, want) + } +} + +// 测试部分字段差异:只输出变化的字段 +func TestDiffEntitySnapshotPartialDiff(t *testing.T) { + before := &entitySnapshot{Name: "t1", Comment: "c", Engine: "InnoDB", RowsCnt: 10, + PropsJson: "{}", Classification: "L2"} + after := &entitySnapshot{Name: "t1", Comment: "c", Engine: "InnoDB", RowsCnt: 99, + PropsJson: "{}", Classification: "L4"} + + diffs := diffEntitySnapshot(before, after) + want := []FieldDiff{ + {Field: "rows_cnt", Before: "10", After: "99"}, + {Field: "classification", Before: "L2", After: "L4"}, + } + if !reflect.DeepEqual(diffs, want) { + t.Fatalf("diffs = %+v, want %+v", diffs, want) + } +} + +// 测试实体 -> 快照 -> JSON -> 快照 的往返一致性 +func TestEntitySnapshotRoundTrip(t *testing.T) { + entity := &model.Entity{ + Id: 1, EntityType: "table", Fqn: "mysql://db.t1", Name: "t1", + Comment: "订单表", Engine: "InnoDB", RowsCnt: 100, + PropsJson: `{"charset":"utf8mb4"}`, Classification: "L3", + Owner: "jack", DsId: 7, Beid: 1, + } + snap := newEntitySnapshot(entity) + if snap.EntityType != "table" || snap.Fqn != "mysql://db.t1" || snap.Owner != "jack" { + t.Fatalf("snapshot = %+v", snap) + } + + data, err := json.Marshal(snap) + if err != nil { + t.Fatalf("marshal error: %v", err) + } + parsed, err := parseEntitySnapshot(string(data)) + if err != nil { + t.Fatalf("parse error: %v", err) + } + if !reflect.DeepEqual(snap, parsed) { + t.Fatalf("parsed = %+v, want %+v", parsed, snap) + } +} + +// 测试非法快照 JSON 返回错误 +func TestParseEntitySnapshotInvalidJson(t *testing.T) { + if _, err := parseEntitySnapshot("not-json"); err == nil { + t.Fatalf("want error for invalid json, got nil") + } +} diff --git a/metadata/rpc/internal/svc/serviceContext.go b/metadata/rpc/internal/svc/serviceContext.go index a9dcb38..871c703 100644 --- a/metadata/rpc/internal/svc/serviceContext.go +++ b/metadata/rpc/internal/svc/serviceContext.go @@ -1,13 +1,27 @@ package svc -import "datacenter/metadata/rpc/internal/config" +import ( + "datacenter/metadata/model" + "datacenter/metadata/rpc/internal/config" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) type ServiceContext struct { - Config config.Config + Config config.Config + Conn sqlx.SqlConn // 主库连接(跨表事务编排用) + EntityModel model.EntityModel + RelationModel model.RelationModel + VersionModel model.VersionModel } func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ - Config: c, + Config: c, + Conn: conn, + EntityModel: model.NewEntityModel(conn), + RelationModel: model.NewRelationModel(conn), + VersionModel: model.NewVersionModel(conn), } } diff --git a/restart.sh b/restart.sh index bd83c74..71f4243 100644 --- a/restart.sh +++ b/restart.sh @@ -51,6 +51,14 @@ RpcServer ${userPath} ${UserRpc} ${configPath} RpcServer ${votesPath} ${VotesRpc} ${configPath} #搜索服务 RpcServerPlus ${getewayPath} search +#数据源服务(Phase 1) +RpcServerPlus ${getewayPath} datasource +#元数据服务(Phase 1) +RpcServerPlus ${getewayPath} metadata +#数据服务API(Phase 1) +RpcServerPlus ${getewayPath} datasvc +#资产目录服务(Phase 1) +RpcServerPlus ${getewayPath} catalog #问答抽奖服务 RpcServerPlus ${getewayPath} questions #Api服务 From 99c426dad4606fbeb28c58c9443f45fbd72cb100 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 13:24:32 +0800 Subject: [PATCH 04/12] =?UTF-8?q?Phase=202=20=E9=AA=A8=E6=9E=B6=EF=BC=9Aqu?= =?UTF-8?q?ality/metric/tag=20=E4=B8=89=E6=9C=8D=E5=8A=A1=EF=BC=88?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=B4=A8=E9=87=8F/=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=8C=87=E6=A0=87/=E6=95=B0=E6=8D=AE=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3 个 proto 契约定稿 + goctl 骨架 + config/yaml - docs/schema/phase2.sql:7 张 dc_ 表(质量规则/任务/结果、指标/关系、标签对象/定义/值) - 端口 :8089/:8090/:8091 --- .gitignore | 3 + docs/schema/phase2.sql | 129 ++ metric/rpc/etc/metric.example.yaml | 8 + metric/rpc/internal/config/config.go | 10 + metric/rpc/internal/logic/bindAtomsLogic.go | 31 + .../rpc/internal/logic/createMetricLogic.go | 31 + .../rpc/internal/logic/deleteMetricLogic.go | 31 + metric/rpc/internal/logic/getMetricLogic.go | 31 + .../internal/logic/listAtomsOfDeriveLogic.go | 31 + .../internal/logic/listMetricsByIdsLogic.go | 31 + metric/rpc/internal/logic/listMetricsLogic.go | 31 + .../rpc/internal/logic/updateMetricLogic.go | 31 + metric/rpc/internal/server/metricSvcServer.go | 72 + metric/rpc/internal/svc/serviceContext.go | 13 + metric/rpc/metric.go | 39 + metric/rpc/metric.proto | 84 ++ metric/rpc/metric/metric.pb.go | 846 +++++++++++ metric/rpc/metric/metric_grpc.pb.go | 403 ++++++ metric/rpc/metricsvc/metricSvc.go | 103 ++ quality/rpc/etc/quality.example.yaml | 8 + quality/rpc/internal/config/config.go | 10 + quality/rpc/internal/logic/createJobLogic.go | 31 + quality/rpc/internal/logic/createRuleLogic.go | 31 + quality/rpc/internal/logic/deleteRuleLogic.go | 31 + quality/rpc/internal/logic/listJobsLogic.go | 31 + .../rpc/internal/logic/listResultsLogic.go | 31 + quality/rpc/internal/logic/listRulesLogic.go | 31 + quality/rpc/internal/logic/runJobLogic.go | 31 + quality/rpc/internal/server/qualityServer.go | 66 + quality/rpc/internal/svc/serviceContext.go | 13 + quality/rpc/quality.go | 39 + quality/rpc/quality.proto | 106 ++ quality/rpc/quality/quality.pb.go | 1279 +++++++++++++++++ quality/rpc/quality/quality_grpc.pb.go | 363 +++++ quality/rpc/qualityClient/quality.go | 101 ++ tag/rpc/etc/tag.example.yaml | 8 + tag/rpc/internal/config/config.go | 10 + tag/rpc/internal/logic/createObjectLogic.go | 31 + tag/rpc/internal/logic/createTagLogic.go | 31 + tag/rpc/internal/logic/deleteTagLogic.go | 31 + tag/rpc/internal/logic/getObjectTagsLogic.go | 31 + tag/rpc/internal/logic/listObjectsLogic.go | 31 + tag/rpc/internal/logic/listTagsLogic.go | 31 + tag/rpc/internal/logic/taggingLogic.go | 31 + tag/rpc/internal/server/tagServer.go | 66 + tag/rpc/internal/svc/serviceContext.go | 13 + tag/rpc/tag.go | 39 + tag/rpc/tag.proto | 106 ++ tag/rpc/tag/tag.pb.go | 1259 ++++++++++++++++ tag/rpc/tag/tag_grpc.pb.go | 363 +++++ tag/rpc/tagClient/tag.go | 102 ++ 51 files changed, 6343 insertions(+) create mode 100644 docs/schema/phase2.sql create mode 100644 metric/rpc/etc/metric.example.yaml create mode 100755 metric/rpc/internal/config/config.go create mode 100644 metric/rpc/internal/logic/bindAtomsLogic.go create mode 100644 metric/rpc/internal/logic/createMetricLogic.go create mode 100644 metric/rpc/internal/logic/deleteMetricLogic.go create mode 100644 metric/rpc/internal/logic/getMetricLogic.go create mode 100644 metric/rpc/internal/logic/listAtomsOfDeriveLogic.go create mode 100644 metric/rpc/internal/logic/listMetricsByIdsLogic.go create mode 100644 metric/rpc/internal/logic/listMetricsLogic.go create mode 100644 metric/rpc/internal/logic/updateMetricLogic.go create mode 100644 metric/rpc/internal/server/metricSvcServer.go create mode 100644 metric/rpc/internal/svc/serviceContext.go create mode 100644 metric/rpc/metric.go create mode 100644 metric/rpc/metric.proto create mode 100644 metric/rpc/metric/metric.pb.go create mode 100644 metric/rpc/metric/metric_grpc.pb.go create mode 100644 metric/rpc/metricsvc/metricSvc.go create mode 100644 quality/rpc/etc/quality.example.yaml create mode 100755 quality/rpc/internal/config/config.go create mode 100644 quality/rpc/internal/logic/createJobLogic.go create mode 100644 quality/rpc/internal/logic/createRuleLogic.go create mode 100644 quality/rpc/internal/logic/deleteRuleLogic.go create mode 100644 quality/rpc/internal/logic/listJobsLogic.go create mode 100644 quality/rpc/internal/logic/listResultsLogic.go create mode 100644 quality/rpc/internal/logic/listRulesLogic.go create mode 100644 quality/rpc/internal/logic/runJobLogic.go create mode 100644 quality/rpc/internal/server/qualityServer.go create mode 100644 quality/rpc/internal/svc/serviceContext.go create mode 100644 quality/rpc/quality.go create mode 100644 quality/rpc/quality.proto create mode 100644 quality/rpc/quality/quality.pb.go create mode 100644 quality/rpc/quality/quality_grpc.pb.go create mode 100644 quality/rpc/qualityClient/quality.go create mode 100644 tag/rpc/etc/tag.example.yaml create mode 100755 tag/rpc/internal/config/config.go create mode 100644 tag/rpc/internal/logic/createObjectLogic.go create mode 100644 tag/rpc/internal/logic/createTagLogic.go create mode 100644 tag/rpc/internal/logic/deleteTagLogic.go create mode 100644 tag/rpc/internal/logic/getObjectTagsLogic.go create mode 100644 tag/rpc/internal/logic/listObjectsLogic.go create mode 100644 tag/rpc/internal/logic/listTagsLogic.go create mode 100644 tag/rpc/internal/logic/taggingLogic.go create mode 100644 tag/rpc/internal/server/tagServer.go create mode 100644 tag/rpc/internal/svc/serviceContext.go create mode 100644 tag/rpc/tag.go create mode 100644 tag/rpc/tag.proto create mode 100644 tag/rpc/tag/tag.pb.go create mode 100644 tag/rpc/tag/tag_grpc.pb.go create mode 100644 tag/rpc/tagClient/tag.go diff --git a/.gitignore b/.gitignore index b214368..e385ea7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ datasource/rpc/etc/datasource.yaml metadata/rpc/etc/metadata.yaml datasvc/rpc/etc/datasvc.yaml catalog/rpc/etc/catalog.yaml +quality/rpc/etc/quality.yaml +metric/rpc/etc/metric.yaml +tag/rpc/etc/tag.yaml diff --git a/docs/schema/phase2.sql b/docs/schema/phase2.sql new file mode 100644 index 0000000..148bf36 --- /dev/null +++ b/docs/schema/phase2.sql @@ -0,0 +1,129 @@ +-- ============================================================ +-- Phase 2 数据中台表(数据质量 / 数据指标 / 数据标签) +-- 目标库: datacenter 字符集: utf8mb4 引擎: InnoDB +-- 约定: 所有表带 beid 多租户字段 +-- ============================================================ + +-- 质量规则表 +CREATE TABLE IF NOT EXISTS `dc_quality_rule` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL COMMENT '规则名', + `rule_type` varchar(32) NOT NULL COMMENT 'row_count/not_null/unique/value_range/consistency/fluctuation', + `target_table` varchar(128) NOT NULL COMMENT '目标表', + `target_column` varchar(128) NOT NULL DEFAULT '' COMMENT '目标列(not_null/unique/value_range 用)', + `params_json` text COMMENT '规则参数: value_range={min,max}; consistency={ref_table,ref_column}; fluctuation={baseline,ratio_pct}', + `threshold_json` text COMMENT '期望阈值: {op:gt|lt|eq|between, value:..}', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '1.启用 0.停用', + `owner` varchar(64) NOT NULL DEFAULT '', + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid_status` (`beid`,`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='质量规则表'; + +-- 质量任务表(规则组合 + 数据源) +CREATE TABLE IF NOT EXISTS `dc_quality_job` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL, + `datasource_id` bigint NOT NULL COMMENT '目标数据源', + `db_name` varchar(128) NOT NULL DEFAULT '' COMMENT '目标库', + `rule_ids` varchar(1024) NOT NULL COMMENT '规则id,逗号分隔', + `cron` varchar(64) NOT NULL DEFAULT '' COMMENT 'cron 表达式(Phase 3 调度接入,本期仅存档)', + `last_status` varchar(16) NOT NULL DEFAULT '' COMMENT 'success/failed/partial', + `last_run_at` datetime(3) DEFAULT NULL, + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid` (`beid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='质量任务表'; + +-- 质量结果表 +CREATE TABLE IF NOT EXISTS `dc_quality_result` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `job_id` bigint NOT NULL, + `rule_id` bigint NOT NULL, + `rule_type` varchar(32) NOT NULL, + `check_value` varchar(255) NOT NULL DEFAULT '' COMMENT '检查实测值', + `pass_flag` tinyint NOT NULL COMMENT '1.通过 0.不通过', + `detail_json` text COMMENT '详情(阈值/实测/差异)', + `run_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_job_run` (`job_id`,`run_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='质量检查结果表'; + +-- 指标表(原子指标 + 派生指标) +CREATE TABLE IF NOT EXISTS `dc_metric` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name_en` varchar(128) NOT NULL COMMENT '英文名/标识', + `name_cn` varchar(128) NOT NULL COMMENT '中文名', + `metric_type` varchar(16) NOT NULL COMMENT 'atom/derive', + `biz_field` varchar(128) NOT NULL DEFAULT '' COMMENT '原子指标业务口径字段(如 pay_amount)', + `agg_type` varchar(16) NOT NULL DEFAULT '' COMMENT '聚合: sum/count/avg/max/min/distinct', + `expr` varchar(1024) NOT NULL DEFAULT '' COMMENT '派生表达式: 原子指标+修饰+周期(如 sum(pay_amount) where status=1 by day)', + `dimension_json` text COMMENT '维度[{name,comment}]', + `comment` varchar(1024) NOT NULL DEFAULT '' COMMENT '口径说明', + `owner` varchar(64) NOT NULL DEFAULT '', + `status` tinyint NOT NULL DEFAULT 1, + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_beid_name` (`beid`,`name_en`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据指标表'; + +-- 指标关系表(派生指标 → 原子指标引用) +CREATE TABLE IF NOT EXISTS `dc_metric_rel` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `derive_metric_id` bigint NOT NULL COMMENT '派生指标id', + `atom_metric_id` bigint NOT NULL COMMENT '被引用原子指标id', + `beid` bigint NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_rel` (`derive_metric_id`,`atom_metric_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='指标关系表'; + +-- 标签对象表 +CREATE TABLE IF NOT EXISTS `dc_tag_object` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `object_type` varchar(32) NOT NULL COMMENT 'user/member/goods', + `name` varchar(128) NOT NULL COMMENT '对象名', + `ds_id` bigint NOT NULL DEFAULT 0 COMMENT '对象主数据所在数据源', + `db_name` varchar(128) NOT NULL DEFAULT '' COMMENT '对象库', + `table_name` varchar(128) NOT NULL DEFAULT '' COMMENT '对象主表', + `pk_column` varchar(64) NOT NULL DEFAULT 'id' COMMENT '主键列', + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid_type` (`beid`,`object_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签对象表'; + +-- 标签定义表 +CREATE TABLE IF NOT EXISTS `dc_tag_def` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `object_id` bigint NOT NULL COMMENT '所属对象', + `tag_key` varchar(64) NOT NULL COMMENT '标签键(如 vip_level)', + `tag_name` varchar(128) NOT NULL COMMENT '标签名', + `tag_type` varchar(16) NOT NULL DEFAULT 'rule' COMMENT 'rule规则/fact事实/model模型', + `rule_sql` text COMMENT '规则SQL: select as object_id, as tag_value from ... where ...', + `refresh_cron` varchar(64) NOT NULL DEFAULT '', + `status` tinyint NOT NULL DEFAULT 1, + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_obj_key` (`object_id`,`tag_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签定义表'; + +-- 标签值表(打标结果) +CREATE TABLE IF NOT EXISTS `dc_tag_value` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `tag_id` bigint NOT NULL COMMENT '标签定义id', + `object_pk` varchar(128) NOT NULL COMMENT '对象主键值', + `tag_value` varchar(255) NOT NULL DEFAULT '' COMMENT '标签值', + `beid` bigint NOT NULL DEFAULT 0, + `tagged_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) COMMENT '打标时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_tag_pk` (`tag_id`,`object_pk`), + KEY `idx_pk` (`object_pk`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签值表'; diff --git a/metric/rpc/etc/metric.example.yaml b/metric/rpc/etc/metric.example.yaml new file mode 100644 index 0000000..d35f6c4 --- /dev/null +++ b/metric/rpc/etc/metric.example.yaml @@ -0,0 +1,8 @@ +Name: metric.rpc +ListenOn: 0.0.0.0:8090 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: metric.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/metric/rpc/internal/config/config.go b/metric/rpc/internal/config/config.go new file mode 100755 index 0000000..96863e9 --- /dev/null +++ b/metric/rpc/internal/config/config.go @@ -0,0 +1,10 @@ +package config + +import "github.com/zeromicro/go-zero/zrpc" + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } +} diff --git a/metric/rpc/internal/logic/bindAtomsLogic.go b/metric/rpc/internal/logic/bindAtomsLogic.go new file mode 100644 index 0000000..65f956d --- /dev/null +++ b/metric/rpc/internal/logic/bindAtomsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/logx" +) + +type BindAtomsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewBindAtomsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindAtomsLogic { + return &BindAtomsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 派生指标绑定原子指标(幂等覆盖) +func (l *BindAtomsLogic) BindAtoms(in *metric.BindAtomReq) (*metric.IdReq, error) { + // todo: add your logic here and delete this line + + return &metric.IdReq{}, nil +} diff --git a/metric/rpc/internal/logic/createMetricLogic.go b/metric/rpc/internal/logic/createMetricLogic.go new file mode 100644 index 0000000..efda2ff --- /dev/null +++ b/metric/rpc/internal/logic/createMetricLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateMetricLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateMetricLogic { + return &CreateMetricLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建指标 +func (l *CreateMetricLogic) CreateMetric(in *metric.CreateMetricReq) (*metric.MetricReply, error) { + // todo: add your logic here and delete this line + + return &metric.MetricReply{}, nil +} diff --git a/metric/rpc/internal/logic/deleteMetricLogic.go b/metric/rpc/internal/logic/deleteMetricLogic.go new file mode 100644 index 0000000..3e84134 --- /dev/null +++ b/metric/rpc/internal/logic/deleteMetricLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteMetricLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeleteMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteMetricLogic { + return &DeleteMetricLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 删除指标 +func (l *DeleteMetricLogic) DeleteMetric(in *metric.IdReq) (*metric.IdReq, error) { + // todo: add your logic here and delete this line + + return &metric.IdReq{}, nil +} diff --git a/metric/rpc/internal/logic/getMetricLogic.go b/metric/rpc/internal/logic/getMetricLogic.go new file mode 100644 index 0000000..a22123c --- /dev/null +++ b/metric/rpc/internal/logic/getMetricLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetMetricLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMetricLogic { + return &GetMetricLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 指标详情 +func (l *GetMetricLogic) GetMetric(in *metric.IdReq) (*metric.MetricReply, error) { + // todo: add your logic here and delete this line + + return &metric.MetricReply{}, nil +} diff --git a/metric/rpc/internal/logic/listAtomsOfDeriveLogic.go b/metric/rpc/internal/logic/listAtomsOfDeriveLogic.go new file mode 100644 index 0000000..33072f8 --- /dev/null +++ b/metric/rpc/internal/logic/listAtomsOfDeriveLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListAtomsOfDeriveLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListAtomsOfDeriveLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListAtomsOfDeriveLogic { + return &ListAtomsOfDeriveLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 取派生指标引用的原子指标 +func (l *ListAtomsOfDeriveLogic) ListAtomsOfDerive(in *metric.IdReq) (*metric.MetricListReply, error) { + // todo: add your logic here and delete this line + + return &metric.MetricListReply{}, nil +} diff --git a/metric/rpc/internal/logic/listMetricsByIdsLogic.go b/metric/rpc/internal/logic/listMetricsByIdsLogic.go new file mode 100644 index 0000000..dc94ae6 --- /dev/null +++ b/metric/rpc/internal/logic/listMetricsByIdsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListMetricsByIdsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListMetricsByIdsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListMetricsByIdsLogic { + return &ListMetricsByIdsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 按 id 批量取指标 +func (l *ListMetricsByIdsLogic) ListMetricsByIds(in *metric.MetricListByIdsReq) (*metric.MetricListReply, error) { + // todo: add your logic here and delete this line + + return &metric.MetricListReply{}, nil +} diff --git a/metric/rpc/internal/logic/listMetricsLogic.go b/metric/rpc/internal/logic/listMetricsLogic.go new file mode 100644 index 0000000..c15b208 --- /dev/null +++ b/metric/rpc/internal/logic/listMetricsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListMetricsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListMetricsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListMetricsLogic { + return &ListMetricsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 指标列表 +func (l *ListMetricsLogic) ListMetrics(in *metric.MetricListReq) (*metric.MetricListReply, error) { + // todo: add your logic here and delete this line + + return &metric.MetricListReply{}, nil +} diff --git a/metric/rpc/internal/logic/updateMetricLogic.go b/metric/rpc/internal/logic/updateMetricLogic.go new file mode 100644 index 0000000..280e2ad --- /dev/null +++ b/metric/rpc/internal/logic/updateMetricLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateMetricLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewUpdateMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateMetricLogic { + return &UpdateMetricLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 更新指标 +func (l *UpdateMetricLogic) UpdateMetric(in *metric.UpdateMetricReq) (*metric.IdReq, error) { + // todo: add your logic here and delete this line + + return &metric.IdReq{}, nil +} diff --git a/metric/rpc/internal/server/metricSvcServer.go b/metric/rpc/internal/server/metricSvcServer.go new file mode 100644 index 0000000..53f3694 --- /dev/null +++ b/metric/rpc/internal/server/metricSvcServer.go @@ -0,0 +1,72 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: metric.proto + +package server + +import ( + "context" + + "datacenter/metric/rpc/internal/logic" + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" +) + +type MetricSvcServer struct { + svcCtx *svc.ServiceContext + metric.UnimplementedMetricSvcServer +} + +func NewMetricSvcServer(svcCtx *svc.ServiceContext) *MetricSvcServer { + return &MetricSvcServer{ + svcCtx: svcCtx, + } +} + +// 创建指标 +func (s *MetricSvcServer) CreateMetric(ctx context.Context, in *metric.CreateMetricReq) (*metric.MetricReply, error) { + l := logic.NewCreateMetricLogic(ctx, s.svcCtx) + return l.CreateMetric(in) +} + +// 更新指标 +func (s *MetricSvcServer) UpdateMetric(ctx context.Context, in *metric.UpdateMetricReq) (*metric.IdReq, error) { + l := logic.NewUpdateMetricLogic(ctx, s.svcCtx) + return l.UpdateMetric(in) +} + +// 删除指标 +func (s *MetricSvcServer) DeleteMetric(ctx context.Context, in *metric.IdReq) (*metric.IdReq, error) { + l := logic.NewDeleteMetricLogic(ctx, s.svcCtx) + return l.DeleteMetric(in) +} + +// 指标详情 +func (s *MetricSvcServer) GetMetric(ctx context.Context, in *metric.IdReq) (*metric.MetricReply, error) { + l := logic.NewGetMetricLogic(ctx, s.svcCtx) + return l.GetMetric(in) +} + +// 指标列表 +func (s *MetricSvcServer) ListMetrics(ctx context.Context, in *metric.MetricListReq) (*metric.MetricListReply, error) { + l := logic.NewListMetricsLogic(ctx, s.svcCtx) + return l.ListMetrics(in) +} + +// 按 id 批量取指标 +func (s *MetricSvcServer) ListMetricsByIds(ctx context.Context, in *metric.MetricListByIdsReq) (*metric.MetricListReply, error) { + l := logic.NewListMetricsByIdsLogic(ctx, s.svcCtx) + return l.ListMetricsByIds(in) +} + +// 派生指标绑定原子指标(幂等覆盖) +func (s *MetricSvcServer) BindAtoms(ctx context.Context, in *metric.BindAtomReq) (*metric.IdReq, error) { + l := logic.NewBindAtomsLogic(ctx, s.svcCtx) + return l.BindAtoms(in) +} + +// 取派生指标引用的原子指标 +func (s *MetricSvcServer) ListAtomsOfDerive(ctx context.Context, in *metric.IdReq) (*metric.MetricListReply, error) { + l := logic.NewListAtomsOfDeriveLogic(ctx, s.svcCtx) + return l.ListAtomsOfDerive(in) +} diff --git a/metric/rpc/internal/svc/serviceContext.go b/metric/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..6987423 --- /dev/null +++ b/metric/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/metric/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/metric/rpc/metric.go b/metric/rpc/metric.go new file mode 100644 index 0000000..2296364 --- /dev/null +++ b/metric/rpc/metric.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/metric/rpc/internal/config" + "datacenter/metric/rpc/internal/server" + "datacenter/metric/rpc/internal/svc" + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/metric.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + metric.RegisterMetricSvcServer(grpcServer, server.NewMetricSvcServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/metric/rpc/metric.proto b/metric/rpc/metric.proto new file mode 100644 index 0000000..6eb30a9 --- /dev/null +++ b/metric/rpc/metric.proto @@ -0,0 +1,84 @@ +syntax = "proto3"; + +package metric; + +option go_package = "./metric"; + +message Metric { + int64 id = 1; + string name_en = 2; + string name_cn = 3; + string metric_type = 4; //atom/derive + string biz_field = 5; + string agg_type = 6; + string expr = 7; + string dimension_json = 8; + string comment = 9; + string owner = 10; + int32 status = 11; + int64 beid = 12; +} +message MetricReply { Metric metric = 1; } +message MetricListReply { + repeated Metric list = 1; + int64 total = 2; +} + +message CreateMetricReq { + string name_en = 1; + string name_cn = 2; + string metric_type = 3; + string biz_field = 4; + string agg_type = 5; + string expr = 6; + string dimension_json = 7; + string comment = 8; + string owner = 9; + int64 beid = 10; +} +message UpdateMetricReq { + int64 id = 1; + int64 beid = 2; + string name_cn = 3; + string expr = 4; + string dimension_json = 5; + string comment = 6; + int32 status = 7; +} +message MetricListReq { + int64 beid = 1; + string metric_type = 2; + string keyword = 3; + int64 page = 4; + int64 size = 5; +} +message IdReq { int64 id = 1; int64 beid = 2; } + +message BindAtomReq { + int64 derive_metric_id = 1; + repeated int64 atom_metric_ids = 2; + int64 beid = 3; +} +message MetricListByIdsReq { + repeated int64 ids = 1; + int64 beid = 2; +} + +service MetricSvc { + //创建指标 + rpc CreateMetric(CreateMetricReq) returns (MetricReply); + //更新指标 + rpc UpdateMetric(UpdateMetricReq) returns (IdReq); + //删除指标 + rpc DeleteMetric(IdReq) returns (IdReq); + //指标详情 + rpc GetMetric(IdReq) returns (MetricReply); + //指标列表 + rpc ListMetrics(MetricListReq) returns (MetricListReply); + //按 id 批量取指标 + rpc ListMetricsByIds(MetricListByIdsReq) returns (MetricListReply); + //派生指标绑定原子指标(幂等覆盖) + rpc BindAtoms(BindAtomReq) returns (IdReq); + //取派生指标引用的原子指标 + rpc ListAtomsOfDerive(IdReq) returns (MetricListReply); +} diff --git a/metric/rpc/metric/metric.pb.go b/metric/rpc/metric/metric.pb.go new file mode 100644 index 0000000..6da77eb --- /dev/null +++ b/metric/rpc/metric/metric.pb.go @@ -0,0 +1,846 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: metric.proto + +package metric + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Metric struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + NameEn string `protobuf:"bytes,2,opt,name=name_en,json=nameEn,proto3" json:"name_en,omitempty"` + NameCn string `protobuf:"bytes,3,opt,name=name_cn,json=nameCn,proto3" json:"name_cn,omitempty"` + MetricType string `protobuf:"bytes,4,opt,name=metric_type,json=metricType,proto3" json:"metric_type,omitempty"` //atom/derive + BizField string `protobuf:"bytes,5,opt,name=biz_field,json=bizField,proto3" json:"biz_field,omitempty"` + AggType string `protobuf:"bytes,6,opt,name=agg_type,json=aggType,proto3" json:"agg_type,omitempty"` + Expr string `protobuf:"bytes,7,opt,name=expr,proto3" json:"expr,omitempty"` + DimensionJson string `protobuf:"bytes,8,opt,name=dimension_json,json=dimensionJson,proto3" json:"dimension_json,omitempty"` + Comment string `protobuf:"bytes,9,opt,name=comment,proto3" json:"comment,omitempty"` + Owner string `protobuf:"bytes,10,opt,name=owner,proto3" json:"owner,omitempty"` + Status int32 `protobuf:"varint,11,opt,name=status,proto3" json:"status,omitempty"` + Beid int64 `protobuf:"varint,12,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Metric) Reset() { + *x = Metric{} + mi := &file_metric_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Metric) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Metric) ProtoMessage() {} + +func (x *Metric) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Metric.ProtoReflect.Descriptor instead. +func (*Metric) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{0} +} + +func (x *Metric) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Metric) GetNameEn() string { + if x != nil { + return x.NameEn + } + return "" +} + +func (x *Metric) GetNameCn() string { + if x != nil { + return x.NameCn + } + return "" +} + +func (x *Metric) GetMetricType() string { + if x != nil { + return x.MetricType + } + return "" +} + +func (x *Metric) GetBizField() string { + if x != nil { + return x.BizField + } + return "" +} + +func (x *Metric) GetAggType() string { + if x != nil { + return x.AggType + } + return "" +} + +func (x *Metric) GetExpr() string { + if x != nil { + return x.Expr + } + return "" +} + +func (x *Metric) GetDimensionJson() string { + if x != nil { + return x.DimensionJson + } + return "" +} + +func (x *Metric) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +func (x *Metric) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *Metric) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *Metric) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type MetricReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metric *Metric `protobuf:"bytes,1,opt,name=metric,proto3" json:"metric,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MetricReply) Reset() { + *x = MetricReply{} + mi := &file_metric_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MetricReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MetricReply) ProtoMessage() {} + +func (x *MetricReply) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MetricReply.ProtoReflect.Descriptor instead. +func (*MetricReply) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{1} +} + +func (x *MetricReply) GetMetric() *Metric { + if x != nil { + return x.Metric + } + return nil +} + +type MetricListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*Metric `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MetricListReply) Reset() { + *x = MetricListReply{} + mi := &file_metric_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MetricListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MetricListReply) ProtoMessage() {} + +func (x *MetricListReply) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MetricListReply.ProtoReflect.Descriptor instead. +func (*MetricListReply) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{2} +} + +func (x *MetricListReply) GetList() []*Metric { + if x != nil { + return x.List + } + return nil +} + +func (x *MetricListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type CreateMetricReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + NameEn string `protobuf:"bytes,1,opt,name=name_en,json=nameEn,proto3" json:"name_en,omitempty"` + NameCn string `protobuf:"bytes,2,opt,name=name_cn,json=nameCn,proto3" json:"name_cn,omitempty"` + MetricType string `protobuf:"bytes,3,opt,name=metric_type,json=metricType,proto3" json:"metric_type,omitempty"` + BizField string `protobuf:"bytes,4,opt,name=biz_field,json=bizField,proto3" json:"biz_field,omitempty"` + AggType string `protobuf:"bytes,5,opt,name=agg_type,json=aggType,proto3" json:"agg_type,omitempty"` + Expr string `protobuf:"bytes,6,opt,name=expr,proto3" json:"expr,omitempty"` + DimensionJson string `protobuf:"bytes,7,opt,name=dimension_json,json=dimensionJson,proto3" json:"dimension_json,omitempty"` + Comment string `protobuf:"bytes,8,opt,name=comment,proto3" json:"comment,omitempty"` + Owner string `protobuf:"bytes,9,opt,name=owner,proto3" json:"owner,omitempty"` + Beid int64 `protobuf:"varint,10,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateMetricReq) Reset() { + *x = CreateMetricReq{} + mi := &file_metric_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateMetricReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateMetricReq) ProtoMessage() {} + +func (x *CreateMetricReq) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateMetricReq.ProtoReflect.Descriptor instead. +func (*CreateMetricReq) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateMetricReq) GetNameEn() string { + if x != nil { + return x.NameEn + } + return "" +} + +func (x *CreateMetricReq) GetNameCn() string { + if x != nil { + return x.NameCn + } + return "" +} + +func (x *CreateMetricReq) GetMetricType() string { + if x != nil { + return x.MetricType + } + return "" +} + +func (x *CreateMetricReq) GetBizField() string { + if x != nil { + return x.BizField + } + return "" +} + +func (x *CreateMetricReq) GetAggType() string { + if x != nil { + return x.AggType + } + return "" +} + +func (x *CreateMetricReq) GetExpr() string { + if x != nil { + return x.Expr + } + return "" +} + +func (x *CreateMetricReq) GetDimensionJson() string { + if x != nil { + return x.DimensionJson + } + return "" +} + +func (x *CreateMetricReq) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +func (x *CreateMetricReq) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *CreateMetricReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type UpdateMetricReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + NameCn string `protobuf:"bytes,3,opt,name=name_cn,json=nameCn,proto3" json:"name_cn,omitempty"` + Expr string `protobuf:"bytes,4,opt,name=expr,proto3" json:"expr,omitempty"` + DimensionJson string `protobuf:"bytes,5,opt,name=dimension_json,json=dimensionJson,proto3" json:"dimension_json,omitempty"` + Comment string `protobuf:"bytes,6,opt,name=comment,proto3" json:"comment,omitempty"` + Status int32 `protobuf:"varint,7,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateMetricReq) Reset() { + *x = UpdateMetricReq{} + mi := &file_metric_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateMetricReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateMetricReq) ProtoMessage() {} + +func (x *UpdateMetricReq) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateMetricReq.ProtoReflect.Descriptor instead. +func (*UpdateMetricReq) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{4} +} + +func (x *UpdateMetricReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UpdateMetricReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *UpdateMetricReq) GetNameCn() string { + if x != nil { + return x.NameCn + } + return "" +} + +func (x *UpdateMetricReq) GetExpr() string { + if x != nil { + return x.Expr + } + return "" +} + +func (x *UpdateMetricReq) GetDimensionJson() string { + if x != nil { + return x.DimensionJson + } + return "" +} + +func (x *UpdateMetricReq) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +func (x *UpdateMetricReq) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +type MetricListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + MetricType string `protobuf:"bytes,2,opt,name=metric_type,json=metricType,proto3" json:"metric_type,omitempty"` + Keyword string `protobuf:"bytes,3,opt,name=keyword,proto3" json:"keyword,omitempty"` + Page int64 `protobuf:"varint,4,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MetricListReq) Reset() { + *x = MetricListReq{} + mi := &file_metric_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MetricListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MetricListReq) ProtoMessage() {} + +func (x *MetricListReq) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MetricListReq.ProtoReflect.Descriptor instead. +func (*MetricListReq) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{5} +} + +func (x *MetricListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *MetricListReq) GetMetricType() string { + if x != nil { + return x.MetricType + } + return "" +} + +func (x *MetricListReq) GetKeyword() string { + if x != nil { + return x.Keyword + } + return "" +} + +func (x *MetricListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *MetricListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_metric_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{6} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type BindAtomReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + DeriveMetricId int64 `protobuf:"varint,1,opt,name=derive_metric_id,json=deriveMetricId,proto3" json:"derive_metric_id,omitempty"` + AtomMetricIds []int64 `protobuf:"varint,2,rep,packed,name=atom_metric_ids,json=atomMetricIds,proto3" json:"atom_metric_ids,omitempty"` + Beid int64 `protobuf:"varint,3,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BindAtomReq) Reset() { + *x = BindAtomReq{} + mi := &file_metric_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BindAtomReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BindAtomReq) ProtoMessage() {} + +func (x *BindAtomReq) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BindAtomReq.ProtoReflect.Descriptor instead. +func (*BindAtomReq) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{7} +} + +func (x *BindAtomReq) GetDeriveMetricId() int64 { + if x != nil { + return x.DeriveMetricId + } + return 0 +} + +func (x *BindAtomReq) GetAtomMetricIds() []int64 { + if x != nil { + return x.AtomMetricIds + } + return nil +} + +func (x *BindAtomReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type MetricListByIdsReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ids []int64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MetricListByIdsReq) Reset() { + *x = MetricListByIdsReq{} + mi := &file_metric_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MetricListByIdsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MetricListByIdsReq) ProtoMessage() {} + +func (x *MetricListByIdsReq) ProtoReflect() protoreflect.Message { + mi := &file_metric_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MetricListByIdsReq.ProtoReflect.Descriptor instead. +func (*MetricListByIdsReq) Descriptor() ([]byte, []int) { + return file_metric_proto_rawDescGZIP(), []int{8} +} + +func (x *MetricListByIdsReq) GetIds() []int64 { + if x != nil { + return x.Ids + } + return nil +} + +func (x *MetricListByIdsReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +var File_metric_proto protoreflect.FileDescriptor + +const file_metric_proto_rawDesc = "" + + "\n" + + "\fmetric.proto\x12\x06metric\"\xba\x02\n" + + "\x06Metric\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" + + "\aname_en\x18\x02 \x01(\tR\x06nameEn\x12\x17\n" + + "\aname_cn\x18\x03 \x01(\tR\x06nameCn\x12\x1f\n" + + "\vmetric_type\x18\x04 \x01(\tR\n" + + "metricType\x12\x1b\n" + + "\tbiz_field\x18\x05 \x01(\tR\bbizField\x12\x19\n" + + "\bagg_type\x18\x06 \x01(\tR\aaggType\x12\x12\n" + + "\x04expr\x18\a \x01(\tR\x04expr\x12%\n" + + "\x0edimension_json\x18\b \x01(\tR\rdimensionJson\x12\x18\n" + + "\acomment\x18\t \x01(\tR\acomment\x12\x14\n" + + "\x05owner\x18\n" + + " \x01(\tR\x05owner\x12\x16\n" + + "\x06status\x18\v \x01(\x05R\x06status\x12\x12\n" + + "\x04beid\x18\f \x01(\x03R\x04beid\"5\n" + + "\vMetricReply\x12&\n" + + "\x06metric\x18\x01 \x01(\v2\x0e.metric.MetricR\x06metric\"K\n" + + "\x0fMetricListReply\x12\"\n" + + "\x04list\x18\x01 \x03(\v2\x0e.metric.MetricR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\x9b\x02\n" + + "\x0fCreateMetricReq\x12\x17\n" + + "\aname_en\x18\x01 \x01(\tR\x06nameEn\x12\x17\n" + + "\aname_cn\x18\x02 \x01(\tR\x06nameCn\x12\x1f\n" + + "\vmetric_type\x18\x03 \x01(\tR\n" + + "metricType\x12\x1b\n" + + "\tbiz_field\x18\x04 \x01(\tR\bbizField\x12\x19\n" + + "\bagg_type\x18\x05 \x01(\tR\aaggType\x12\x12\n" + + "\x04expr\x18\x06 \x01(\tR\x04expr\x12%\n" + + "\x0edimension_json\x18\a \x01(\tR\rdimensionJson\x12\x18\n" + + "\acomment\x18\b \x01(\tR\acomment\x12\x14\n" + + "\x05owner\x18\t \x01(\tR\x05owner\x12\x12\n" + + "\x04beid\x18\n" + + " \x01(\x03R\x04beid\"\xbb\x01\n" + + "\x0fUpdateMetricReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\x12\x17\n" + + "\aname_cn\x18\x03 \x01(\tR\x06nameCn\x12\x12\n" + + "\x04expr\x18\x04 \x01(\tR\x04expr\x12%\n" + + "\x0edimension_json\x18\x05 \x01(\tR\rdimensionJson\x12\x18\n" + + "\acomment\x18\x06 \x01(\tR\acomment\x12\x16\n" + + "\x06status\x18\a \x01(\x05R\x06status\"\x86\x01\n" + + "\rMetricListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x1f\n" + + "\vmetric_type\x18\x02 \x01(\tR\n" + + "metricType\x12\x18\n" + + "\akeyword\x18\x03 \x01(\tR\akeyword\x12\x12\n" + + "\x04page\x18\x04 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x05 \x01(\x03R\x04size\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"s\n" + + "\vBindAtomReq\x12(\n" + + "\x10derive_metric_id\x18\x01 \x01(\x03R\x0ederiveMetricId\x12&\n" + + "\x0fatom_metric_ids\x18\x02 \x03(\x03R\ratomMetricIds\x12\x12\n" + + "\x04beid\x18\x03 \x01(\x03R\x04beid\":\n" + + "\x12MetricListByIdsReq\x12\x10\n" + + "\x03ids\x18\x01 \x03(\x03R\x03ids\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid2\xd6\x03\n" + + "\tMetricSvc\x12<\n" + + "\fCreateMetric\x12\x17.metric.CreateMetricReq\x1a\x13.metric.MetricReply\x126\n" + + "\fUpdateMetric\x12\x17.metric.UpdateMetricReq\x1a\r.metric.IdReq\x12,\n" + + "\fDeleteMetric\x12\r.metric.IdReq\x1a\r.metric.IdReq\x12/\n" + + "\tGetMetric\x12\r.metric.IdReq\x1a\x13.metric.MetricReply\x12=\n" + + "\vListMetrics\x12\x15.metric.MetricListReq\x1a\x17.metric.MetricListReply\x12G\n" + + "\x10ListMetricsByIds\x12\x1a.metric.MetricListByIdsReq\x1a\x17.metric.MetricListReply\x12/\n" + + "\tBindAtoms\x12\x13.metric.BindAtomReq\x1a\r.metric.IdReq\x12;\n" + + "\x11ListAtomsOfDerive\x12\r.metric.IdReq\x1a\x17.metric.MetricListReplyB\n" + + "Z\b./metricb\x06proto3" + +var ( + file_metric_proto_rawDescOnce sync.Once + file_metric_proto_rawDescData []byte +) + +func file_metric_proto_rawDescGZIP() []byte { + file_metric_proto_rawDescOnce.Do(func() { + file_metric_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_metric_proto_rawDesc), len(file_metric_proto_rawDesc))) + }) + return file_metric_proto_rawDescData +} + +var file_metric_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_metric_proto_goTypes = []any{ + (*Metric)(nil), // 0: metric.Metric + (*MetricReply)(nil), // 1: metric.MetricReply + (*MetricListReply)(nil), // 2: metric.MetricListReply + (*CreateMetricReq)(nil), // 3: metric.CreateMetricReq + (*UpdateMetricReq)(nil), // 4: metric.UpdateMetricReq + (*MetricListReq)(nil), // 5: metric.MetricListReq + (*IdReq)(nil), // 6: metric.IdReq + (*BindAtomReq)(nil), // 7: metric.BindAtomReq + (*MetricListByIdsReq)(nil), // 8: metric.MetricListByIdsReq +} +var file_metric_proto_depIdxs = []int32{ + 0, // 0: metric.MetricReply.metric:type_name -> metric.Metric + 0, // 1: metric.MetricListReply.list:type_name -> metric.Metric + 3, // 2: metric.MetricSvc.CreateMetric:input_type -> metric.CreateMetricReq + 4, // 3: metric.MetricSvc.UpdateMetric:input_type -> metric.UpdateMetricReq + 6, // 4: metric.MetricSvc.DeleteMetric:input_type -> metric.IdReq + 6, // 5: metric.MetricSvc.GetMetric:input_type -> metric.IdReq + 5, // 6: metric.MetricSvc.ListMetrics:input_type -> metric.MetricListReq + 8, // 7: metric.MetricSvc.ListMetricsByIds:input_type -> metric.MetricListByIdsReq + 7, // 8: metric.MetricSvc.BindAtoms:input_type -> metric.BindAtomReq + 6, // 9: metric.MetricSvc.ListAtomsOfDerive:input_type -> metric.IdReq + 1, // 10: metric.MetricSvc.CreateMetric:output_type -> metric.MetricReply + 6, // 11: metric.MetricSvc.UpdateMetric:output_type -> metric.IdReq + 6, // 12: metric.MetricSvc.DeleteMetric:output_type -> metric.IdReq + 1, // 13: metric.MetricSvc.GetMetric:output_type -> metric.MetricReply + 2, // 14: metric.MetricSvc.ListMetrics:output_type -> metric.MetricListReply + 2, // 15: metric.MetricSvc.ListMetricsByIds:output_type -> metric.MetricListReply + 6, // 16: metric.MetricSvc.BindAtoms:output_type -> metric.IdReq + 2, // 17: metric.MetricSvc.ListAtomsOfDerive:output_type -> metric.MetricListReply + 10, // [10:18] is the sub-list for method output_type + 2, // [2:10] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_metric_proto_init() } +func file_metric_proto_init() { + if File_metric_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_metric_proto_rawDesc), len(file_metric_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_metric_proto_goTypes, + DependencyIndexes: file_metric_proto_depIdxs, + MessageInfos: file_metric_proto_msgTypes, + }.Build() + File_metric_proto = out.File + file_metric_proto_goTypes = nil + file_metric_proto_depIdxs = nil +} diff --git a/metric/rpc/metric/metric_grpc.pb.go b/metric/rpc/metric/metric_grpc.pb.go new file mode 100644 index 0000000..61eb050 --- /dev/null +++ b/metric/rpc/metric/metric_grpc.pb.go @@ -0,0 +1,403 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: metric.proto + +package metric + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + MetricSvc_CreateMetric_FullMethodName = "/metric.MetricSvc/CreateMetric" + MetricSvc_UpdateMetric_FullMethodName = "/metric.MetricSvc/UpdateMetric" + MetricSvc_DeleteMetric_FullMethodName = "/metric.MetricSvc/DeleteMetric" + MetricSvc_GetMetric_FullMethodName = "/metric.MetricSvc/GetMetric" + MetricSvc_ListMetrics_FullMethodName = "/metric.MetricSvc/ListMetrics" + MetricSvc_ListMetricsByIds_FullMethodName = "/metric.MetricSvc/ListMetricsByIds" + MetricSvc_BindAtoms_FullMethodName = "/metric.MetricSvc/BindAtoms" + MetricSvc_ListAtomsOfDerive_FullMethodName = "/metric.MetricSvc/ListAtomsOfDerive" +) + +// MetricSvcClient is the client API for MetricSvc service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type MetricSvcClient interface { + //创建指标 + CreateMetric(ctx context.Context, in *CreateMetricReq, opts ...grpc.CallOption) (*MetricReply, error) + //更新指标 + UpdateMetric(ctx context.Context, in *UpdateMetricReq, opts ...grpc.CallOption) (*IdReq, error) + //删除指标 + DeleteMetric(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + //指标详情 + GetMetric(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*MetricReply, error) + //指标列表 + ListMetrics(ctx context.Context, in *MetricListReq, opts ...grpc.CallOption) (*MetricListReply, error) + //按 id 批量取指标 + ListMetricsByIds(ctx context.Context, in *MetricListByIdsReq, opts ...grpc.CallOption) (*MetricListReply, error) + //派生指标绑定原子指标(幂等覆盖) + BindAtoms(ctx context.Context, in *BindAtomReq, opts ...grpc.CallOption) (*IdReq, error) + //取派生指标引用的原子指标 + ListAtomsOfDerive(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*MetricListReply, error) +} + +type metricSvcClient struct { + cc grpc.ClientConnInterface +} + +func NewMetricSvcClient(cc grpc.ClientConnInterface) MetricSvcClient { + return &metricSvcClient{cc} +} + +func (c *metricSvcClient) CreateMetric(ctx context.Context, in *CreateMetricReq, opts ...grpc.CallOption) (*MetricReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MetricReply) + err := c.cc.Invoke(ctx, MetricSvc_CreateMetric_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metricSvcClient) UpdateMetric(ctx context.Context, in *UpdateMetricReq, opts ...grpc.CallOption) (*IdReq, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdReq) + err := c.cc.Invoke(ctx, MetricSvc_UpdateMetric_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metricSvcClient) DeleteMetric(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdReq) + err := c.cc.Invoke(ctx, MetricSvc_DeleteMetric_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metricSvcClient) GetMetric(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*MetricReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MetricReply) + err := c.cc.Invoke(ctx, MetricSvc_GetMetric_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metricSvcClient) ListMetrics(ctx context.Context, in *MetricListReq, opts ...grpc.CallOption) (*MetricListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MetricListReply) + err := c.cc.Invoke(ctx, MetricSvc_ListMetrics_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metricSvcClient) ListMetricsByIds(ctx context.Context, in *MetricListByIdsReq, opts ...grpc.CallOption) (*MetricListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MetricListReply) + err := c.cc.Invoke(ctx, MetricSvc_ListMetricsByIds_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metricSvcClient) BindAtoms(ctx context.Context, in *BindAtomReq, opts ...grpc.CallOption) (*IdReq, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdReq) + err := c.cc.Invoke(ctx, MetricSvc_BindAtoms_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *metricSvcClient) ListAtomsOfDerive(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*MetricListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MetricListReply) + err := c.cc.Invoke(ctx, MetricSvc_ListAtomsOfDerive_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MetricSvcServer is the server API for MetricSvc service. +// All implementations must embed UnimplementedMetricSvcServer +// for forward compatibility. +type MetricSvcServer interface { + //创建指标 + CreateMetric(context.Context, *CreateMetricReq) (*MetricReply, error) + //更新指标 + UpdateMetric(context.Context, *UpdateMetricReq) (*IdReq, error) + //删除指标 + DeleteMetric(context.Context, *IdReq) (*IdReq, error) + //指标详情 + GetMetric(context.Context, *IdReq) (*MetricReply, error) + //指标列表 + ListMetrics(context.Context, *MetricListReq) (*MetricListReply, error) + //按 id 批量取指标 + ListMetricsByIds(context.Context, *MetricListByIdsReq) (*MetricListReply, error) + //派生指标绑定原子指标(幂等覆盖) + BindAtoms(context.Context, *BindAtomReq) (*IdReq, error) + //取派生指标引用的原子指标 + ListAtomsOfDerive(context.Context, *IdReq) (*MetricListReply, error) + mustEmbedUnimplementedMetricSvcServer() +} + +// UnimplementedMetricSvcServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedMetricSvcServer struct{} + +func (UnimplementedMetricSvcServer) CreateMetric(context.Context, *CreateMetricReq) (*MetricReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateMetric not implemented") +} +func (UnimplementedMetricSvcServer) UpdateMetric(context.Context, *UpdateMetricReq) (*IdReq, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateMetric not implemented") +} +func (UnimplementedMetricSvcServer) DeleteMetric(context.Context, *IdReq) (*IdReq, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteMetric not implemented") +} +func (UnimplementedMetricSvcServer) GetMetric(context.Context, *IdReq) (*MetricReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetMetric not implemented") +} +func (UnimplementedMetricSvcServer) ListMetrics(context.Context, *MetricListReq) (*MetricListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListMetrics not implemented") +} +func (UnimplementedMetricSvcServer) ListMetricsByIds(context.Context, *MetricListByIdsReq) (*MetricListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListMetricsByIds not implemented") +} +func (UnimplementedMetricSvcServer) BindAtoms(context.Context, *BindAtomReq) (*IdReq, error) { + return nil, status.Error(codes.Unimplemented, "method BindAtoms not implemented") +} +func (UnimplementedMetricSvcServer) ListAtomsOfDerive(context.Context, *IdReq) (*MetricListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListAtomsOfDerive not implemented") +} +func (UnimplementedMetricSvcServer) mustEmbedUnimplementedMetricSvcServer() {} +func (UnimplementedMetricSvcServer) testEmbeddedByValue() {} + +// UnsafeMetricSvcServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to MetricSvcServer will +// result in compilation errors. +type UnsafeMetricSvcServer interface { + mustEmbedUnimplementedMetricSvcServer() +} + +func RegisterMetricSvcServer(s grpc.ServiceRegistrar, srv MetricSvcServer) { + // If the following call panics, it indicates UnimplementedMetricSvcServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&MetricSvc_ServiceDesc, srv) +} + +func _MetricSvc_CreateMetric_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateMetricReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetricSvcServer).CreateMetric(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MetricSvc_CreateMetric_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetricSvcServer).CreateMetric(ctx, req.(*CreateMetricReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _MetricSvc_UpdateMetric_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateMetricReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetricSvcServer).UpdateMetric(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MetricSvc_UpdateMetric_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetricSvcServer).UpdateMetric(ctx, req.(*UpdateMetricReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _MetricSvc_DeleteMetric_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetricSvcServer).DeleteMetric(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MetricSvc_DeleteMetric_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetricSvcServer).DeleteMetric(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _MetricSvc_GetMetric_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetricSvcServer).GetMetric(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MetricSvc_GetMetric_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetricSvcServer).GetMetric(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _MetricSvc_ListMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MetricListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetricSvcServer).ListMetrics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MetricSvc_ListMetrics_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetricSvcServer).ListMetrics(ctx, req.(*MetricListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _MetricSvc_ListMetricsByIds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MetricListByIdsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetricSvcServer).ListMetricsByIds(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MetricSvc_ListMetricsByIds_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetricSvcServer).ListMetricsByIds(ctx, req.(*MetricListByIdsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _MetricSvc_BindAtoms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BindAtomReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetricSvcServer).BindAtoms(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MetricSvc_BindAtoms_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetricSvcServer).BindAtoms(ctx, req.(*BindAtomReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _MetricSvc_ListAtomsOfDerive_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MetricSvcServer).ListAtomsOfDerive(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MetricSvc_ListAtomsOfDerive_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MetricSvcServer).ListAtomsOfDerive(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +// MetricSvc_ServiceDesc is the grpc.ServiceDesc for MetricSvc service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var MetricSvc_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "metric.MetricSvc", + HandlerType: (*MetricSvcServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateMetric", + Handler: _MetricSvc_CreateMetric_Handler, + }, + { + MethodName: "UpdateMetric", + Handler: _MetricSvc_UpdateMetric_Handler, + }, + { + MethodName: "DeleteMetric", + Handler: _MetricSvc_DeleteMetric_Handler, + }, + { + MethodName: "GetMetric", + Handler: _MetricSvc_GetMetric_Handler, + }, + { + MethodName: "ListMetrics", + Handler: _MetricSvc_ListMetrics_Handler, + }, + { + MethodName: "ListMetricsByIds", + Handler: _MetricSvc_ListMetricsByIds_Handler, + }, + { + MethodName: "BindAtoms", + Handler: _MetricSvc_BindAtoms_Handler, + }, + { + MethodName: "ListAtomsOfDerive", + Handler: _MetricSvc_ListAtomsOfDerive_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "metric.proto", +} diff --git a/metric/rpc/metricsvc/metricSvc.go b/metric/rpc/metricsvc/metricSvc.go new file mode 100644 index 0000000..beabfb4 --- /dev/null +++ b/metric/rpc/metricsvc/metricSvc.go @@ -0,0 +1,103 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: metric.proto + +package metricsvc + +import ( + "context" + + "datacenter/metric/rpc/metric" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + BindAtomReq = metric.BindAtomReq + CreateMetricReq = metric.CreateMetricReq + IdReq = metric.IdReq + Metric = metric.Metric + MetricListByIdsReq = metric.MetricListByIdsReq + MetricListReply = metric.MetricListReply + MetricListReq = metric.MetricListReq + MetricReply = metric.MetricReply + UpdateMetricReq = metric.UpdateMetricReq + + MetricSvc interface { + // 创建指标 + CreateMetric(ctx context.Context, in *CreateMetricReq, opts ...grpc.CallOption) (*MetricReply, error) + // 更新指标 + UpdateMetric(ctx context.Context, in *UpdateMetricReq, opts ...grpc.CallOption) (*IdReq, error) + // 删除指标 + DeleteMetric(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + // 指标详情 + GetMetric(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*MetricReply, error) + // 指标列表 + ListMetrics(ctx context.Context, in *MetricListReq, opts ...grpc.CallOption) (*MetricListReply, error) + // 按 id 批量取指标 + ListMetricsByIds(ctx context.Context, in *MetricListByIdsReq, opts ...grpc.CallOption) (*MetricListReply, error) + // 派生指标绑定原子指标(幂等覆盖) + BindAtoms(ctx context.Context, in *BindAtomReq, opts ...grpc.CallOption) (*IdReq, error) + // 取派生指标引用的原子指标 + ListAtomsOfDerive(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*MetricListReply, error) + } + + defaultMetricSvc struct { + cli zrpc.Client + } +) + +func NewMetricSvc(cli zrpc.Client) MetricSvc { + return &defaultMetricSvc{ + cli: cli, + } +} + +// 创建指标 +func (m *defaultMetricSvc) CreateMetric(ctx context.Context, in *CreateMetricReq, opts ...grpc.CallOption) (*MetricReply, error) { + client := metric.NewMetricSvcClient(m.cli.Conn()) + return client.CreateMetric(ctx, in, opts...) +} + +// 更新指标 +func (m *defaultMetricSvc) UpdateMetric(ctx context.Context, in *UpdateMetricReq, opts ...grpc.CallOption) (*IdReq, error) { + client := metric.NewMetricSvcClient(m.cli.Conn()) + return client.UpdateMetric(ctx, in, opts...) +} + +// 删除指标 +func (m *defaultMetricSvc) DeleteMetric(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + client := metric.NewMetricSvcClient(m.cli.Conn()) + return client.DeleteMetric(ctx, in, opts...) +} + +// 指标详情 +func (m *defaultMetricSvc) GetMetric(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*MetricReply, error) { + client := metric.NewMetricSvcClient(m.cli.Conn()) + return client.GetMetric(ctx, in, opts...) +} + +// 指标列表 +func (m *defaultMetricSvc) ListMetrics(ctx context.Context, in *MetricListReq, opts ...grpc.CallOption) (*MetricListReply, error) { + client := metric.NewMetricSvcClient(m.cli.Conn()) + return client.ListMetrics(ctx, in, opts...) +} + +// 按 id 批量取指标 +func (m *defaultMetricSvc) ListMetricsByIds(ctx context.Context, in *MetricListByIdsReq, opts ...grpc.CallOption) (*MetricListReply, error) { + client := metric.NewMetricSvcClient(m.cli.Conn()) + return client.ListMetricsByIds(ctx, in, opts...) +} + +// 派生指标绑定原子指标(幂等覆盖) +func (m *defaultMetricSvc) BindAtoms(ctx context.Context, in *BindAtomReq, opts ...grpc.CallOption) (*IdReq, error) { + client := metric.NewMetricSvcClient(m.cli.Conn()) + return client.BindAtoms(ctx, in, opts...) +} + +// 取派生指标引用的原子指标 +func (m *defaultMetricSvc) ListAtomsOfDerive(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*MetricListReply, error) { + client := metric.NewMetricSvcClient(m.cli.Conn()) + return client.ListAtomsOfDerive(ctx, in, opts...) +} diff --git a/quality/rpc/etc/quality.example.yaml b/quality/rpc/etc/quality.example.yaml new file mode 100644 index 0000000..4c8334a --- /dev/null +++ b/quality/rpc/etc/quality.example.yaml @@ -0,0 +1,8 @@ +Name: quality.rpc +ListenOn: 0.0.0.0:8089 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: quality.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/quality/rpc/internal/config/config.go b/quality/rpc/internal/config/config.go new file mode 100755 index 0000000..96863e9 --- /dev/null +++ b/quality/rpc/internal/config/config.go @@ -0,0 +1,10 @@ +package config + +import "github.com/zeromicro/go-zero/zrpc" + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } +} diff --git a/quality/rpc/internal/logic/createJobLogic.go b/quality/rpc/internal/logic/createJobLogic.go new file mode 100644 index 0000000..903d232 --- /dev/null +++ b/quality/rpc/internal/logic/createJobLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateJobLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateJobLogic { + return &CreateJobLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建任务 +func (l *CreateJobLogic) CreateJob(in *quality.CreateJobReq) (*quality.JobReply, error) { + // todo: add your logic here and delete this line + + return &quality.JobReply{}, nil +} diff --git a/quality/rpc/internal/logic/createRuleLogic.go b/quality/rpc/internal/logic/createRuleLogic.go new file mode 100644 index 0000000..af009c9 --- /dev/null +++ b/quality/rpc/internal/logic/createRuleLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateRuleLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRuleLogic { + return &CreateRuleLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建规则 +func (l *CreateRuleLogic) CreateRule(in *quality.CreateRuleReq) (*quality.RuleReply, error) { + // todo: add your logic here and delete this line + + return &quality.RuleReply{}, nil +} diff --git a/quality/rpc/internal/logic/deleteRuleLogic.go b/quality/rpc/internal/logic/deleteRuleLogic.go new file mode 100644 index 0000000..25718cb --- /dev/null +++ b/quality/rpc/internal/logic/deleteRuleLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteRuleLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeleteRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRuleLogic { + return &DeleteRuleLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 删除规则 +func (l *DeleteRuleLogic) DeleteRule(in *quality.IdReq) (*quality.IdReq, error) { + // todo: add your logic here and delete this line + + return &quality.IdReq{}, nil +} diff --git a/quality/rpc/internal/logic/listJobsLogic.go b/quality/rpc/internal/logic/listJobsLogic.go new file mode 100644 index 0000000..7a4402d --- /dev/null +++ b/quality/rpc/internal/logic/listJobsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListJobsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListJobsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListJobsLogic { + return &ListJobsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 任务列表 +func (l *ListJobsLogic) ListJobs(in *quality.JobListReq) (*quality.JobListReply, error) { + // todo: add your logic here and delete this line + + return &quality.JobListReply{}, nil +} diff --git a/quality/rpc/internal/logic/listResultsLogic.go b/quality/rpc/internal/logic/listResultsLogic.go new file mode 100644 index 0000000..cf12f69 --- /dev/null +++ b/quality/rpc/internal/logic/listResultsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListResultsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListResultsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListResultsLogic { + return &ListResultsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 结果列表 +func (l *ListResultsLogic) ListResults(in *quality.ResultListReq) (*quality.ResultListReply, error) { + // todo: add your logic here and delete this line + + return &quality.ResultListReply{}, nil +} diff --git a/quality/rpc/internal/logic/listRulesLogic.go b/quality/rpc/internal/logic/listRulesLogic.go new file mode 100644 index 0000000..8416e98 --- /dev/null +++ b/quality/rpc/internal/logic/listRulesLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListRulesLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListRulesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListRulesLogic { + return &ListRulesLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 规则列表 +func (l *ListRulesLogic) ListRules(in *quality.RuleListReq) (*quality.RuleListReply, error) { + // todo: add your logic here and delete this line + + return &quality.RuleListReply{}, nil +} diff --git a/quality/rpc/internal/logic/runJobLogic.go b/quality/rpc/internal/logic/runJobLogic.go new file mode 100644 index 0000000..63adab5 --- /dev/null +++ b/quality/rpc/internal/logic/runJobLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RunJobLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewRunJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RunJobLogic { + return &RunJobLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 执行任务(同步跑完规则并落结果) +func (l *RunJobLogic) RunJob(in *quality.IdReq) (*quality.RunJobReply, error) { + // todo: add your logic here and delete this line + + return &quality.RunJobReply{}, nil +} diff --git a/quality/rpc/internal/server/qualityServer.go b/quality/rpc/internal/server/qualityServer.go new file mode 100644 index 0000000..53b68df --- /dev/null +++ b/quality/rpc/internal/server/qualityServer.go @@ -0,0 +1,66 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: quality.proto + +package server + +import ( + "context" + + "datacenter/quality/rpc/internal/logic" + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" +) + +type QualityServer struct { + svcCtx *svc.ServiceContext + quality.UnimplementedQualityServer +} + +func NewQualityServer(svcCtx *svc.ServiceContext) *QualityServer { + return &QualityServer{ + svcCtx: svcCtx, + } +} + +// 创建规则 +func (s *QualityServer) CreateRule(ctx context.Context, in *quality.CreateRuleReq) (*quality.RuleReply, error) { + l := logic.NewCreateRuleLogic(ctx, s.svcCtx) + return l.CreateRule(in) +} + +// 删除规则 +func (s *QualityServer) DeleteRule(ctx context.Context, in *quality.IdReq) (*quality.IdReq, error) { + l := logic.NewDeleteRuleLogic(ctx, s.svcCtx) + return l.DeleteRule(in) +} + +// 规则列表 +func (s *QualityServer) ListRules(ctx context.Context, in *quality.RuleListReq) (*quality.RuleListReply, error) { + l := logic.NewListRulesLogic(ctx, s.svcCtx) + return l.ListRules(in) +} + +// 创建任务 +func (s *QualityServer) CreateJob(ctx context.Context, in *quality.CreateJobReq) (*quality.JobReply, error) { + l := logic.NewCreateJobLogic(ctx, s.svcCtx) + return l.CreateJob(in) +} + +// 任务列表 +func (s *QualityServer) ListJobs(ctx context.Context, in *quality.JobListReq) (*quality.JobListReply, error) { + l := logic.NewListJobsLogic(ctx, s.svcCtx) + return l.ListJobs(in) +} + +// 执行任务(同步跑完规则并落结果) +func (s *QualityServer) RunJob(ctx context.Context, in *quality.IdReq) (*quality.RunJobReply, error) { + l := logic.NewRunJobLogic(ctx, s.svcCtx) + return l.RunJob(in) +} + +// 结果列表 +func (s *QualityServer) ListResults(ctx context.Context, in *quality.ResultListReq) (*quality.ResultListReply, error) { + l := logic.NewListResultsLogic(ctx, s.svcCtx) + return l.ListResults(in) +} diff --git a/quality/rpc/internal/svc/serviceContext.go b/quality/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..4462f7c --- /dev/null +++ b/quality/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/quality/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/quality/rpc/quality.go b/quality/rpc/quality.go new file mode 100644 index 0000000..c9a096f --- /dev/null +++ b/quality/rpc/quality.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/quality/rpc/internal/config" + "datacenter/quality/rpc/internal/server" + "datacenter/quality/rpc/internal/svc" + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/quality.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + quality.RegisterQualityServer(grpcServer, server.NewQualityServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/quality/rpc/quality.proto b/quality/rpc/quality.proto new file mode 100644 index 0000000..84ddabe --- /dev/null +++ b/quality/rpc/quality.proto @@ -0,0 +1,106 @@ +syntax = "proto3"; + +package quality; + +option go_package = "./quality"; + +message QualityRule { + int64 id = 1; + string name = 2; + string rule_type = 3; //row_count/not_null/unique/value_range/consistency/fluctuation + string target_table = 4; + string target_column = 5; + string params_json = 6; + string threshold_json = 7; + int32 status = 8; + string owner = 9; + int64 beid = 10; +} +message RuleReply { QualityRule rule = 1; } +message RuleListReply { + repeated QualityRule list = 1; + int64 total = 2; +} + +message CreateRuleReq { + string name = 1; + string rule_type = 2; + string target_table = 3; + string target_column = 4; + string params_json = 5; + string threshold_json = 6; + string owner = 7; + int64 beid = 8; +} +message IdReq { int64 id = 1; int64 beid = 2; } +message RuleListReq { + int64 beid = 1; + string rule_type = 2; + int64 page = 3; + int64 size = 4; +} + +message CreateJobReq { + string name = 1; + int64 datasource_id = 2; + string db_name = 3; + string rule_ids = 4; //逗号分隔 + string cron = 5; + int64 beid = 6; +} +message QualityJob { + int64 id = 1; + string name = 2; + int64 datasource_id = 3; + string db_name = 4; + string rule_ids = 5; + string cron = 6; + string last_status = 7; + int64 last_run_at = 8; + int64 beid = 9; +} +message JobReply { QualityJob job = 1; } +message JobListReply { + repeated QualityJob list = 1; + int64 total = 2; +} +message JobListReq { int64 beid = 1; int64 page = 2; int64 size = 3; } + +message QualityResult { + int64 id = 1; + int64 job_id = 2; + int64 rule_id = 3; + string rule_type = 4; + string check_value = 5; + bool pass_flag = 6; + string detail_json = 7; + int64 run_at = 8; +} +message RunJobReply { + int64 job_id = 1; + int64 total_rules = 2; + int64 passed = 3; + string last_status = 4; //success/failed/partial +} +message ResultListReply { + repeated QualityResult list = 1; + int64 total = 2; +} +message ResultListReq { int64 job_id = 1; int64 pass_filter = 2; int64 page = 3; int64 size = 4; } + +service Quality { + //创建规则 + rpc CreateRule(CreateRuleReq) returns (RuleReply); + //删除规则 + rpc DeleteRule(IdReq) returns (IdReq); + //规则列表 + rpc ListRules(RuleListReq) returns (RuleListReply); + //创建任务 + rpc CreateJob(CreateJobReq) returns (JobReply); + //任务列表 + rpc ListJobs(JobListReq) returns (JobListReply); + //执行任务(同步跑完规则并落结果) + rpc RunJob(IdReq) returns (RunJobReply); + //结果列表 + rpc ListResults(ResultListReq) returns (ResultListReply); +} diff --git a/quality/rpc/quality/quality.pb.go b/quality/rpc/quality/quality.pb.go new file mode 100644 index 0000000..a52071c --- /dev/null +++ b/quality/rpc/quality/quality.pb.go @@ -0,0 +1,1279 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: quality.proto + +package quality + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type QualityRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + RuleType string `protobuf:"bytes,3,opt,name=rule_type,json=ruleType,proto3" json:"rule_type,omitempty"` //row_count/not_null/unique/value_range/consistency/fluctuation + TargetTable string `protobuf:"bytes,4,opt,name=target_table,json=targetTable,proto3" json:"target_table,omitempty"` + TargetColumn string `protobuf:"bytes,5,opt,name=target_column,json=targetColumn,proto3" json:"target_column,omitempty"` + ParamsJson string `protobuf:"bytes,6,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` + ThresholdJson string `protobuf:"bytes,7,opt,name=threshold_json,json=thresholdJson,proto3" json:"threshold_json,omitempty"` + Status int32 `protobuf:"varint,8,opt,name=status,proto3" json:"status,omitempty"` + Owner string `protobuf:"bytes,9,opt,name=owner,proto3" json:"owner,omitempty"` + Beid int64 `protobuf:"varint,10,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QualityRule) Reset() { + *x = QualityRule{} + mi := &file_quality_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QualityRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QualityRule) ProtoMessage() {} + +func (x *QualityRule) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QualityRule.ProtoReflect.Descriptor instead. +func (*QualityRule) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{0} +} + +func (x *QualityRule) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *QualityRule) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *QualityRule) GetRuleType() string { + if x != nil { + return x.RuleType + } + return "" +} + +func (x *QualityRule) GetTargetTable() string { + if x != nil { + return x.TargetTable + } + return "" +} + +func (x *QualityRule) GetTargetColumn() string { + if x != nil { + return x.TargetColumn + } + return "" +} + +func (x *QualityRule) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *QualityRule) GetThresholdJson() string { + if x != nil { + return x.ThresholdJson + } + return "" +} + +func (x *QualityRule) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *QualityRule) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *QualityRule) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type RuleReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule *QualityRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RuleReply) Reset() { + *x = RuleReply{} + mi := &file_quality_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RuleReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RuleReply) ProtoMessage() {} + +func (x *RuleReply) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RuleReply.ProtoReflect.Descriptor instead. +func (*RuleReply) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{1} +} + +func (x *RuleReply) GetRule() *QualityRule { + if x != nil { + return x.Rule + } + return nil +} + +type RuleListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*QualityRule `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RuleListReply) Reset() { + *x = RuleListReply{} + mi := &file_quality_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RuleListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RuleListReply) ProtoMessage() {} + +func (x *RuleListReply) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RuleListReply.ProtoReflect.Descriptor instead. +func (*RuleListReply) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{2} +} + +func (x *RuleListReply) GetList() []*QualityRule { + if x != nil { + return x.List + } + return nil +} + +func (x *RuleListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type CreateRuleReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + RuleType string `protobuf:"bytes,2,opt,name=rule_type,json=ruleType,proto3" json:"rule_type,omitempty"` + TargetTable string `protobuf:"bytes,3,opt,name=target_table,json=targetTable,proto3" json:"target_table,omitempty"` + TargetColumn string `protobuf:"bytes,4,opt,name=target_column,json=targetColumn,proto3" json:"target_column,omitempty"` + ParamsJson string `protobuf:"bytes,5,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` + ThresholdJson string `protobuf:"bytes,6,opt,name=threshold_json,json=thresholdJson,proto3" json:"threshold_json,omitempty"` + Owner string `protobuf:"bytes,7,opt,name=owner,proto3" json:"owner,omitempty"` + Beid int64 `protobuf:"varint,8,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRuleReq) Reset() { + *x = CreateRuleReq{} + mi := &file_quality_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRuleReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRuleReq) ProtoMessage() {} + +func (x *CreateRuleReq) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRuleReq.ProtoReflect.Descriptor instead. +func (*CreateRuleReq) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateRuleReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateRuleReq) GetRuleType() string { + if x != nil { + return x.RuleType + } + return "" +} + +func (x *CreateRuleReq) GetTargetTable() string { + if x != nil { + return x.TargetTable + } + return "" +} + +func (x *CreateRuleReq) GetTargetColumn() string { + if x != nil { + return x.TargetColumn + } + return "" +} + +func (x *CreateRuleReq) GetParamsJson() string { + if x != nil { + return x.ParamsJson + } + return "" +} + +func (x *CreateRuleReq) GetThresholdJson() string { + if x != nil { + return x.ThresholdJson + } + return "" +} + +func (x *CreateRuleReq) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *CreateRuleReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_quality_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{4} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type RuleListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + RuleType string `protobuf:"bytes,2,opt,name=rule_type,json=ruleType,proto3" json:"rule_type,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RuleListReq) Reset() { + *x = RuleListReq{} + mi := &file_quality_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RuleListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RuleListReq) ProtoMessage() {} + +func (x *RuleListReq) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RuleListReq.ProtoReflect.Descriptor instead. +func (*RuleListReq) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{5} +} + +func (x *RuleListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *RuleListReq) GetRuleType() string { + if x != nil { + return x.RuleType + } + return "" +} + +func (x *RuleListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *RuleListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type CreateJobReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + DatasourceId int64 `protobuf:"varint,2,opt,name=datasource_id,json=datasourceId,proto3" json:"datasource_id,omitempty"` + DbName string `protobuf:"bytes,3,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + RuleIds string `protobuf:"bytes,4,opt,name=rule_ids,json=ruleIds,proto3" json:"rule_ids,omitempty"` //逗号分隔 + Cron string `protobuf:"bytes,5,opt,name=cron,proto3" json:"cron,omitempty"` + Beid int64 `protobuf:"varint,6,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateJobReq) Reset() { + *x = CreateJobReq{} + mi := &file_quality_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateJobReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateJobReq) ProtoMessage() {} + +func (x *CreateJobReq) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateJobReq.ProtoReflect.Descriptor instead. +func (*CreateJobReq) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateJobReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateJobReq) GetDatasourceId() int64 { + if x != nil { + return x.DatasourceId + } + return 0 +} + +func (x *CreateJobReq) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *CreateJobReq) GetRuleIds() string { + if x != nil { + return x.RuleIds + } + return "" +} + +func (x *CreateJobReq) GetCron() string { + if x != nil { + return x.Cron + } + return "" +} + +func (x *CreateJobReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type QualityJob struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + DatasourceId int64 `protobuf:"varint,3,opt,name=datasource_id,json=datasourceId,proto3" json:"datasource_id,omitempty"` + DbName string `protobuf:"bytes,4,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + RuleIds string `protobuf:"bytes,5,opt,name=rule_ids,json=ruleIds,proto3" json:"rule_ids,omitempty"` + Cron string `protobuf:"bytes,6,opt,name=cron,proto3" json:"cron,omitempty"` + LastStatus string `protobuf:"bytes,7,opt,name=last_status,json=lastStatus,proto3" json:"last_status,omitempty"` + LastRunAt int64 `protobuf:"varint,8,opt,name=last_run_at,json=lastRunAt,proto3" json:"last_run_at,omitempty"` + Beid int64 `protobuf:"varint,9,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QualityJob) Reset() { + *x = QualityJob{} + mi := &file_quality_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QualityJob) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QualityJob) ProtoMessage() {} + +func (x *QualityJob) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QualityJob.ProtoReflect.Descriptor instead. +func (*QualityJob) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{7} +} + +func (x *QualityJob) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *QualityJob) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *QualityJob) GetDatasourceId() int64 { + if x != nil { + return x.DatasourceId + } + return 0 +} + +func (x *QualityJob) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *QualityJob) GetRuleIds() string { + if x != nil { + return x.RuleIds + } + return "" +} + +func (x *QualityJob) GetCron() string { + if x != nil { + return x.Cron + } + return "" +} + +func (x *QualityJob) GetLastStatus() string { + if x != nil { + return x.LastStatus + } + return "" +} + +func (x *QualityJob) GetLastRunAt() int64 { + if x != nil { + return x.LastRunAt + } + return 0 +} + +func (x *QualityJob) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type JobReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Job *QualityJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobReply) Reset() { + *x = JobReply{} + mi := &file_quality_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobReply) ProtoMessage() {} + +func (x *JobReply) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobReply.ProtoReflect.Descriptor instead. +func (*JobReply) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{8} +} + +func (x *JobReply) GetJob() *QualityJob { + if x != nil { + return x.Job + } + return nil +} + +type JobListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*QualityJob `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobListReply) Reset() { + *x = JobListReply{} + mi := &file_quality_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobListReply) ProtoMessage() {} + +func (x *JobListReply) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobListReply.ProtoReflect.Descriptor instead. +func (*JobListReply) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{9} +} + +func (x *JobListReply) GetList() []*QualityJob { + if x != nil { + return x.List + } + return nil +} + +func (x *JobListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type JobListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + Page int64 `protobuf:"varint,2,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobListReq) Reset() { + *x = JobListReq{} + mi := &file_quality_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobListReq) ProtoMessage() {} + +func (x *JobListReq) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobListReq.ProtoReflect.Descriptor instead. +func (*JobListReq) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{10} +} + +func (x *JobListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *JobListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *JobListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type QualityResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + JobId int64 `protobuf:"varint,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + RuleId int64 `protobuf:"varint,3,opt,name=rule_id,json=ruleId,proto3" json:"rule_id,omitempty"` + RuleType string `protobuf:"bytes,4,opt,name=rule_type,json=ruleType,proto3" json:"rule_type,omitempty"` + CheckValue string `protobuf:"bytes,5,opt,name=check_value,json=checkValue,proto3" json:"check_value,omitempty"` + PassFlag bool `protobuf:"varint,6,opt,name=pass_flag,json=passFlag,proto3" json:"pass_flag,omitempty"` + DetailJson string `protobuf:"bytes,7,opt,name=detail_json,json=detailJson,proto3" json:"detail_json,omitempty"` + RunAt int64 `protobuf:"varint,8,opt,name=run_at,json=runAt,proto3" json:"run_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QualityResult) Reset() { + *x = QualityResult{} + mi := &file_quality_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QualityResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QualityResult) ProtoMessage() {} + +func (x *QualityResult) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QualityResult.ProtoReflect.Descriptor instead. +func (*QualityResult) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{11} +} + +func (x *QualityResult) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *QualityResult) GetJobId() int64 { + if x != nil { + return x.JobId + } + return 0 +} + +func (x *QualityResult) GetRuleId() int64 { + if x != nil { + return x.RuleId + } + return 0 +} + +func (x *QualityResult) GetRuleType() string { + if x != nil { + return x.RuleType + } + return "" +} + +func (x *QualityResult) GetCheckValue() string { + if x != nil { + return x.CheckValue + } + return "" +} + +func (x *QualityResult) GetPassFlag() bool { + if x != nil { + return x.PassFlag + } + return false +} + +func (x *QualityResult) GetDetailJson() string { + if x != nil { + return x.DetailJson + } + return "" +} + +func (x *QualityResult) GetRunAt() int64 { + if x != nil { + return x.RunAt + } + return 0 +} + +type RunJobReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId int64 `protobuf:"varint,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + TotalRules int64 `protobuf:"varint,2,opt,name=total_rules,json=totalRules,proto3" json:"total_rules,omitempty"` + Passed int64 `protobuf:"varint,3,opt,name=passed,proto3" json:"passed,omitempty"` + LastStatus string `protobuf:"bytes,4,opt,name=last_status,json=lastStatus,proto3" json:"last_status,omitempty"` //success/failed/partial + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunJobReply) Reset() { + *x = RunJobReply{} + mi := &file_quality_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunJobReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunJobReply) ProtoMessage() {} + +func (x *RunJobReply) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunJobReply.ProtoReflect.Descriptor instead. +func (*RunJobReply) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{12} +} + +func (x *RunJobReply) GetJobId() int64 { + if x != nil { + return x.JobId + } + return 0 +} + +func (x *RunJobReply) GetTotalRules() int64 { + if x != nil { + return x.TotalRules + } + return 0 +} + +func (x *RunJobReply) GetPassed() int64 { + if x != nil { + return x.Passed + } + return 0 +} + +func (x *RunJobReply) GetLastStatus() string { + if x != nil { + return x.LastStatus + } + return "" +} + +type ResultListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*QualityResult `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResultListReply) Reset() { + *x = ResultListReply{} + mi := &file_quality_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResultListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResultListReply) ProtoMessage() {} + +func (x *ResultListReply) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResultListReply.ProtoReflect.Descriptor instead. +func (*ResultListReply) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{13} +} + +func (x *ResultListReply) GetList() []*QualityResult { + if x != nil { + return x.List + } + return nil +} + +func (x *ResultListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type ResultListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId int64 `protobuf:"varint,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + PassFilter int64 `protobuf:"varint,2,opt,name=pass_filter,json=passFilter,proto3" json:"pass_filter,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResultListReq) Reset() { + *x = ResultListReq{} + mi := &file_quality_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResultListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResultListReq) ProtoMessage() {} + +func (x *ResultListReq) ProtoReflect() protoreflect.Message { + mi := &file_quality_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResultListReq.ProtoReflect.Descriptor instead. +func (*ResultListReq) Descriptor() ([]byte, []int) { + return file_quality_proto_rawDescGZIP(), []int{14} +} + +func (x *ResultListReq) GetJobId() int64 { + if x != nil { + return x.JobId + } + return 0 +} + +func (x *ResultListReq) GetPassFilter() int64 { + if x != nil { + return x.PassFilter + } + return 0 +} + +func (x *ResultListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ResultListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +var File_quality_proto protoreflect.FileDescriptor + +const file_quality_proto_rawDesc = "" + + "\n" + + "\rquality.proto\x12\aquality\"\xa0\x02\n" + + "\vQualityRule\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" + + "\trule_type\x18\x03 \x01(\tR\bruleType\x12!\n" + + "\ftarget_table\x18\x04 \x01(\tR\vtargetTable\x12#\n" + + "\rtarget_column\x18\x05 \x01(\tR\ftargetColumn\x12\x1f\n" + + "\vparams_json\x18\x06 \x01(\tR\n" + + "paramsJson\x12%\n" + + "\x0ethreshold_json\x18\a \x01(\tR\rthresholdJson\x12\x16\n" + + "\x06status\x18\b \x01(\x05R\x06status\x12\x14\n" + + "\x05owner\x18\t \x01(\tR\x05owner\x12\x12\n" + + "\x04beid\x18\n" + + " \x01(\x03R\x04beid\"5\n" + + "\tRuleReply\x12(\n" + + "\x04rule\x18\x01 \x01(\v2\x14.quality.QualityRuleR\x04rule\"O\n" + + "\rRuleListReply\x12(\n" + + "\x04list\x18\x01 \x03(\v2\x14.quality.QualityRuleR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\xfa\x01\n" + + "\rCreateRuleReq\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1b\n" + + "\trule_type\x18\x02 \x01(\tR\bruleType\x12!\n" + + "\ftarget_table\x18\x03 \x01(\tR\vtargetTable\x12#\n" + + "\rtarget_column\x18\x04 \x01(\tR\ftargetColumn\x12\x1f\n" + + "\vparams_json\x18\x05 \x01(\tR\n" + + "paramsJson\x12%\n" + + "\x0ethreshold_json\x18\x06 \x01(\tR\rthresholdJson\x12\x14\n" + + "\x05owner\x18\a \x01(\tR\x05owner\x12\x12\n" + + "\x04beid\x18\b \x01(\x03R\x04beid\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"f\n" + + "\vRuleListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x1b\n" + + "\trule_type\x18\x02 \x01(\tR\bruleType\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"\xa3\x01\n" + + "\fCreateJobReq\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + + "\rdatasource_id\x18\x02 \x01(\x03R\fdatasourceId\x12\x17\n" + + "\adb_name\x18\x03 \x01(\tR\x06dbName\x12\x19\n" + + "\brule_ids\x18\x04 \x01(\tR\aruleIds\x12\x12\n" + + "\x04cron\x18\x05 \x01(\tR\x04cron\x12\x12\n" + + "\x04beid\x18\x06 \x01(\x03R\x04beid\"\xf2\x01\n" + + "\n" + + "QualityJob\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12#\n" + + "\rdatasource_id\x18\x03 \x01(\x03R\fdatasourceId\x12\x17\n" + + "\adb_name\x18\x04 \x01(\tR\x06dbName\x12\x19\n" + + "\brule_ids\x18\x05 \x01(\tR\aruleIds\x12\x12\n" + + "\x04cron\x18\x06 \x01(\tR\x04cron\x12\x1f\n" + + "\vlast_status\x18\a \x01(\tR\n" + + "lastStatus\x12\x1e\n" + + "\vlast_run_at\x18\b \x01(\x03R\tlastRunAt\x12\x12\n" + + "\x04beid\x18\t \x01(\x03R\x04beid\"1\n" + + "\bJobReply\x12%\n" + + "\x03job\x18\x01 \x01(\v2\x13.quality.QualityJobR\x03job\"M\n" + + "\fJobListReply\x12'\n" + + "\x04list\x18\x01 \x03(\v2\x13.quality.QualityJobR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"H\n" + + "\n" + + "JobListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x12\n" + + "\x04page\x18\x02 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x03 \x01(\x03R\x04size\"\xe2\x01\n" + + "\rQualityResult\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x15\n" + + "\x06job_id\x18\x02 \x01(\x03R\x05jobId\x12\x17\n" + + "\arule_id\x18\x03 \x01(\x03R\x06ruleId\x12\x1b\n" + + "\trule_type\x18\x04 \x01(\tR\bruleType\x12\x1f\n" + + "\vcheck_value\x18\x05 \x01(\tR\n" + + "checkValue\x12\x1b\n" + + "\tpass_flag\x18\x06 \x01(\bR\bpassFlag\x12\x1f\n" + + "\vdetail_json\x18\a \x01(\tR\n" + + "detailJson\x12\x15\n" + + "\x06run_at\x18\b \x01(\x03R\x05runAt\"~\n" + + "\vRunJobReply\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\x03R\x05jobId\x12\x1f\n" + + "\vtotal_rules\x18\x02 \x01(\x03R\n" + + "totalRules\x12\x16\n" + + "\x06passed\x18\x03 \x01(\x03R\x06passed\x12\x1f\n" + + "\vlast_status\x18\x04 \x01(\tR\n" + + "lastStatus\"S\n" + + "\x0fResultListReply\x12*\n" + + "\x04list\x18\x01 \x03(\v2\x16.quality.QualityResultR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"o\n" + + "\rResultListReq\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\x03R\x05jobId\x12\x1f\n" + + "\vpass_filter\x18\x02 \x01(\x03R\n" + + "passFilter\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size2\x8c\x03\n" + + "\aQuality\x128\n" + + "\n" + + "CreateRule\x12\x16.quality.CreateRuleReq\x1a\x12.quality.RuleReply\x12,\n" + + "\n" + + "DeleteRule\x12\x0e.quality.IdReq\x1a\x0e.quality.IdReq\x129\n" + + "\tListRules\x12\x14.quality.RuleListReq\x1a\x16.quality.RuleListReply\x125\n" + + "\tCreateJob\x12\x15.quality.CreateJobReq\x1a\x11.quality.JobReply\x126\n" + + "\bListJobs\x12\x13.quality.JobListReq\x1a\x15.quality.JobListReply\x12.\n" + + "\x06RunJob\x12\x0e.quality.IdReq\x1a\x14.quality.RunJobReply\x12?\n" + + "\vListResults\x12\x16.quality.ResultListReq\x1a\x18.quality.ResultListReplyB\vZ\t./qualityb\x06proto3" + +var ( + file_quality_proto_rawDescOnce sync.Once + file_quality_proto_rawDescData []byte +) + +func file_quality_proto_rawDescGZIP() []byte { + file_quality_proto_rawDescOnce.Do(func() { + file_quality_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_quality_proto_rawDesc), len(file_quality_proto_rawDesc))) + }) + return file_quality_proto_rawDescData +} + +var file_quality_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_quality_proto_goTypes = []any{ + (*QualityRule)(nil), // 0: quality.QualityRule + (*RuleReply)(nil), // 1: quality.RuleReply + (*RuleListReply)(nil), // 2: quality.RuleListReply + (*CreateRuleReq)(nil), // 3: quality.CreateRuleReq + (*IdReq)(nil), // 4: quality.IdReq + (*RuleListReq)(nil), // 5: quality.RuleListReq + (*CreateJobReq)(nil), // 6: quality.CreateJobReq + (*QualityJob)(nil), // 7: quality.QualityJob + (*JobReply)(nil), // 8: quality.JobReply + (*JobListReply)(nil), // 9: quality.JobListReply + (*JobListReq)(nil), // 10: quality.JobListReq + (*QualityResult)(nil), // 11: quality.QualityResult + (*RunJobReply)(nil), // 12: quality.RunJobReply + (*ResultListReply)(nil), // 13: quality.ResultListReply + (*ResultListReq)(nil), // 14: quality.ResultListReq +} +var file_quality_proto_depIdxs = []int32{ + 0, // 0: quality.RuleReply.rule:type_name -> quality.QualityRule + 0, // 1: quality.RuleListReply.list:type_name -> quality.QualityRule + 7, // 2: quality.JobReply.job:type_name -> quality.QualityJob + 7, // 3: quality.JobListReply.list:type_name -> quality.QualityJob + 11, // 4: quality.ResultListReply.list:type_name -> quality.QualityResult + 3, // 5: quality.Quality.CreateRule:input_type -> quality.CreateRuleReq + 4, // 6: quality.Quality.DeleteRule:input_type -> quality.IdReq + 5, // 7: quality.Quality.ListRules:input_type -> quality.RuleListReq + 6, // 8: quality.Quality.CreateJob:input_type -> quality.CreateJobReq + 10, // 9: quality.Quality.ListJobs:input_type -> quality.JobListReq + 4, // 10: quality.Quality.RunJob:input_type -> quality.IdReq + 14, // 11: quality.Quality.ListResults:input_type -> quality.ResultListReq + 1, // 12: quality.Quality.CreateRule:output_type -> quality.RuleReply + 4, // 13: quality.Quality.DeleteRule:output_type -> quality.IdReq + 2, // 14: quality.Quality.ListRules:output_type -> quality.RuleListReply + 8, // 15: quality.Quality.CreateJob:output_type -> quality.JobReply + 9, // 16: quality.Quality.ListJobs:output_type -> quality.JobListReply + 12, // 17: quality.Quality.RunJob:output_type -> quality.RunJobReply + 13, // 18: quality.Quality.ListResults:output_type -> quality.ResultListReply + 12, // [12:19] is the sub-list for method output_type + 5, // [5:12] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_quality_proto_init() } +func file_quality_proto_init() { + if File_quality_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_quality_proto_rawDesc), len(file_quality_proto_rawDesc)), + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_quality_proto_goTypes, + DependencyIndexes: file_quality_proto_depIdxs, + MessageInfos: file_quality_proto_msgTypes, + }.Build() + File_quality_proto = out.File + file_quality_proto_goTypes = nil + file_quality_proto_depIdxs = nil +} diff --git a/quality/rpc/quality/quality_grpc.pb.go b/quality/rpc/quality/quality_grpc.pb.go new file mode 100644 index 0000000..a938928 --- /dev/null +++ b/quality/rpc/quality/quality_grpc.pb.go @@ -0,0 +1,363 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: quality.proto + +package quality + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Quality_CreateRule_FullMethodName = "/quality.Quality/CreateRule" + Quality_DeleteRule_FullMethodName = "/quality.Quality/DeleteRule" + Quality_ListRules_FullMethodName = "/quality.Quality/ListRules" + Quality_CreateJob_FullMethodName = "/quality.Quality/CreateJob" + Quality_ListJobs_FullMethodName = "/quality.Quality/ListJobs" + Quality_RunJob_FullMethodName = "/quality.Quality/RunJob" + Quality_ListResults_FullMethodName = "/quality.Quality/ListResults" +) + +// QualityClient is the client API for Quality service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type QualityClient interface { + //创建规则 + CreateRule(ctx context.Context, in *CreateRuleReq, opts ...grpc.CallOption) (*RuleReply, error) + //删除规则 + DeleteRule(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + //规则列表 + ListRules(ctx context.Context, in *RuleListReq, opts ...grpc.CallOption) (*RuleListReply, error) + //创建任务 + CreateJob(ctx context.Context, in *CreateJobReq, opts ...grpc.CallOption) (*JobReply, error) + //任务列表 + ListJobs(ctx context.Context, in *JobListReq, opts ...grpc.CallOption) (*JobListReply, error) + //执行任务(同步跑完规则并落结果) + RunJob(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*RunJobReply, error) + //结果列表 + ListResults(ctx context.Context, in *ResultListReq, opts ...grpc.CallOption) (*ResultListReply, error) +} + +type qualityClient struct { + cc grpc.ClientConnInterface +} + +func NewQualityClient(cc grpc.ClientConnInterface) QualityClient { + return &qualityClient{cc} +} + +func (c *qualityClient) CreateRule(ctx context.Context, in *CreateRuleReq, opts ...grpc.CallOption) (*RuleReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RuleReply) + err := c.cc.Invoke(ctx, Quality_CreateRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *qualityClient) DeleteRule(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdReq) + err := c.cc.Invoke(ctx, Quality_DeleteRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *qualityClient) ListRules(ctx context.Context, in *RuleListReq, opts ...grpc.CallOption) (*RuleListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RuleListReply) + err := c.cc.Invoke(ctx, Quality_ListRules_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *qualityClient) CreateJob(ctx context.Context, in *CreateJobReq, opts ...grpc.CallOption) (*JobReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(JobReply) + err := c.cc.Invoke(ctx, Quality_CreateJob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *qualityClient) ListJobs(ctx context.Context, in *JobListReq, opts ...grpc.CallOption) (*JobListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(JobListReply) + err := c.cc.Invoke(ctx, Quality_ListJobs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *qualityClient) RunJob(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*RunJobReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RunJobReply) + err := c.cc.Invoke(ctx, Quality_RunJob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *qualityClient) ListResults(ctx context.Context, in *ResultListReq, opts ...grpc.CallOption) (*ResultListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResultListReply) + err := c.cc.Invoke(ctx, Quality_ListResults_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// QualityServer is the server API for Quality service. +// All implementations must embed UnimplementedQualityServer +// for forward compatibility. +type QualityServer interface { + //创建规则 + CreateRule(context.Context, *CreateRuleReq) (*RuleReply, error) + //删除规则 + DeleteRule(context.Context, *IdReq) (*IdReq, error) + //规则列表 + ListRules(context.Context, *RuleListReq) (*RuleListReply, error) + //创建任务 + CreateJob(context.Context, *CreateJobReq) (*JobReply, error) + //任务列表 + ListJobs(context.Context, *JobListReq) (*JobListReply, error) + //执行任务(同步跑完规则并落结果) + RunJob(context.Context, *IdReq) (*RunJobReply, error) + //结果列表 + ListResults(context.Context, *ResultListReq) (*ResultListReply, error) + mustEmbedUnimplementedQualityServer() +} + +// UnimplementedQualityServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedQualityServer struct{} + +func (UnimplementedQualityServer) CreateRule(context.Context, *CreateRuleReq) (*RuleReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateRule not implemented") +} +func (UnimplementedQualityServer) DeleteRule(context.Context, *IdReq) (*IdReq, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteRule not implemented") +} +func (UnimplementedQualityServer) ListRules(context.Context, *RuleListReq) (*RuleListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListRules not implemented") +} +func (UnimplementedQualityServer) CreateJob(context.Context, *CreateJobReq) (*JobReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateJob not implemented") +} +func (UnimplementedQualityServer) ListJobs(context.Context, *JobListReq) (*JobListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListJobs not implemented") +} +func (UnimplementedQualityServer) RunJob(context.Context, *IdReq) (*RunJobReply, error) { + return nil, status.Error(codes.Unimplemented, "method RunJob not implemented") +} +func (UnimplementedQualityServer) ListResults(context.Context, *ResultListReq) (*ResultListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListResults not implemented") +} +func (UnimplementedQualityServer) mustEmbedUnimplementedQualityServer() {} +func (UnimplementedQualityServer) testEmbeddedByValue() {} + +// UnsafeQualityServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to QualityServer will +// result in compilation errors. +type UnsafeQualityServer interface { + mustEmbedUnimplementedQualityServer() +} + +func RegisterQualityServer(s grpc.ServiceRegistrar, srv QualityServer) { + // If the following call panics, it indicates UnimplementedQualityServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Quality_ServiceDesc, srv) +} + +func _Quality_CreateRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateRuleReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QualityServer).CreateRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quality_CreateRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QualityServer).CreateRule(ctx, req.(*CreateRuleReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Quality_DeleteRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QualityServer).DeleteRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quality_DeleteRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QualityServer).DeleteRule(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Quality_ListRules_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RuleListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QualityServer).ListRules(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quality_ListRules_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QualityServer).ListRules(ctx, req.(*RuleListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Quality_CreateJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateJobReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QualityServer).CreateJob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quality_CreateJob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QualityServer).CreateJob(ctx, req.(*CreateJobReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Quality_ListJobs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(JobListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QualityServer).ListJobs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quality_ListJobs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QualityServer).ListJobs(ctx, req.(*JobListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Quality_RunJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QualityServer).RunJob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quality_RunJob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QualityServer).RunJob(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Quality_ListResults_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ResultListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QualityServer).ListResults(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quality_ListResults_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QualityServer).ListResults(ctx, req.(*ResultListReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Quality_ServiceDesc is the grpc.ServiceDesc for Quality service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Quality_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "quality.Quality", + HandlerType: (*QualityServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateRule", + Handler: _Quality_CreateRule_Handler, + }, + { + MethodName: "DeleteRule", + Handler: _Quality_DeleteRule_Handler, + }, + { + MethodName: "ListRules", + Handler: _Quality_ListRules_Handler, + }, + { + MethodName: "CreateJob", + Handler: _Quality_CreateJob_Handler, + }, + { + MethodName: "ListJobs", + Handler: _Quality_ListJobs_Handler, + }, + { + MethodName: "RunJob", + Handler: _Quality_RunJob_Handler, + }, + { + MethodName: "ListResults", + Handler: _Quality_ListResults_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "quality.proto", +} diff --git a/quality/rpc/qualityClient/quality.go b/quality/rpc/qualityClient/quality.go new file mode 100644 index 0000000..9612f7a --- /dev/null +++ b/quality/rpc/qualityClient/quality.go @@ -0,0 +1,101 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: quality.proto + +package qualityClient + +import ( + "context" + + "datacenter/quality/rpc/quality" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + CreateJobReq = quality.CreateJobReq + CreateRuleReq = quality.CreateRuleReq + IdReq = quality.IdReq + JobListReply = quality.JobListReply + JobListReq = quality.JobListReq + JobReply = quality.JobReply + QualityJob = quality.QualityJob + QualityResult = quality.QualityResult + QualityRule = quality.QualityRule + ResultListReply = quality.ResultListReply + ResultListReq = quality.ResultListReq + RuleListReply = quality.RuleListReply + RuleListReq = quality.RuleListReq + RuleReply = quality.RuleReply + RunJobReply = quality.RunJobReply + + Quality interface { + // 创建规则 + CreateRule(ctx context.Context, in *CreateRuleReq, opts ...grpc.CallOption) (*RuleReply, error) + // 删除规则 + DeleteRule(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + // 规则列表 + ListRules(ctx context.Context, in *RuleListReq, opts ...grpc.CallOption) (*RuleListReply, error) + // 创建任务 + CreateJob(ctx context.Context, in *CreateJobReq, opts ...grpc.CallOption) (*JobReply, error) + // 任务列表 + ListJobs(ctx context.Context, in *JobListReq, opts ...grpc.CallOption) (*JobListReply, error) + // 执行任务(同步跑完规则并落结果) + RunJob(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*RunJobReply, error) + // 结果列表 + ListResults(ctx context.Context, in *ResultListReq, opts ...grpc.CallOption) (*ResultListReply, error) + } + + defaultQuality struct { + cli zrpc.Client + } +) + +func NewQuality(cli zrpc.Client) Quality { + return &defaultQuality{ + cli: cli, + } +} + +// 创建规则 +func (m *defaultQuality) CreateRule(ctx context.Context, in *CreateRuleReq, opts ...grpc.CallOption) (*RuleReply, error) { + client := quality.NewQualityClient(m.cli.Conn()) + return client.CreateRule(ctx, in, opts...) +} + +// 删除规则 +func (m *defaultQuality) DeleteRule(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + client := quality.NewQualityClient(m.cli.Conn()) + return client.DeleteRule(ctx, in, opts...) +} + +// 规则列表 +func (m *defaultQuality) ListRules(ctx context.Context, in *RuleListReq, opts ...grpc.CallOption) (*RuleListReply, error) { + client := quality.NewQualityClient(m.cli.Conn()) + return client.ListRules(ctx, in, opts...) +} + +// 创建任务 +func (m *defaultQuality) CreateJob(ctx context.Context, in *CreateJobReq, opts ...grpc.CallOption) (*JobReply, error) { + client := quality.NewQualityClient(m.cli.Conn()) + return client.CreateJob(ctx, in, opts...) +} + +// 任务列表 +func (m *defaultQuality) ListJobs(ctx context.Context, in *JobListReq, opts ...grpc.CallOption) (*JobListReply, error) { + client := quality.NewQualityClient(m.cli.Conn()) + return client.ListJobs(ctx, in, opts...) +} + +// 执行任务(同步跑完规则并落结果) +func (m *defaultQuality) RunJob(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*RunJobReply, error) { + client := quality.NewQualityClient(m.cli.Conn()) + return client.RunJob(ctx, in, opts...) +} + +// 结果列表 +func (m *defaultQuality) ListResults(ctx context.Context, in *ResultListReq, opts ...grpc.CallOption) (*ResultListReply, error) { + client := quality.NewQualityClient(m.cli.Conn()) + return client.ListResults(ctx, in, opts...) +} diff --git a/tag/rpc/etc/tag.example.yaml b/tag/rpc/etc/tag.example.yaml new file mode 100644 index 0000000..18850d4 --- /dev/null +++ b/tag/rpc/etc/tag.example.yaml @@ -0,0 +1,8 @@ +Name: tag.rpc +ListenOn: 0.0.0.0:8091 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: tag.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/tag/rpc/internal/config/config.go b/tag/rpc/internal/config/config.go new file mode 100755 index 0000000..96863e9 --- /dev/null +++ b/tag/rpc/internal/config/config.go @@ -0,0 +1,10 @@ +package config + +import "github.com/zeromicro/go-zero/zrpc" + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } +} diff --git a/tag/rpc/internal/logic/createObjectLogic.go b/tag/rpc/internal/logic/createObjectLogic.go new file mode 100644 index 0000000..5dbe688 --- /dev/null +++ b/tag/rpc/internal/logic/createObjectLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateObjectLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateObjectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateObjectLogic { + return &CreateObjectLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建标签对象 +func (l *CreateObjectLogic) CreateObject(in *tag.CreateObjectReq) (*tag.ObjectReply, error) { + // todo: add your logic here and delete this line + + return &tag.ObjectReply{}, nil +} diff --git a/tag/rpc/internal/logic/createTagLogic.go b/tag/rpc/internal/logic/createTagLogic.go new file mode 100644 index 0000000..61942d9 --- /dev/null +++ b/tag/rpc/internal/logic/createTagLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateTagLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateTagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateTagLogic { + return &CreateTagLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建标签定义 +func (l *CreateTagLogic) CreateTag(in *tag.CreateTagReq) (*tag.TagDefReply, error) { + // todo: add your logic here and delete this line + + return &tag.TagDefReply{}, nil +} diff --git a/tag/rpc/internal/logic/deleteTagLogic.go b/tag/rpc/internal/logic/deleteTagLogic.go new file mode 100644 index 0000000..766c96c --- /dev/null +++ b/tag/rpc/internal/logic/deleteTagLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteTagLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeleteTagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteTagLogic { + return &DeleteTagLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 删除标签定义 +func (l *DeleteTagLogic) DeleteTag(in *tag.IdReq) (*tag.IdReq, error) { + // todo: add your logic here and delete this line + + return &tag.IdReq{}, nil +} diff --git a/tag/rpc/internal/logic/getObjectTagsLogic.go b/tag/rpc/internal/logic/getObjectTagsLogic.go new file mode 100644 index 0000000..3be618c --- /dev/null +++ b/tag/rpc/internal/logic/getObjectTagsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetObjectTagsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetObjectTagsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetObjectTagsLogic { + return &GetObjectTagsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 查对象标签画像 +func (l *GetObjectTagsLogic) GetObjectTags(in *tag.ObjectTagsReq) (*tag.ObjectTagsReply, error) { + // todo: add your logic here and delete this line + + return &tag.ObjectTagsReply{}, nil +} diff --git a/tag/rpc/internal/logic/listObjectsLogic.go b/tag/rpc/internal/logic/listObjectsLogic.go new file mode 100644 index 0000000..89d16a6 --- /dev/null +++ b/tag/rpc/internal/logic/listObjectsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListObjectsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListObjectsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListObjectsLogic { + return &ListObjectsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 对象列表 +func (l *ListObjectsLogic) ListObjects(in *tag.ObjectListReq) (*tag.ObjectListReply, error) { + // todo: add your logic here and delete this line + + return &tag.ObjectListReply{}, nil +} diff --git a/tag/rpc/internal/logic/listTagsLogic.go b/tag/rpc/internal/logic/listTagsLogic.go new file mode 100644 index 0000000..1b90f3c --- /dev/null +++ b/tag/rpc/internal/logic/listTagsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListTagsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListTagsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListTagsLogic { + return &ListTagsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 标签定义列表 +func (l *ListTagsLogic) ListTags(in *tag.TagDefListReq) (*tag.TagDefListReply, error) { + // todo: add your logic here and delete this line + + return &tag.TagDefListReply{}, nil +} diff --git a/tag/rpc/internal/logic/taggingLogic.go b/tag/rpc/internal/logic/taggingLogic.go new file mode 100644 index 0000000..5b5ea2f --- /dev/null +++ b/tag/rpc/internal/logic/taggingLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TaggingLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewTaggingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TaggingLogic { + return &TaggingLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 执行打标(规则 SQL 跑目标库,结果写 dc_tag_value) +func (l *TaggingLogic) Tagging(in *tag.TaggingReq) (*tag.TaggingReply, error) { + // todo: add your logic here and delete this line + + return &tag.TaggingReply{}, nil +} diff --git a/tag/rpc/internal/server/tagServer.go b/tag/rpc/internal/server/tagServer.go new file mode 100644 index 0000000..0725dce --- /dev/null +++ b/tag/rpc/internal/server/tagServer.go @@ -0,0 +1,66 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: tag.proto + +package server + +import ( + "context" + + "datacenter/tag/rpc/internal/logic" + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" +) + +type TagServer struct { + svcCtx *svc.ServiceContext + tag.UnimplementedTagServer +} + +func NewTagServer(svcCtx *svc.ServiceContext) *TagServer { + return &TagServer{ + svcCtx: svcCtx, + } +} + +// 创建标签对象 +func (s *TagServer) CreateObject(ctx context.Context, in *tag.CreateObjectReq) (*tag.ObjectReply, error) { + l := logic.NewCreateObjectLogic(ctx, s.svcCtx) + return l.CreateObject(in) +} + +// 对象列表 +func (s *TagServer) ListObjects(ctx context.Context, in *tag.ObjectListReq) (*tag.ObjectListReply, error) { + l := logic.NewListObjectsLogic(ctx, s.svcCtx) + return l.ListObjects(in) +} + +// 创建标签定义 +func (s *TagServer) CreateTag(ctx context.Context, in *tag.CreateTagReq) (*tag.TagDefReply, error) { + l := logic.NewCreateTagLogic(ctx, s.svcCtx) + return l.CreateTag(in) +} + +// 标签定义列表 +func (s *TagServer) ListTags(ctx context.Context, in *tag.TagDefListReq) (*tag.TagDefListReply, error) { + l := logic.NewListTagsLogic(ctx, s.svcCtx) + return l.ListTags(in) +} + +// 删除标签定义 +func (s *TagServer) DeleteTag(ctx context.Context, in *tag.IdReq) (*tag.IdReq, error) { + l := logic.NewDeleteTagLogic(ctx, s.svcCtx) + return l.DeleteTag(in) +} + +// 执行打标(规则 SQL 跑目标库,结果写 dc_tag_value) +func (s *TagServer) Tagging(ctx context.Context, in *tag.TaggingReq) (*tag.TaggingReply, error) { + l := logic.NewTaggingLogic(ctx, s.svcCtx) + return l.Tagging(in) +} + +// 查对象标签画像 +func (s *TagServer) GetObjectTags(ctx context.Context, in *tag.ObjectTagsReq) (*tag.ObjectTagsReply, error) { + l := logic.NewGetObjectTagsLogic(ctx, s.svcCtx) + return l.GetObjectTags(in) +} diff --git a/tag/rpc/internal/svc/serviceContext.go b/tag/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..814124b --- /dev/null +++ b/tag/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/tag/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/tag/rpc/tag.go b/tag/rpc/tag.go new file mode 100644 index 0000000..9a8220a --- /dev/null +++ b/tag/rpc/tag.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/tag/rpc/internal/config" + "datacenter/tag/rpc/internal/server" + "datacenter/tag/rpc/internal/svc" + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/tag.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + tag.RegisterTagServer(grpcServer, server.NewTagServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/tag/rpc/tag.proto b/tag/rpc/tag.proto new file mode 100644 index 0000000..fc94b01 --- /dev/null +++ b/tag/rpc/tag.proto @@ -0,0 +1,106 @@ +syntax = "proto3"; + +package tag; + +option go_package = "./tag"; + +message TagObject { + int64 id = 1; + string object_type = 2; //user/member/goods + string name = 3; + int64 ds_id = 4; + string db_name = 5; + string table_name = 6; + string pk_column = 7; + int64 beid = 8; +} +message ObjectReply { TagObject object = 1; } +message ObjectListReply { + repeated TagObject list = 1; + int64 total = 2; +} + +message CreateObjectReq { + string object_type = 1; + string name = 2; + int64 ds_id = 3; + string db_name = 4; + string table_name = 5; + string pk_column = 6; + int64 beid = 7; +} +message ObjectListReq { int64 beid = 1; string object_type = 2; int64 page = 3; int64 size = 4; } +message IdReq { int64 id = 1; int64 beid = 2; } + +message TagDef { + int64 id = 1; + int64 object_id = 2; + string tag_key = 3; + string tag_name = 4; + string tag_type = 5; //rule/fact/model + string rule_sql = 6; //select as object_id, as tag_value from ... + string refresh_cron = 7; + int32 status = 8; + int64 beid = 9; +} +message TagDefReply { TagDef def = 1; } +message TagDefListReply { + repeated TagDef list = 1; + int64 total = 2; +} + +message CreateTagReq { + int64 object_id = 1; + string tag_key = 2; + string tag_name = 3; + string tag_type = 4; + string rule_sql = 5; + string refresh_cron = 6; + int64 beid = 7; +} +message TagDefListReq { + int64 beid = 1; + int64 object_id = 2; + int64 page = 3; + int64 size = 4; +} + +message TaggingReq { + int64 tag_id = 1; //为空则对 object 全部 rule 标签打标 + int64 object_id = 2; + int64 beid = 3; +} +message TaggingReply { + int64 tagged_rows = 1; //本次写入标签值行数 + int64 tag_id = 2; +} + +message ObjectTagsReq { + string object_pk = 1; //对象主键值 + int64 object_id = 2; + int64 beid = 3; +} +message TagValueItem { + string tag_key = 1; + string tag_name = 2; + string tag_value = 3; + int64 tagged_at = 4; +} +message ObjectTagsReply { repeated TagValueItem tags = 1; } + +service Tag { + //创建标签对象 + rpc CreateObject(CreateObjectReq) returns (ObjectReply); + //对象列表 + rpc ListObjects(ObjectListReq) returns (ObjectListReply); + //创建标签定义 + rpc CreateTag(CreateTagReq) returns (TagDefReply); + //标签定义列表 + rpc ListTags(TagDefListReq) returns (TagDefListReply); + //删除标签定义 + rpc DeleteTag(IdReq) returns (IdReq); + //执行打标(规则 SQL 跑目标库,结果写 dc_tag_value) + rpc Tagging(TaggingReq) returns (TaggingReply); + //查对象标签画像 + rpc GetObjectTags(ObjectTagsReq) returns (ObjectTagsReply); +} diff --git a/tag/rpc/tag/tag.pb.go b/tag/rpc/tag/tag.pb.go new file mode 100644 index 0000000..8143519 --- /dev/null +++ b/tag/rpc/tag/tag.pb.go @@ -0,0 +1,1259 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: tag.proto + +package tag + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TagObject struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + ObjectType string `protobuf:"bytes,2,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"` //user/member/goods + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + DsId int64 `protobuf:"varint,4,opt,name=ds_id,json=dsId,proto3" json:"ds_id,omitempty"` + DbName string `protobuf:"bytes,5,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + TableName string `protobuf:"bytes,6,opt,name=table_name,json=tableName,proto3" json:"table_name,omitempty"` + PkColumn string `protobuf:"bytes,7,opt,name=pk_column,json=pkColumn,proto3" json:"pk_column,omitempty"` + Beid int64 `protobuf:"varint,8,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagObject) Reset() { + *x = TagObject{} + mi := &file_tag_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagObject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagObject) ProtoMessage() {} + +func (x *TagObject) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagObject.ProtoReflect.Descriptor instead. +func (*TagObject) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{0} +} + +func (x *TagObject) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *TagObject) GetObjectType() string { + if x != nil { + return x.ObjectType + } + return "" +} + +func (x *TagObject) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TagObject) GetDsId() int64 { + if x != nil { + return x.DsId + } + return 0 +} + +func (x *TagObject) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *TagObject) GetTableName() string { + if x != nil { + return x.TableName + } + return "" +} + +func (x *TagObject) GetPkColumn() string { + if x != nil { + return x.PkColumn + } + return "" +} + +func (x *TagObject) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ObjectReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Object *TagObject `protobuf:"bytes,1,opt,name=object,proto3" json:"object,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObjectReply) Reset() { + *x = ObjectReply{} + mi := &file_tag_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObjectReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObjectReply) ProtoMessage() {} + +func (x *ObjectReply) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ObjectReply.ProtoReflect.Descriptor instead. +func (*ObjectReply) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{1} +} + +func (x *ObjectReply) GetObject() *TagObject { + if x != nil { + return x.Object + } + return nil +} + +type ObjectListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*TagObject `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObjectListReply) Reset() { + *x = ObjectListReply{} + mi := &file_tag_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObjectListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObjectListReply) ProtoMessage() {} + +func (x *ObjectListReply) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ObjectListReply.ProtoReflect.Descriptor instead. +func (*ObjectListReply) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{2} +} + +func (x *ObjectListReply) GetList() []*TagObject { + if x != nil { + return x.List + } + return nil +} + +func (x *ObjectListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type CreateObjectReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjectType string `protobuf:"bytes,1,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + DsId int64 `protobuf:"varint,3,opt,name=ds_id,json=dsId,proto3" json:"ds_id,omitempty"` + DbName string `protobuf:"bytes,4,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + TableName string `protobuf:"bytes,5,opt,name=table_name,json=tableName,proto3" json:"table_name,omitempty"` + PkColumn string `protobuf:"bytes,6,opt,name=pk_column,json=pkColumn,proto3" json:"pk_column,omitempty"` + Beid int64 `protobuf:"varint,7,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateObjectReq) Reset() { + *x = CreateObjectReq{} + mi := &file_tag_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateObjectReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateObjectReq) ProtoMessage() {} + +func (x *CreateObjectReq) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateObjectReq.ProtoReflect.Descriptor instead. +func (*CreateObjectReq) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateObjectReq) GetObjectType() string { + if x != nil { + return x.ObjectType + } + return "" +} + +func (x *CreateObjectReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateObjectReq) GetDsId() int64 { + if x != nil { + return x.DsId + } + return 0 +} + +func (x *CreateObjectReq) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *CreateObjectReq) GetTableName() string { + if x != nil { + return x.TableName + } + return "" +} + +func (x *CreateObjectReq) GetPkColumn() string { + if x != nil { + return x.PkColumn + } + return "" +} + +func (x *CreateObjectReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ObjectListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + ObjectType string `protobuf:"bytes,2,opt,name=object_type,json=objectType,proto3" json:"object_type,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObjectListReq) Reset() { + *x = ObjectListReq{} + mi := &file_tag_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObjectListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObjectListReq) ProtoMessage() {} + +func (x *ObjectListReq) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ObjectListReq.ProtoReflect.Descriptor instead. +func (*ObjectListReq) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{4} +} + +func (x *ObjectListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *ObjectListReq) GetObjectType() string { + if x != nil { + return x.ObjectType + } + return "" +} + +func (x *ObjectListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ObjectListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_tag_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{5} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type TagDef struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + ObjectId int64 `protobuf:"varint,2,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"` + TagKey string `protobuf:"bytes,3,opt,name=tag_key,json=tagKey,proto3" json:"tag_key,omitempty"` + TagName string `protobuf:"bytes,4,opt,name=tag_name,json=tagName,proto3" json:"tag_name,omitempty"` + TagType string `protobuf:"bytes,5,opt,name=tag_type,json=tagType,proto3" json:"tag_type,omitempty"` //rule/fact/model + RuleSql string `protobuf:"bytes,6,opt,name=rule_sql,json=ruleSql,proto3" json:"rule_sql,omitempty"` //select as object_id, as tag_value from ... + RefreshCron string `protobuf:"bytes,7,opt,name=refresh_cron,json=refreshCron,proto3" json:"refresh_cron,omitempty"` + Status int32 `protobuf:"varint,8,opt,name=status,proto3" json:"status,omitempty"` + Beid int64 `protobuf:"varint,9,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagDef) Reset() { + *x = TagDef{} + mi := &file_tag_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagDef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagDef) ProtoMessage() {} + +func (x *TagDef) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagDef.ProtoReflect.Descriptor instead. +func (*TagDef) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{6} +} + +func (x *TagDef) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *TagDef) GetObjectId() int64 { + if x != nil { + return x.ObjectId + } + return 0 +} + +func (x *TagDef) GetTagKey() string { + if x != nil { + return x.TagKey + } + return "" +} + +func (x *TagDef) GetTagName() string { + if x != nil { + return x.TagName + } + return "" +} + +func (x *TagDef) GetTagType() string { + if x != nil { + return x.TagType + } + return "" +} + +func (x *TagDef) GetRuleSql() string { + if x != nil { + return x.RuleSql + } + return "" +} + +func (x *TagDef) GetRefreshCron() string { + if x != nil { + return x.RefreshCron + } + return "" +} + +func (x *TagDef) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *TagDef) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type TagDefReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Def *TagDef `protobuf:"bytes,1,opt,name=def,proto3" json:"def,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagDefReply) Reset() { + *x = TagDefReply{} + mi := &file_tag_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagDefReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagDefReply) ProtoMessage() {} + +func (x *TagDefReply) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagDefReply.ProtoReflect.Descriptor instead. +func (*TagDefReply) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{7} +} + +func (x *TagDefReply) GetDef() *TagDef { + if x != nil { + return x.Def + } + return nil +} + +type TagDefListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*TagDef `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagDefListReply) Reset() { + *x = TagDefListReply{} + mi := &file_tag_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagDefListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagDefListReply) ProtoMessage() {} + +func (x *TagDefListReply) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagDefListReply.ProtoReflect.Descriptor instead. +func (*TagDefListReply) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{8} +} + +func (x *TagDefListReply) GetList() []*TagDef { + if x != nil { + return x.List + } + return nil +} + +func (x *TagDefListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type CreateTagReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjectId int64 `protobuf:"varint,1,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"` + TagKey string `protobuf:"bytes,2,opt,name=tag_key,json=tagKey,proto3" json:"tag_key,omitempty"` + TagName string `protobuf:"bytes,3,opt,name=tag_name,json=tagName,proto3" json:"tag_name,omitempty"` + TagType string `protobuf:"bytes,4,opt,name=tag_type,json=tagType,proto3" json:"tag_type,omitempty"` + RuleSql string `protobuf:"bytes,5,opt,name=rule_sql,json=ruleSql,proto3" json:"rule_sql,omitempty"` + RefreshCron string `protobuf:"bytes,6,opt,name=refresh_cron,json=refreshCron,proto3" json:"refresh_cron,omitempty"` + Beid int64 `protobuf:"varint,7,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTagReq) Reset() { + *x = CreateTagReq{} + mi := &file_tag_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTagReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTagReq) ProtoMessage() {} + +func (x *CreateTagReq) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateTagReq.ProtoReflect.Descriptor instead. +func (*CreateTagReq) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{9} +} + +func (x *CreateTagReq) GetObjectId() int64 { + if x != nil { + return x.ObjectId + } + return 0 +} + +func (x *CreateTagReq) GetTagKey() string { + if x != nil { + return x.TagKey + } + return "" +} + +func (x *CreateTagReq) GetTagName() string { + if x != nil { + return x.TagName + } + return "" +} + +func (x *CreateTagReq) GetTagType() string { + if x != nil { + return x.TagType + } + return "" +} + +func (x *CreateTagReq) GetRuleSql() string { + if x != nil { + return x.RuleSql + } + return "" +} + +func (x *CreateTagReq) GetRefreshCron() string { + if x != nil { + return x.RefreshCron + } + return "" +} + +func (x *CreateTagReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type TagDefListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + ObjectId int64 `protobuf:"varint,2,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagDefListReq) Reset() { + *x = TagDefListReq{} + mi := &file_tag_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagDefListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagDefListReq) ProtoMessage() {} + +func (x *TagDefListReq) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagDefListReq.ProtoReflect.Descriptor instead. +func (*TagDefListReq) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{10} +} + +func (x *TagDefListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *TagDefListReq) GetObjectId() int64 { + if x != nil { + return x.ObjectId + } + return 0 +} + +func (x *TagDefListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *TagDefListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type TaggingReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + TagId int64 `protobuf:"varint,1,opt,name=tag_id,json=tagId,proto3" json:"tag_id,omitempty"` //为空则对 object 全部 rule 标签打标 + ObjectId int64 `protobuf:"varint,2,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"` + Beid int64 `protobuf:"varint,3,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaggingReq) Reset() { + *x = TaggingReq{} + mi := &file_tag_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaggingReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaggingReq) ProtoMessage() {} + +func (x *TaggingReq) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaggingReq.ProtoReflect.Descriptor instead. +func (*TaggingReq) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{11} +} + +func (x *TaggingReq) GetTagId() int64 { + if x != nil { + return x.TagId + } + return 0 +} + +func (x *TaggingReq) GetObjectId() int64 { + if x != nil { + return x.ObjectId + } + return 0 +} + +func (x *TaggingReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type TaggingReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaggedRows int64 `protobuf:"varint,1,opt,name=tagged_rows,json=taggedRows,proto3" json:"tagged_rows,omitempty"` //本次写入标签值行数 + TagId int64 `protobuf:"varint,2,opt,name=tag_id,json=tagId,proto3" json:"tag_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaggingReply) Reset() { + *x = TaggingReply{} + mi := &file_tag_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaggingReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaggingReply) ProtoMessage() {} + +func (x *TaggingReply) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaggingReply.ProtoReflect.Descriptor instead. +func (*TaggingReply) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{12} +} + +func (x *TaggingReply) GetTaggedRows() int64 { + if x != nil { + return x.TaggedRows + } + return 0 +} + +func (x *TaggingReply) GetTagId() int64 { + if x != nil { + return x.TagId + } + return 0 +} + +type ObjectTagsReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjectPk string `protobuf:"bytes,1,opt,name=object_pk,json=objectPk,proto3" json:"object_pk,omitempty"` //对象主键值 + ObjectId int64 `protobuf:"varint,2,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"` + Beid int64 `protobuf:"varint,3,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObjectTagsReq) Reset() { + *x = ObjectTagsReq{} + mi := &file_tag_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObjectTagsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObjectTagsReq) ProtoMessage() {} + +func (x *ObjectTagsReq) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ObjectTagsReq.ProtoReflect.Descriptor instead. +func (*ObjectTagsReq) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{13} +} + +func (x *ObjectTagsReq) GetObjectPk() string { + if x != nil { + return x.ObjectPk + } + return "" +} + +func (x *ObjectTagsReq) GetObjectId() int64 { + if x != nil { + return x.ObjectId + } + return 0 +} + +func (x *ObjectTagsReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type TagValueItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + TagKey string `protobuf:"bytes,1,opt,name=tag_key,json=tagKey,proto3" json:"tag_key,omitempty"` + TagName string `protobuf:"bytes,2,opt,name=tag_name,json=tagName,proto3" json:"tag_name,omitempty"` + TagValue string `protobuf:"bytes,3,opt,name=tag_value,json=tagValue,proto3" json:"tag_value,omitempty"` + TaggedAt int64 `protobuf:"varint,4,opt,name=tagged_at,json=taggedAt,proto3" json:"tagged_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagValueItem) Reset() { + *x = TagValueItem{} + mi := &file_tag_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagValueItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagValueItem) ProtoMessage() {} + +func (x *TagValueItem) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagValueItem.ProtoReflect.Descriptor instead. +func (*TagValueItem) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{14} +} + +func (x *TagValueItem) GetTagKey() string { + if x != nil { + return x.TagKey + } + return "" +} + +func (x *TagValueItem) GetTagName() string { + if x != nil { + return x.TagName + } + return "" +} + +func (x *TagValueItem) GetTagValue() string { + if x != nil { + return x.TagValue + } + return "" +} + +func (x *TagValueItem) GetTaggedAt() int64 { + if x != nil { + return x.TaggedAt + } + return 0 +} + +type ObjectTagsReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tags []*TagValueItem `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObjectTagsReply) Reset() { + *x = ObjectTagsReply{} + mi := &file_tag_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObjectTagsReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObjectTagsReply) ProtoMessage() {} + +func (x *ObjectTagsReply) ProtoReflect() protoreflect.Message { + mi := &file_tag_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ObjectTagsReply.ProtoReflect.Descriptor instead. +func (*ObjectTagsReply) Descriptor() ([]byte, []int) { + return file_tag_proto_rawDescGZIP(), []int{15} +} + +func (x *ObjectTagsReply) GetTags() []*TagValueItem { + if x != nil { + return x.Tags + } + return nil +} + +var File_tag_proto protoreflect.FileDescriptor + +const file_tag_proto_rawDesc = "" + + "\n" + + "\ttag.proto\x12\x03tag\"\xce\x01\n" + + "\tTagObject\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1f\n" + + "\vobject_type\x18\x02 \x01(\tR\n" + + "objectType\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x13\n" + + "\x05ds_id\x18\x04 \x01(\x03R\x04dsId\x12\x17\n" + + "\adb_name\x18\x05 \x01(\tR\x06dbName\x12\x1d\n" + + "\n" + + "table_name\x18\x06 \x01(\tR\ttableName\x12\x1b\n" + + "\tpk_column\x18\a \x01(\tR\bpkColumn\x12\x12\n" + + "\x04beid\x18\b \x01(\x03R\x04beid\"5\n" + + "\vObjectReply\x12&\n" + + "\x06object\x18\x01 \x01(\v2\x0e.tag.TagObjectR\x06object\"K\n" + + "\x0fObjectListReply\x12\"\n" + + "\x04list\x18\x01 \x03(\v2\x0e.tag.TagObjectR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\xc4\x01\n" + + "\x0fCreateObjectReq\x12\x1f\n" + + "\vobject_type\x18\x01 \x01(\tR\n" + + "objectType\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x13\n" + + "\x05ds_id\x18\x03 \x01(\x03R\x04dsId\x12\x17\n" + + "\adb_name\x18\x04 \x01(\tR\x06dbName\x12\x1d\n" + + "\n" + + "table_name\x18\x05 \x01(\tR\ttableName\x12\x1b\n" + + "\tpk_column\x18\x06 \x01(\tR\bpkColumn\x12\x12\n" + + "\x04beid\x18\a \x01(\x03R\x04beid\"l\n" + + "\rObjectListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x1f\n" + + "\vobject_type\x18\x02 \x01(\tR\n" + + "objectType\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"\xee\x01\n" + + "\x06TagDef\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1b\n" + + "\tobject_id\x18\x02 \x01(\x03R\bobjectId\x12\x17\n" + + "\atag_key\x18\x03 \x01(\tR\x06tagKey\x12\x19\n" + + "\btag_name\x18\x04 \x01(\tR\atagName\x12\x19\n" + + "\btag_type\x18\x05 \x01(\tR\atagType\x12\x19\n" + + "\brule_sql\x18\x06 \x01(\tR\aruleSql\x12!\n" + + "\frefresh_cron\x18\a \x01(\tR\vrefreshCron\x12\x16\n" + + "\x06status\x18\b \x01(\x05R\x06status\x12\x12\n" + + "\x04beid\x18\t \x01(\x03R\x04beid\",\n" + + "\vTagDefReply\x12\x1d\n" + + "\x03def\x18\x01 \x01(\v2\v.tag.TagDefR\x03def\"H\n" + + "\x0fTagDefListReply\x12\x1f\n" + + "\x04list\x18\x01 \x03(\v2\v.tag.TagDefR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\xcc\x01\n" + + "\fCreateTagReq\x12\x1b\n" + + "\tobject_id\x18\x01 \x01(\x03R\bobjectId\x12\x17\n" + + "\atag_key\x18\x02 \x01(\tR\x06tagKey\x12\x19\n" + + "\btag_name\x18\x03 \x01(\tR\atagName\x12\x19\n" + + "\btag_type\x18\x04 \x01(\tR\atagType\x12\x19\n" + + "\brule_sql\x18\x05 \x01(\tR\aruleSql\x12!\n" + + "\frefresh_cron\x18\x06 \x01(\tR\vrefreshCron\x12\x12\n" + + "\x04beid\x18\a \x01(\x03R\x04beid\"h\n" + + "\rTagDefListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x1b\n" + + "\tobject_id\x18\x02 \x01(\x03R\bobjectId\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"T\n" + + "\n" + + "TaggingReq\x12\x15\n" + + "\x06tag_id\x18\x01 \x01(\x03R\x05tagId\x12\x1b\n" + + "\tobject_id\x18\x02 \x01(\x03R\bobjectId\x12\x12\n" + + "\x04beid\x18\x03 \x01(\x03R\x04beid\"F\n" + + "\fTaggingReply\x12\x1f\n" + + "\vtagged_rows\x18\x01 \x01(\x03R\n" + + "taggedRows\x12\x15\n" + + "\x06tag_id\x18\x02 \x01(\x03R\x05tagId\"]\n" + + "\rObjectTagsReq\x12\x1b\n" + + "\tobject_pk\x18\x01 \x01(\tR\bobjectPk\x12\x1b\n" + + "\tobject_id\x18\x02 \x01(\x03R\bobjectId\x12\x12\n" + + "\x04beid\x18\x03 \x01(\x03R\x04beid\"|\n" + + "\fTagValueItem\x12\x17\n" + + "\atag_key\x18\x01 \x01(\tR\x06tagKey\x12\x19\n" + + "\btag_name\x18\x02 \x01(\tR\atagName\x12\x1b\n" + + "\ttag_value\x18\x03 \x01(\tR\btagValue\x12\x1b\n" + + "\ttagged_at\x18\x04 \x01(\x03R\btaggedAt\"8\n" + + "\x0fObjectTagsReply\x12%\n" + + "\x04tags\x18\x01 \x03(\v2\x11.tag.TagValueItemR\x04tags2\xed\x02\n" + + "\x03Tag\x126\n" + + "\fCreateObject\x12\x14.tag.CreateObjectReq\x1a\x10.tag.ObjectReply\x127\n" + + "\vListObjects\x12\x12.tag.ObjectListReq\x1a\x14.tag.ObjectListReply\x120\n" + + "\tCreateTag\x12\x11.tag.CreateTagReq\x1a\x10.tag.TagDefReply\x124\n" + + "\bListTags\x12\x12.tag.TagDefListReq\x1a\x14.tag.TagDefListReply\x12#\n" + + "\tDeleteTag\x12\n" + + ".tag.IdReq\x1a\n" + + ".tag.IdReq\x12-\n" + + "\aTagging\x12\x0f.tag.TaggingReq\x1a\x11.tag.TaggingReply\x129\n" + + "\rGetObjectTags\x12\x12.tag.ObjectTagsReq\x1a\x14.tag.ObjectTagsReplyB\aZ\x05./tagb\x06proto3" + +var ( + file_tag_proto_rawDescOnce sync.Once + file_tag_proto_rawDescData []byte +) + +func file_tag_proto_rawDescGZIP() []byte { + file_tag_proto_rawDescOnce.Do(func() { + file_tag_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tag_proto_rawDesc), len(file_tag_proto_rawDesc))) + }) + return file_tag_proto_rawDescData +} + +var file_tag_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_tag_proto_goTypes = []any{ + (*TagObject)(nil), // 0: tag.TagObject + (*ObjectReply)(nil), // 1: tag.ObjectReply + (*ObjectListReply)(nil), // 2: tag.ObjectListReply + (*CreateObjectReq)(nil), // 3: tag.CreateObjectReq + (*ObjectListReq)(nil), // 4: tag.ObjectListReq + (*IdReq)(nil), // 5: tag.IdReq + (*TagDef)(nil), // 6: tag.TagDef + (*TagDefReply)(nil), // 7: tag.TagDefReply + (*TagDefListReply)(nil), // 8: tag.TagDefListReply + (*CreateTagReq)(nil), // 9: tag.CreateTagReq + (*TagDefListReq)(nil), // 10: tag.TagDefListReq + (*TaggingReq)(nil), // 11: tag.TaggingReq + (*TaggingReply)(nil), // 12: tag.TaggingReply + (*ObjectTagsReq)(nil), // 13: tag.ObjectTagsReq + (*TagValueItem)(nil), // 14: tag.TagValueItem + (*ObjectTagsReply)(nil), // 15: tag.ObjectTagsReply +} +var file_tag_proto_depIdxs = []int32{ + 0, // 0: tag.ObjectReply.object:type_name -> tag.TagObject + 0, // 1: tag.ObjectListReply.list:type_name -> tag.TagObject + 6, // 2: tag.TagDefReply.def:type_name -> tag.TagDef + 6, // 3: tag.TagDefListReply.list:type_name -> tag.TagDef + 14, // 4: tag.ObjectTagsReply.tags:type_name -> tag.TagValueItem + 3, // 5: tag.Tag.CreateObject:input_type -> tag.CreateObjectReq + 4, // 6: tag.Tag.ListObjects:input_type -> tag.ObjectListReq + 9, // 7: tag.Tag.CreateTag:input_type -> tag.CreateTagReq + 10, // 8: tag.Tag.ListTags:input_type -> tag.TagDefListReq + 5, // 9: tag.Tag.DeleteTag:input_type -> tag.IdReq + 11, // 10: tag.Tag.Tagging:input_type -> tag.TaggingReq + 13, // 11: tag.Tag.GetObjectTags:input_type -> tag.ObjectTagsReq + 1, // 12: tag.Tag.CreateObject:output_type -> tag.ObjectReply + 2, // 13: tag.Tag.ListObjects:output_type -> tag.ObjectListReply + 7, // 14: tag.Tag.CreateTag:output_type -> tag.TagDefReply + 8, // 15: tag.Tag.ListTags:output_type -> tag.TagDefListReply + 5, // 16: tag.Tag.DeleteTag:output_type -> tag.IdReq + 12, // 17: tag.Tag.Tagging:output_type -> tag.TaggingReply + 15, // 18: tag.Tag.GetObjectTags:output_type -> tag.ObjectTagsReply + 12, // [12:19] is the sub-list for method output_type + 5, // [5:12] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_tag_proto_init() } +func file_tag_proto_init() { + if File_tag_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_tag_proto_rawDesc), len(file_tag_proto_rawDesc)), + NumEnums: 0, + NumMessages: 16, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_tag_proto_goTypes, + DependencyIndexes: file_tag_proto_depIdxs, + MessageInfos: file_tag_proto_msgTypes, + }.Build() + File_tag_proto = out.File + file_tag_proto_goTypes = nil + file_tag_proto_depIdxs = nil +} diff --git a/tag/rpc/tag/tag_grpc.pb.go b/tag/rpc/tag/tag_grpc.pb.go new file mode 100644 index 0000000..386d632 --- /dev/null +++ b/tag/rpc/tag/tag_grpc.pb.go @@ -0,0 +1,363 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: tag.proto + +package tag + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Tag_CreateObject_FullMethodName = "/tag.Tag/CreateObject" + Tag_ListObjects_FullMethodName = "/tag.Tag/ListObjects" + Tag_CreateTag_FullMethodName = "/tag.Tag/CreateTag" + Tag_ListTags_FullMethodName = "/tag.Tag/ListTags" + Tag_DeleteTag_FullMethodName = "/tag.Tag/DeleteTag" + Tag_Tagging_FullMethodName = "/tag.Tag/Tagging" + Tag_GetObjectTags_FullMethodName = "/tag.Tag/GetObjectTags" +) + +// TagClient is the client API for Tag service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type TagClient interface { + //创建标签对象 + CreateObject(ctx context.Context, in *CreateObjectReq, opts ...grpc.CallOption) (*ObjectReply, error) + //对象列表 + ListObjects(ctx context.Context, in *ObjectListReq, opts ...grpc.CallOption) (*ObjectListReply, error) + //创建标签定义 + CreateTag(ctx context.Context, in *CreateTagReq, opts ...grpc.CallOption) (*TagDefReply, error) + //标签定义列表 + ListTags(ctx context.Context, in *TagDefListReq, opts ...grpc.CallOption) (*TagDefListReply, error) + //删除标签定义 + DeleteTag(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + //执行打标(规则 SQL 跑目标库,结果写 dc_tag_value) + Tagging(ctx context.Context, in *TaggingReq, opts ...grpc.CallOption) (*TaggingReply, error) + //查对象标签画像 + GetObjectTags(ctx context.Context, in *ObjectTagsReq, opts ...grpc.CallOption) (*ObjectTagsReply, error) +} + +type tagClient struct { + cc grpc.ClientConnInterface +} + +func NewTagClient(cc grpc.ClientConnInterface) TagClient { + return &tagClient{cc} +} + +func (c *tagClient) CreateObject(ctx context.Context, in *CreateObjectReq, opts ...grpc.CallOption) (*ObjectReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ObjectReply) + err := c.cc.Invoke(ctx, Tag_CreateObject_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *tagClient) ListObjects(ctx context.Context, in *ObjectListReq, opts ...grpc.CallOption) (*ObjectListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ObjectListReply) + err := c.cc.Invoke(ctx, Tag_ListObjects_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *tagClient) CreateTag(ctx context.Context, in *CreateTagReq, opts ...grpc.CallOption) (*TagDefReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TagDefReply) + err := c.cc.Invoke(ctx, Tag_CreateTag_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *tagClient) ListTags(ctx context.Context, in *TagDefListReq, opts ...grpc.CallOption) (*TagDefListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TagDefListReply) + err := c.cc.Invoke(ctx, Tag_ListTags_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *tagClient) DeleteTag(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdReq) + err := c.cc.Invoke(ctx, Tag_DeleteTag_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *tagClient) Tagging(ctx context.Context, in *TaggingReq, opts ...grpc.CallOption) (*TaggingReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TaggingReply) + err := c.cc.Invoke(ctx, Tag_Tagging_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *tagClient) GetObjectTags(ctx context.Context, in *ObjectTagsReq, opts ...grpc.CallOption) (*ObjectTagsReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ObjectTagsReply) + err := c.cc.Invoke(ctx, Tag_GetObjectTags_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TagServer is the server API for Tag service. +// All implementations must embed UnimplementedTagServer +// for forward compatibility. +type TagServer interface { + //创建标签对象 + CreateObject(context.Context, *CreateObjectReq) (*ObjectReply, error) + //对象列表 + ListObjects(context.Context, *ObjectListReq) (*ObjectListReply, error) + //创建标签定义 + CreateTag(context.Context, *CreateTagReq) (*TagDefReply, error) + //标签定义列表 + ListTags(context.Context, *TagDefListReq) (*TagDefListReply, error) + //删除标签定义 + DeleteTag(context.Context, *IdReq) (*IdReq, error) + //执行打标(规则 SQL 跑目标库,结果写 dc_tag_value) + Tagging(context.Context, *TaggingReq) (*TaggingReply, error) + //查对象标签画像 + GetObjectTags(context.Context, *ObjectTagsReq) (*ObjectTagsReply, error) + mustEmbedUnimplementedTagServer() +} + +// UnimplementedTagServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTagServer struct{} + +func (UnimplementedTagServer) CreateObject(context.Context, *CreateObjectReq) (*ObjectReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateObject not implemented") +} +func (UnimplementedTagServer) ListObjects(context.Context, *ObjectListReq) (*ObjectListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListObjects not implemented") +} +func (UnimplementedTagServer) CreateTag(context.Context, *CreateTagReq) (*TagDefReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateTag not implemented") +} +func (UnimplementedTagServer) ListTags(context.Context, *TagDefListReq) (*TagDefListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListTags not implemented") +} +func (UnimplementedTagServer) DeleteTag(context.Context, *IdReq) (*IdReq, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteTag not implemented") +} +func (UnimplementedTagServer) Tagging(context.Context, *TaggingReq) (*TaggingReply, error) { + return nil, status.Error(codes.Unimplemented, "method Tagging not implemented") +} +func (UnimplementedTagServer) GetObjectTags(context.Context, *ObjectTagsReq) (*ObjectTagsReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetObjectTags not implemented") +} +func (UnimplementedTagServer) mustEmbedUnimplementedTagServer() {} +func (UnimplementedTagServer) testEmbeddedByValue() {} + +// UnsafeTagServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TagServer will +// result in compilation errors. +type UnsafeTagServer interface { + mustEmbedUnimplementedTagServer() +} + +func RegisterTagServer(s grpc.ServiceRegistrar, srv TagServer) { + // If the following call panics, it indicates UnimplementedTagServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Tag_ServiceDesc, srv) +} + +func _Tag_CreateObject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateObjectReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TagServer).CreateObject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Tag_CreateObject_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TagServer).CreateObject(ctx, req.(*CreateObjectReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Tag_ListObjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ObjectListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TagServer).ListObjects(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Tag_ListObjects_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TagServer).ListObjects(ctx, req.(*ObjectListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Tag_CreateTag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateTagReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TagServer).CreateTag(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Tag_CreateTag_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TagServer).CreateTag(ctx, req.(*CreateTagReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Tag_ListTags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TagDefListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TagServer).ListTags(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Tag_ListTags_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TagServer).ListTags(ctx, req.(*TagDefListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Tag_DeleteTag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TagServer).DeleteTag(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Tag_DeleteTag_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TagServer).DeleteTag(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Tag_Tagging_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TaggingReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TagServer).Tagging(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Tag_Tagging_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TagServer).Tagging(ctx, req.(*TaggingReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Tag_GetObjectTags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ObjectTagsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TagServer).GetObjectTags(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Tag_GetObjectTags_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TagServer).GetObjectTags(ctx, req.(*ObjectTagsReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Tag_ServiceDesc is the grpc.ServiceDesc for Tag service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Tag_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "tag.Tag", + HandlerType: (*TagServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateObject", + Handler: _Tag_CreateObject_Handler, + }, + { + MethodName: "ListObjects", + Handler: _Tag_ListObjects_Handler, + }, + { + MethodName: "CreateTag", + Handler: _Tag_CreateTag_Handler, + }, + { + MethodName: "ListTags", + Handler: _Tag_ListTags_Handler, + }, + { + MethodName: "DeleteTag", + Handler: _Tag_DeleteTag_Handler, + }, + { + MethodName: "Tagging", + Handler: _Tag_Tagging_Handler, + }, + { + MethodName: "GetObjectTags", + Handler: _Tag_GetObjectTags_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "tag.proto", +} diff --git a/tag/rpc/tagClient/tag.go b/tag/rpc/tagClient/tag.go new file mode 100644 index 0000000..cfafc60 --- /dev/null +++ b/tag/rpc/tagClient/tag.go @@ -0,0 +1,102 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: tag.proto + +package tagClient + +import ( + "context" + + "datacenter/tag/rpc/tag" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + CreateObjectReq = tag.CreateObjectReq + CreateTagReq = tag.CreateTagReq + IdReq = tag.IdReq + ObjectListReply = tag.ObjectListReply + ObjectListReq = tag.ObjectListReq + ObjectReply = tag.ObjectReply + ObjectTagsReply = tag.ObjectTagsReply + ObjectTagsReq = tag.ObjectTagsReq + TagDef = tag.TagDef + TagDefListReply = tag.TagDefListReply + TagDefListReq = tag.TagDefListReq + TagDefReply = tag.TagDefReply + TagObject = tag.TagObject + TagValueItem = tag.TagValueItem + TaggingReply = tag.TaggingReply + TaggingReq = tag.TaggingReq + + Tag interface { + // 创建标签对象 + CreateObject(ctx context.Context, in *CreateObjectReq, opts ...grpc.CallOption) (*ObjectReply, error) + // 对象列表 + ListObjects(ctx context.Context, in *ObjectListReq, opts ...grpc.CallOption) (*ObjectListReply, error) + // 创建标签定义 + CreateTag(ctx context.Context, in *CreateTagReq, opts ...grpc.CallOption) (*TagDefReply, error) + // 标签定义列表 + ListTags(ctx context.Context, in *TagDefListReq, opts ...grpc.CallOption) (*TagDefListReply, error) + // 删除标签定义 + DeleteTag(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + // 执行打标(规则 SQL 跑目标库,结果写 dc_tag_value) + Tagging(ctx context.Context, in *TaggingReq, opts ...grpc.CallOption) (*TaggingReply, error) + // 查对象标签画像 + GetObjectTags(ctx context.Context, in *ObjectTagsReq, opts ...grpc.CallOption) (*ObjectTagsReply, error) + } + + defaultTag struct { + cli zrpc.Client + } +) + +func NewTag(cli zrpc.Client) Tag { + return &defaultTag{ + cli: cli, + } +} + +// 创建标签对象 +func (m *defaultTag) CreateObject(ctx context.Context, in *CreateObjectReq, opts ...grpc.CallOption) (*ObjectReply, error) { + client := tag.NewTagClient(m.cli.Conn()) + return client.CreateObject(ctx, in, opts...) +} + +// 对象列表 +func (m *defaultTag) ListObjects(ctx context.Context, in *ObjectListReq, opts ...grpc.CallOption) (*ObjectListReply, error) { + client := tag.NewTagClient(m.cli.Conn()) + return client.ListObjects(ctx, in, opts...) +} + +// 创建标签定义 +func (m *defaultTag) CreateTag(ctx context.Context, in *CreateTagReq, opts ...grpc.CallOption) (*TagDefReply, error) { + client := tag.NewTagClient(m.cli.Conn()) + return client.CreateTag(ctx, in, opts...) +} + +// 标签定义列表 +func (m *defaultTag) ListTags(ctx context.Context, in *TagDefListReq, opts ...grpc.CallOption) (*TagDefListReply, error) { + client := tag.NewTagClient(m.cli.Conn()) + return client.ListTags(ctx, in, opts...) +} + +// 删除标签定义 +func (m *defaultTag) DeleteTag(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + client := tag.NewTagClient(m.cli.Conn()) + return client.DeleteTag(ctx, in, opts...) +} + +// 执行打标(规则 SQL 跑目标库,结果写 dc_tag_value) +func (m *defaultTag) Tagging(ctx context.Context, in *TaggingReq, opts ...grpc.CallOption) (*TaggingReply, error) { + client := tag.NewTagClient(m.cli.Conn()) + return client.Tagging(ctx, in, opts...) +} + +// 查对象标签画像 +func (m *defaultTag) GetObjectTags(ctx context.Context, in *ObjectTagsReq, opts ...grpc.CallOption) (*ObjectTagsReply, error) { + client := tag.NewTagClient(m.cli.Conn()) + return client.GetObjectTags(ctx, in, opts...) +} From 2a0597a53fa776d117729c5a6aeb82a9632a1374 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 13:44:23 +0800 Subject: [PATCH 05/12] =?UTF-8?q?Phase=202=EF=BC=9A=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=B4=A8=E9=87=8F/=E6=95=B0=E6=8D=AE=E6=8C=87=E6=A0=87/?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=A0=87=E7=AD=BE=E4=B8=89=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - quality-rpc(:8089):6 种质量规则(行数/非空/唯一/值域/一致性/波动),标识符正则白名单+数值占位符防注入,RunJob 真实跑库校验并落结果(实测 2 规则 2 通过) - metric-rpc(:8090):原子/派生指标体系,聚合白名单校验(非法 agg_type 实测拒绝),派生引用原子绑定与级联删除保护 - tag-rpc(:8091):标签对象/定义/打标/画像;规则 SQL 强校验(仅 SELECT 单语句+as object_id/as tag_value 别名,分号注入实测拒绝);打标实测 1001→yes/1002→no 画像正确 - 网关:3 个 .api 契约 + 21 路由 + 21 编排 logic + ServiceContext 装配 - docs/schema/phase2.sql:7 张 dc_ 表 --- datacenter.api | 3 + etc/datacenter-api.example.yaml | 15 + internal/config/config.go | 3 + internal/handler/metric/bindatomshandler.go | 32 ++ .../handler/metric/createmetrichandler.go | 32 ++ .../handler/metric/deletemetrichandler.go | 32 ++ internal/handler/metric/metricatomshandler.go | 32 ++ internal/handler/metric/metricinfohandler.go | 32 ++ internal/handler/metric/metriclisthandler.go | 32 ++ .../handler/metric/updatemetrichandler.go | 32 ++ .../quality/createqualityjobhandler.go | 32 ++ .../quality/createqualityrulehandler.go | 32 ++ .../quality/deletequalityrulehandler.go | 32 ++ .../handler/quality/qualityjoblisthandler.go | 32 ++ .../quality/qualityresultlisthandler.go | 32 ++ .../handler/quality/qualityrulelisthandler.go | 32 ++ .../handler/quality/runqualityjobhandler.go | 32 ++ internal/handler/routes.go | 144 ++++++ internal/handler/tag/createtagdefhandler.go | 32 ++ .../handler/tag/createtagobjecthandler.go | 32 ++ internal/handler/tag/deletetagdefhandler.go | 32 ++ internal/handler/tag/objecttagshandler.go | 32 ++ internal/handler/tag/tagdeflisthandler.go | 32 ++ internal/handler/tag/tagginghandler.go | 32 ++ internal/handler/tag/tagobjectlisthandler.go | 32 ++ internal/logic/metric/bindatomslogic.go | 42 ++ internal/logic/metric/convert.go | 39 ++ internal/logic/metric/createmetriclogic.go | 54 +++ internal/logic/metric/deletemetriclogic.go | 38 ++ internal/logic/metric/metricatomslogic.go | 42 ++ internal/logic/metric/metricinfologic.go | 42 ++ internal/logic/metric/metriclistlogic.go | 52 ++ internal/logic/metric/updatemetriclogic.go | 43 ++ internal/logic/quality/convert.go | 60 +++ .../logic/quality/createqualityjoblogic.go | 46 ++ .../logic/quality/createqualityrulelogic.go | 48 ++ .../logic/quality/deletequalityrulelogic.go | 38 ++ internal/logic/quality/qualityjoblistlogic.go | 57 +++ .../logic/quality/qualityresultlistlogic.go | 59 +++ .../logic/quality/qualityrulelistlogic.go | 58 +++ internal/logic/quality/runqualityjoblogic.go | 47 ++ internal/logic/tag/convert.go | 41 ++ internal/logic/tag/createtagdeflogic.go | 51 ++ internal/logic/tag/createtagobjectlogic.go | 51 ++ internal/logic/tag/deletetagdeflogic.go | 38 ++ internal/logic/tag/objecttagslogic.go | 54 +++ internal/logic/tag/tagdeflistlogic.go | 58 +++ internal/logic/tag/tagginglogic.go | 46 ++ internal/logic/tag/tagobjectlistlogic.go | 58 +++ internal/svc/servicecontext.go | 12 + internal/types/types.go | 263 ++++++++++ metric.api | 103 ++++ metric/model/metricmodel.go | 261 ++++++++++ metric/model/metricrelmodel.go | 111 +++++ metric/model/vars.go | 56 +++ metric/rpc/internal/logic/bindAtomsLogic.go | 54 ++- metric/rpc/internal/logic/convert.go | 33 ++ .../rpc/internal/logic/createMetricLogic.go | 57 ++- .../rpc/internal/logic/deleteMetricLogic.go | 46 +- metric/rpc/internal/logic/error.go | 39 ++ metric/rpc/internal/logic/getMetricLogic.go | 16 +- .../internal/logic/listAtomsOfDeriveLogic.go | 32 +- .../internal/logic/listMetricsByIdsLogic.go | 13 +- metric/rpc/internal/logic/listMetricsLogic.go | 13 +- .../rpc/internal/logic/updateMetricLogic.go | 41 +- metric/rpc/internal/logic/validate.go | 47 ++ metric/rpc/internal/logic/validate_test.go | 64 +++ metric/rpc/internal/svc/serviceContext.go | 18 +- quality.api | 139 ++++++ quality/model/jobmodel.go | 153 ++++++ quality/model/resultmodel.go | 116 +++++ quality/model/rulemodel.go | 186 +++++++ quality/model/vars.go | 71 +++ quality/rpc/internal/logic/checker.go | 454 ++++++++++++++++++ quality/rpc/internal/logic/checker_test.go | 340 +++++++++++++ quality/rpc/internal/logic/convert.go | 51 ++ quality/rpc/internal/logic/createJobLogic.go | 61 ++- quality/rpc/internal/logic/createRuleLogic.go | 34 +- quality/rpc/internal/logic/deleteRuleLogic.go | 16 +- quality/rpc/internal/logic/error.go | 29 ++ quality/rpc/internal/logic/listJobsLogic.go | 19 +- .../rpc/internal/logic/listResultsLogic.go | 18 +- quality/rpc/internal/logic/listRulesLogic.go | 19 +- quality/rpc/internal/logic/runJobLogic.go | 152 +++++- quality/rpc/internal/svc/serviceContext.go | 29 +- restart.sh | 6 + tag.api | 138 ++++++ tag/model/objectmodel.go | 160 ++++++ tag/model/tagdefmodel.go | 204 ++++++++ tag/model/tagvaluemodel.go | 112 +++++ tag/model/vars.go | 33 ++ tag/rpc/internal/logic/convert.go | 41 ++ tag/rpc/internal/logic/createObjectLogic.go | 40 +- tag/rpc/internal/logic/createTagLogic.go | 62 ++- tag/rpc/internal/logic/deleteTagLogic.go | 23 +- tag/rpc/internal/logic/error.go | 33 ++ tag/rpc/internal/logic/getObjectTagsLogic.go | 32 +- tag/rpc/internal/logic/listObjectsLogic.go | 18 +- tag/rpc/internal/logic/listTagsLogic.go | 18 +- tag/rpc/internal/logic/rule.go | 76 +++ tag/rpc/internal/logic/rule_test.go | 80 +++ tag/rpc/internal/logic/taggingLogic.go | 169 ++++++- tag/rpc/internal/svc/serviceContext.go | 55 ++- 103 files changed, 6379 insertions(+), 86 deletions(-) create mode 100644 internal/handler/metric/bindatomshandler.go create mode 100644 internal/handler/metric/createmetrichandler.go create mode 100644 internal/handler/metric/deletemetrichandler.go create mode 100644 internal/handler/metric/metricatomshandler.go create mode 100644 internal/handler/metric/metricinfohandler.go create mode 100644 internal/handler/metric/metriclisthandler.go create mode 100644 internal/handler/metric/updatemetrichandler.go create mode 100644 internal/handler/quality/createqualityjobhandler.go create mode 100644 internal/handler/quality/createqualityrulehandler.go create mode 100644 internal/handler/quality/deletequalityrulehandler.go create mode 100644 internal/handler/quality/qualityjoblisthandler.go create mode 100644 internal/handler/quality/qualityresultlisthandler.go create mode 100644 internal/handler/quality/qualityrulelisthandler.go create mode 100644 internal/handler/quality/runqualityjobhandler.go create mode 100644 internal/handler/tag/createtagdefhandler.go create mode 100644 internal/handler/tag/createtagobjecthandler.go create mode 100644 internal/handler/tag/deletetagdefhandler.go create mode 100644 internal/handler/tag/objecttagshandler.go create mode 100644 internal/handler/tag/tagdeflisthandler.go create mode 100644 internal/handler/tag/tagginghandler.go create mode 100644 internal/handler/tag/tagobjectlisthandler.go create mode 100644 internal/logic/metric/bindatomslogic.go create mode 100644 internal/logic/metric/convert.go create mode 100644 internal/logic/metric/createmetriclogic.go create mode 100644 internal/logic/metric/deletemetriclogic.go create mode 100644 internal/logic/metric/metricatomslogic.go create mode 100644 internal/logic/metric/metricinfologic.go create mode 100644 internal/logic/metric/metriclistlogic.go create mode 100644 internal/logic/metric/updatemetriclogic.go create mode 100644 internal/logic/quality/convert.go create mode 100644 internal/logic/quality/createqualityjoblogic.go create mode 100644 internal/logic/quality/createqualityrulelogic.go create mode 100644 internal/logic/quality/deletequalityrulelogic.go create mode 100644 internal/logic/quality/qualityjoblistlogic.go create mode 100644 internal/logic/quality/qualityresultlistlogic.go create mode 100644 internal/logic/quality/qualityrulelistlogic.go create mode 100644 internal/logic/quality/runqualityjoblogic.go create mode 100644 internal/logic/tag/convert.go create mode 100644 internal/logic/tag/createtagdeflogic.go create mode 100644 internal/logic/tag/createtagobjectlogic.go create mode 100644 internal/logic/tag/deletetagdeflogic.go create mode 100644 internal/logic/tag/objecttagslogic.go create mode 100644 internal/logic/tag/tagdeflistlogic.go create mode 100644 internal/logic/tag/tagginglogic.go create mode 100644 internal/logic/tag/tagobjectlistlogic.go create mode 100644 metric.api create mode 100644 metric/model/metricmodel.go create mode 100644 metric/model/metricrelmodel.go create mode 100644 metric/model/vars.go create mode 100644 metric/rpc/internal/logic/convert.go create mode 100644 metric/rpc/internal/logic/error.go create mode 100644 metric/rpc/internal/logic/validate.go create mode 100644 metric/rpc/internal/logic/validate_test.go create mode 100644 quality.api create mode 100644 quality/model/jobmodel.go create mode 100644 quality/model/resultmodel.go create mode 100644 quality/model/rulemodel.go create mode 100644 quality/model/vars.go create mode 100644 quality/rpc/internal/logic/checker.go create mode 100644 quality/rpc/internal/logic/checker_test.go create mode 100644 quality/rpc/internal/logic/convert.go create mode 100644 quality/rpc/internal/logic/error.go create mode 100644 tag.api create mode 100644 tag/model/objectmodel.go create mode 100644 tag/model/tagdefmodel.go create mode 100644 tag/model/tagvaluemodel.go create mode 100644 tag/model/vars.go create mode 100644 tag/rpc/internal/logic/convert.go create mode 100644 tag/rpc/internal/logic/error.go create mode 100644 tag/rpc/internal/logic/rule.go create mode 100644 tag/rpc/internal/logic/rule_test.go diff --git a/datacenter.api b/datacenter.api index f8ce5d3..f1b2477 100644 --- a/datacenter.api +++ b/datacenter.api @@ -12,6 +12,9 @@ import "datasource.api" import "metadata.api" import "datasvc.api" import "catalog.api" +import "quality.api" +import "metric.api" +import "tag.api" import "votes.api" import "search.api" import "questions.api" diff --git a/etc/datacenter-api.example.yaml b/etc/datacenter-api.example.yaml index d73f535..318c18c 100644 --- a/etc/datacenter-api.example.yaml +++ b/etc/datacenter-api.example.yaml @@ -54,3 +54,18 @@ CatalogRpc: Hosts: - 127.0.0.1:2379 Key: catalog.rpc +QualityRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: quality.rpc +MetricRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: metric.rpc +TagRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: tag.rpc diff --git a/internal/config/config.go b/internal/config/config.go index daf4980..0a62883 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,6 +24,9 @@ type Config struct { MetadataRpc zrpc.RpcClientConf DataSvcRpc zrpc.RpcClientConf CatalogRpc zrpc.RpcClientConf + QualityRpc zrpc.RpcClientConf + MetricRpc zrpc.RpcClientConf + TagRpc zrpc.RpcClientConf CacheRedis cache.ClusterConf } diff --git a/internal/handler/metric/bindatomshandler.go b/internal/handler/metric/bindatomshandler.go new file mode 100644 index 0000000..efd3200 --- /dev/null +++ b/internal/handler/metric/bindatomshandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "net/http" + + "datacenter/internal/logic/metric" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 派生指标绑定原子指标 +func BindAtomsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.BindAtomsReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metric.NewBindAtomsLogic(r.Context(), svcCtx) + err := l.BindAtoms(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/metric/createmetrichandler.go b/internal/handler/metric/createmetrichandler.go new file mode 100644 index 0000000..d628dee --- /dev/null +++ b/internal/handler/metric/createmetrichandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "net/http" + + "datacenter/internal/logic/metric" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建指标 +func CreateMetricHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateMetricReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metric.NewCreateMetricLogic(r.Context(), svcCtx) + resp, err := l.CreateMetric(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metric/deletemetrichandler.go b/internal/handler/metric/deletemetrichandler.go new file mode 100644 index 0000000..2b2d673 --- /dev/null +++ b/internal/handler/metric/deletemetrichandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "net/http" + + "datacenter/internal/logic/metric" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除指标 +func DeleteMetricHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MetricIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metric.NewDeleteMetricLogic(r.Context(), svcCtx) + err := l.DeleteMetric(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/metric/metricatomshandler.go b/internal/handler/metric/metricatomshandler.go new file mode 100644 index 0000000..d755787 --- /dev/null +++ b/internal/handler/metric/metricatomshandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "net/http" + + "datacenter/internal/logic/metric" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 派生指标引用的原子指标 +func MetricAtomsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MetricIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metric.NewMetricAtomsLogic(r.Context(), svcCtx) + resp, err := l.MetricAtoms(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metric/metricinfohandler.go b/internal/handler/metric/metricinfohandler.go new file mode 100644 index 0000000..cebd3a0 --- /dev/null +++ b/internal/handler/metric/metricinfohandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "net/http" + + "datacenter/internal/logic/metric" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 指标详情 +func MetricInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MetricIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metric.NewMetricInfoLogic(r.Context(), svcCtx) + resp, err := l.MetricInfo(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metric/metriclisthandler.go b/internal/handler/metric/metriclisthandler.go new file mode 100644 index 0000000..e205ef1 --- /dev/null +++ b/internal/handler/metric/metriclisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "net/http" + + "datacenter/internal/logic/metric" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 指标列表 +func MetricListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MetricListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metric.NewMetricListLogic(r.Context(), svcCtx) + resp, err := l.MetricList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/metric/updatemetrichandler.go b/internal/handler/metric/updatemetrichandler.go new file mode 100644 index 0000000..cc9eb71 --- /dev/null +++ b/internal/handler/metric/updatemetrichandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "net/http" + + "datacenter/internal/logic/metric" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新指标 +func UpdateMetricHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateMetricReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := metric.NewUpdateMetricLogic(r.Context(), svcCtx) + err := l.UpdateMetric(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/quality/createqualityjobhandler.go b/internal/handler/quality/createqualityjobhandler.go new file mode 100644 index 0000000..0c69e9e --- /dev/null +++ b/internal/handler/quality/createqualityjobhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "net/http" + + "datacenter/internal/logic/quality" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建质量任务 +func CreateQualityJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateQualityJobReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := quality.NewCreateQualityJobLogic(r.Context(), svcCtx) + resp, err := l.CreateQualityJob(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/quality/createqualityrulehandler.go b/internal/handler/quality/createqualityrulehandler.go new file mode 100644 index 0000000..7c85b42 --- /dev/null +++ b/internal/handler/quality/createqualityrulehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "net/http" + + "datacenter/internal/logic/quality" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建质量规则 +func CreateQualityRuleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateQualityRuleReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := quality.NewCreateQualityRuleLogic(r.Context(), svcCtx) + resp, err := l.CreateQualityRule(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/quality/deletequalityrulehandler.go b/internal/handler/quality/deletequalityrulehandler.go new file mode 100644 index 0000000..98931ce --- /dev/null +++ b/internal/handler/quality/deletequalityrulehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "net/http" + + "datacenter/internal/logic/quality" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除质量规则 +func DeleteQualityRuleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QualityIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := quality.NewDeleteQualityRuleLogic(r.Context(), svcCtx) + err := l.DeleteQualityRule(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/quality/qualityjoblisthandler.go b/internal/handler/quality/qualityjoblisthandler.go new file mode 100644 index 0000000..0053494 --- /dev/null +++ b/internal/handler/quality/qualityjoblisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "net/http" + + "datacenter/internal/logic/quality" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 质量任务列表 +func QualityJobListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.JobListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := quality.NewQualityJobListLogic(r.Context(), svcCtx) + resp, err := l.QualityJobList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/quality/qualityresultlisthandler.go b/internal/handler/quality/qualityresultlisthandler.go new file mode 100644 index 0000000..0b00c53 --- /dev/null +++ b/internal/handler/quality/qualityresultlisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "net/http" + + "datacenter/internal/logic/quality" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 质量结果列表 +func QualityResultListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ResultListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := quality.NewQualityResultListLogic(r.Context(), svcCtx) + resp, err := l.QualityResultList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/quality/qualityrulelisthandler.go b/internal/handler/quality/qualityrulelisthandler.go new file mode 100644 index 0000000..c7e8fc9 --- /dev/null +++ b/internal/handler/quality/qualityrulelisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "net/http" + + "datacenter/internal/logic/quality" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 质量规则列表 +func QualityRuleListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QualityRuleListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := quality.NewQualityRuleListLogic(r.Context(), svcCtx) + resp, err := l.QualityRuleList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/quality/runqualityjobhandler.go b/internal/handler/quality/runqualityjobhandler.go new file mode 100644 index 0000000..0f7fa44 --- /dev/null +++ b/internal/handler/quality/runqualityjobhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "net/http" + + "datacenter/internal/logic/quality" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 执行质量任务 +func RunQualityJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QualityIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := quality.NewRunQualityJobLogic(r.Context(), svcCtx) + resp, err := l.RunQualityJob(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 9305347..2068b32 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -11,8 +11,11 @@ import ( datasource "datacenter/internal/handler/datasource" datasvc "datacenter/internal/handler/datasvc" metadata "datacenter/internal/handler/metadata" + metric "datacenter/internal/handler/metric" + quality "datacenter/internal/handler/quality" questions "datacenter/internal/handler/questions" search "datacenter/internal/handler/search" + tag "datacenter/internal/handler/tag" user "datacenter/internal/handler/user" votes "datacenter/internal/handler/votes" "datacenter/internal/svc" @@ -243,6 +246,100 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { }, ) + server.AddRoutes( + []rest.Route{ + { + // 派生指标引用的原子指标 + Method: http.MethodGet, + Path: "/metric/atoms", + Handler: metric.MetricAtomsHandler(serverCtx), + }, + { + // 派生指标绑定原子指标 + Method: http.MethodPost, + Path: "/metric/bind/atoms", + Handler: metric.BindAtomsHandler(serverCtx), + }, + { + // 创建指标 + Method: http.MethodPost, + Path: "/metric/create", + Handler: metric.CreateMetricHandler(serverCtx), + }, + { + // 删除指标 + Method: http.MethodPost, + Path: "/metric/delete", + Handler: metric.DeleteMetricHandler(serverCtx), + }, + { + // 指标详情 + Method: http.MethodGet, + Path: "/metric/info", + Handler: metric.MetricInfoHandler(serverCtx), + }, + { + // 指标列表 + Method: http.MethodGet, + Path: "/metric/list", + Handler: metric.MetricListHandler(serverCtx), + }, + { + // 更新指标 + Method: http.MethodPost, + Path: "/metric/update", + Handler: metric.UpdateMetricHandler(serverCtx), + }, + }, + ) + + server.AddRoutes( + []rest.Route{ + { + // 创建质量任务 + Method: http.MethodPost, + Path: "/quality/job/create", + Handler: quality.CreateQualityJobHandler(serverCtx), + }, + { + // 质量任务列表 + Method: http.MethodGet, + Path: "/quality/job/list", + Handler: quality.QualityJobListHandler(serverCtx), + }, + { + // 执行质量任务 + Method: http.MethodPost, + Path: "/quality/job/run", + Handler: quality.RunQualityJobHandler(serverCtx), + }, + { + // 质量结果列表 + Method: http.MethodGet, + Path: "/quality/result/list", + Handler: quality.QualityResultListHandler(serverCtx), + }, + { + // 创建质量规则 + Method: http.MethodPost, + Path: "/quality/rule/create", + Handler: quality.CreateQualityRuleHandler(serverCtx), + }, + { + // 删除质量规则 + Method: http.MethodPost, + Path: "/quality/rule/delete", + Handler: quality.DeleteQualityRuleHandler(serverCtx), + }, + { + // 质量规则列表 + Method: http.MethodGet, + Path: "/quality/rule/list", + Handler: quality.QualityRuleListHandler(serverCtx), + }, + }, + ) + server.AddRoutes( []rest.Route{ { @@ -328,6 +425,53 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { ), ) + server.AddRoutes( + []rest.Route{ + { + // 创建标签定义 + Method: http.MethodPost, + Path: "/tag/def/create", + Handler: tag.CreateTagDefHandler(serverCtx), + }, + { + // 删除标签定义 + Method: http.MethodPost, + Path: "/tag/def/delete", + Handler: tag.DeleteTagDefHandler(serverCtx), + }, + { + // 标签定义列表 + Method: http.MethodGet, + Path: "/tag/def/list", + Handler: tag.TagDefListHandler(serverCtx), + }, + { + // 创建标签对象 + Method: http.MethodPost, + Path: "/tag/object/create", + Handler: tag.CreateTagObjectHandler(serverCtx), + }, + { + // 标签对象列表 + Method: http.MethodGet, + Path: "/tag/object/list", + Handler: tag.TagObjectListHandler(serverCtx), + }, + { + // 对象标签画像 + Method: http.MethodGet, + Path: "/tag/object/tags", + Handler: tag.ObjectTagsHandler(serverCtx), + }, + { + // 执行打标 + Method: http.MethodPost, + Path: "/tag/tagging", + Handler: tag.TaggingHandler(serverCtx), + }, + }, + ) + server.AddRoutes( []rest.Route{ { diff --git a/internal/handler/tag/createtagdefhandler.go b/internal/handler/tag/createtagdefhandler.go new file mode 100644 index 0000000..1a3439a --- /dev/null +++ b/internal/handler/tag/createtagdefhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "net/http" + + "datacenter/internal/logic/tag" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建标签定义 +func CreateTagDefHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateTagDefReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := tag.NewCreateTagDefLogic(r.Context(), svcCtx) + resp, err := l.CreateTagDef(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/tag/createtagobjecthandler.go b/internal/handler/tag/createtagobjecthandler.go new file mode 100644 index 0000000..aa9cda4 --- /dev/null +++ b/internal/handler/tag/createtagobjecthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "net/http" + + "datacenter/internal/logic/tag" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建标签对象 +func CreateTagObjectHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateTagObjectReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := tag.NewCreateTagObjectLogic(r.Context(), svcCtx) + resp, err := l.CreateTagObject(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/tag/deletetagdefhandler.go b/internal/handler/tag/deletetagdefhandler.go new file mode 100644 index 0000000..654f8e7 --- /dev/null +++ b/internal/handler/tag/deletetagdefhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "net/http" + + "datacenter/internal/logic/tag" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除标签定义 +func DeleteTagDefHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.TagDefIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := tag.NewDeleteTagDefLogic(r.Context(), svcCtx) + err := l.DeleteTagDef(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/tag/objecttagshandler.go b/internal/handler/tag/objecttagshandler.go new file mode 100644 index 0000000..484db53 --- /dev/null +++ b/internal/handler/tag/objecttagshandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "net/http" + + "datacenter/internal/logic/tag" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 对象标签画像 +func ObjectTagsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ObjectTagsReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := tag.NewObjectTagsLogic(r.Context(), svcCtx) + resp, err := l.ObjectTags(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/tag/tagdeflisthandler.go b/internal/handler/tag/tagdeflisthandler.go new file mode 100644 index 0000000..eaa1597 --- /dev/null +++ b/internal/handler/tag/tagdeflisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "net/http" + + "datacenter/internal/logic/tag" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 标签定义列表 +func TagDefListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.TagDefListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := tag.NewTagDefListLogic(r.Context(), svcCtx) + resp, err := l.TagDefList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/tag/tagginghandler.go b/internal/handler/tag/tagginghandler.go new file mode 100644 index 0000000..02eeafb --- /dev/null +++ b/internal/handler/tag/tagginghandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "net/http" + + "datacenter/internal/logic/tag" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 执行打标 +func TaggingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.TaggingReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := tag.NewTaggingLogic(r.Context(), svcCtx) + resp, err := l.Tagging(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/tag/tagobjectlisthandler.go b/internal/handler/tag/tagobjectlisthandler.go new file mode 100644 index 0000000..7d2c35d --- /dev/null +++ b/internal/handler/tag/tagobjectlisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "net/http" + + "datacenter/internal/logic/tag" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 标签对象列表 +func TagObjectListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.TagObjectListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := tag.NewTagObjectListLogic(r.Context(), svcCtx) + resp, err := l.TagObjectList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/logic/metric/bindatomslogic.go b/internal/logic/metric/bindatomslogic.go new file mode 100644 index 0000000..af4507d --- /dev/null +++ b/internal/logic/metric/bindatomslogic.go @@ -0,0 +1,42 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metric/rpc/metricsvc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type BindAtomsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 派生指标绑定原子指标 +func NewBindAtomsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindAtomsLogic { + return &BindAtomsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 派生指标绑定原子指标: 原子指标 id 切片拷贝一份再转发, 避免与网关请求共享底层数组; +// pb BindAtoms 虽返回 IdReq, 网关无返回体, 仅透传错误 +func (l *BindAtomsLogic) BindAtoms(req *types.BindAtomsReq) error { + atomMetricIds := make([]int64, 0, len(req.AtomMetricIds)) + atomMetricIds = append(atomMetricIds, req.AtomMetricIds...) + _, err := l.svcCtx.MetricRpc.BindAtoms(l.ctx, &metricsvc.BindAtomReq{ + DeriveMetricId: req.DeriveMetricId, + AtomMetricIds: atomMetricIds, + Beid: req.Beid, + }) + return err +} diff --git a/internal/logic/metric/convert.go b/internal/logic/metric/convert.go new file mode 100644 index 0000000..410cb8d --- /dev/null +++ b/internal/logic/metric/convert.go @@ -0,0 +1,39 @@ +package metric + +import ( + "datacenter/internal/types" + "datacenter/metric/rpc/metricsvc" +) + +// metricToTypes 将 rpc 的 Metric 逐字段转换为网关类型。 +// rpc 侧的 Beid 为租户内部字段, 网关类型未定义, 故不做映射。 +func metricToTypes(m *metricsvc.Metric) *types.MetricInfo { + if m == nil { + return &types.MetricInfo{} + } + return &types.MetricInfo{ + Id: m.GetId(), + NameEn: m.GetNameEn(), + NameCn: m.GetNameCn(), + MetricType: m.GetMetricType(), + BizField: m.GetBizField(), + AggType: m.GetAggType(), + Expr: m.GetExpr(), + DimensionJson: m.GetDimensionJson(), + Comment: m.GetComment(), + Owner: m.GetOwner(), + Status: m.GetStatus(), + } +} + +// metricListToTypes 将 rpc 的指标列表逐字段转换为网关类型, 供指标列表与原子指标查询复用。 +func metricListToTypes(reply *metricsvc.MetricListReply) *types.MetricListResp { + list := make([]types.MetricInfo, 0, len(reply.GetList())) + for _, item := range reply.GetList() { + list = append(list, *metricToTypes(item)) + } + return &types.MetricListResp{ + List: list, + Total: reply.GetTotal(), + } +} diff --git a/internal/logic/metric/createmetriclogic.go b/internal/logic/metric/createmetriclogic.go new file mode 100644 index 0000000..068ef5d --- /dev/null +++ b/internal/logic/metric/createmetriclogic.go @@ -0,0 +1,54 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metric/rpc/metricsvc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateMetricLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建指标 +func NewCreateMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateMetricLogic { + return &CreateMetricLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 创建指标: 网关请求逐字段映射为 rpc 请求, 指标类型缺省为 atom(原子指标), 转发后把指标转为网关类型返回 +func (l *CreateMetricLogic) CreateMetric(req *types.CreateMetricReq) (resp *types.MetricInfo, err error) { + metricType := req.MetricType + if metricType == "" { + metricType = "atom" + } + reply, err := l.svcCtx.MetricRpc.CreateMetric(l.ctx, &metricsvc.CreateMetricReq{ + NameEn: req.NameEn, + NameCn: req.NameCn, + MetricType: metricType, + BizField: req.BizField, + AggType: req.AggType, + Expr: req.Expr, + DimensionJson: req.DimensionJson, + Comment: req.Comment, + Owner: req.Owner, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return metricToTypes(reply.GetMetric()), nil +} diff --git a/internal/logic/metric/deletemetriclogic.go b/internal/logic/metric/deletemetriclogic.go new file mode 100644 index 0000000..32654ff --- /dev/null +++ b/internal/logic/metric/deletemetriclogic.go @@ -0,0 +1,38 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metric/rpc/metricsvc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteMetricLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除指标 +func NewDeleteMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteMetricLogic { + return &DeleteMetricLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 删除指标: 网关请求逐字段映射为 rpc 请求后转发; pb DeleteMetric 虽返回 IdReq, 网关无返回体, 仅透传错误 +func (l *DeleteMetricLogic) DeleteMetric(req *types.MetricIdReq) error { + _, err := l.svcCtx.MetricRpc.DeleteMetric(l.ctx, &metricsvc.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + return err +} diff --git a/internal/logic/metric/metricatomslogic.go b/internal/logic/metric/metricatomslogic.go new file mode 100644 index 0000000..665e3e5 --- /dev/null +++ b/internal/logic/metric/metricatomslogic.go @@ -0,0 +1,42 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metric/rpc/metricsvc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type MetricAtomsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 派生指标引用的原子指标 +func NewMetricAtomsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MetricAtomsLogic { + return &MetricAtomsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 派生指标引用的原子指标: 转发 ListAtomsOfDerive, 复用指标列表映射转为网关类型返回 +func (l *MetricAtomsLogic) MetricAtoms(req *types.MetricIdReq) (resp *types.MetricListResp, err error) { + reply, err := l.svcCtx.MetricRpc.ListAtomsOfDerive(l.ctx, &metricsvc.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return metricListToTypes(reply), nil +} diff --git a/internal/logic/metric/metricinfologic.go b/internal/logic/metric/metricinfologic.go new file mode 100644 index 0000000..9c076b3 --- /dev/null +++ b/internal/logic/metric/metricinfologic.go @@ -0,0 +1,42 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metric/rpc/metricsvc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type MetricInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 指标详情 +func NewMetricInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MetricInfoLogic { + return &MetricInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 指标详情: 网关请求逐字段映射为 rpc 请求, 转发后把指标转为网关类型返回 +func (l *MetricInfoLogic) MetricInfo(req *types.MetricIdReq) (resp *types.MetricInfo, err error) { + reply, err := l.svcCtx.MetricRpc.GetMetric(l.ctx, &metricsvc.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return metricToTypes(reply.GetMetric()), nil +} diff --git a/internal/logic/metric/metriclistlogic.go b/internal/logic/metric/metriclistlogic.go new file mode 100644 index 0000000..d97a2ff --- /dev/null +++ b/internal/logic/metric/metriclistlogic.go @@ -0,0 +1,52 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metric/rpc/metricsvc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type MetricListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 指标列表 +func NewMetricListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MetricListLogic { + return &MetricListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 指标列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *MetricListLogic) MetricList(req *types.MetricListReq) (resp *types.MetricListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.MetricRpc.ListMetrics(l.ctx, &metricsvc.MetricListReq{ + Beid: req.Beid, + MetricType: req.MetricType, + Keyword: req.Keyword, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return metricListToTypes(reply), nil +} diff --git a/internal/logic/metric/updatemetriclogic.go b/internal/logic/metric/updatemetriclogic.go new file mode 100644 index 0000000..6b259ad --- /dev/null +++ b/internal/logic/metric/updatemetriclogic.go @@ -0,0 +1,43 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package metric + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/metric/rpc/metricsvc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateMetricLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新指标 +func NewUpdateMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateMetricLogic { + return &UpdateMetricLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 更新指标: 网关请求逐字段映射为 rpc 请求后转发; pb UpdateMetric 虽返回 IdReq, 网关无返回体, 仅透传错误 +func (l *UpdateMetricLogic) UpdateMetric(req *types.UpdateMetricReq) error { + _, err := l.svcCtx.MetricRpc.UpdateMetric(l.ctx, &metricsvc.UpdateMetricReq{ + Id: req.Id, + Beid: req.Beid, + NameCn: req.NameCn, + Expr: req.Expr, + DimensionJson: req.DimensionJson, + Comment: req.Comment, + Status: req.Status, + }) + return err +} diff --git a/internal/logic/quality/convert.go b/internal/logic/quality/convert.go new file mode 100644 index 0000000..8c36d98 --- /dev/null +++ b/internal/logic/quality/convert.go @@ -0,0 +1,60 @@ +package quality + +import ( + "datacenter/internal/types" + "datacenter/quality/rpc/qualityClient" +) + +// qualityRuleToTypes 将 rpc 的 QualityRule 逐字段转换为网关类型。 +// rpc 侧的 Beid 为租户内部字段, 网关类型未定义, 故不做映射。 +func qualityRuleToTypes(rule *qualityClient.QualityRule) *types.QualityRule { + if rule == nil { + return &types.QualityRule{} + } + return &types.QualityRule{ + Id: rule.GetId(), + Name: rule.GetName(), + RuleType: rule.GetRuleType(), + TargetTable: rule.GetTargetTable(), + TargetColumn: rule.GetTargetColumn(), + ParamsJson: rule.GetParamsJson(), + ThresholdJson: rule.GetThresholdJson(), + Status: rule.GetStatus(), + Owner: rule.GetOwner(), + } +} + +// qualityJobToTypes 将 rpc 的 QualityJob 逐字段转换为网关类型。 +func qualityJobToTypes(job *qualityClient.QualityJob) *types.QualityJob { + if job == nil { + return &types.QualityJob{} + } + return &types.QualityJob{ + Id: job.GetId(), + Name: job.GetName(), + DatasourceId: job.GetDatasourceId(), + DbName: job.GetDbName(), + RuleIds: job.GetRuleIds(), + Cron: job.GetCron(), + LastStatus: job.GetLastStatus(), + LastRunAt: job.GetLastRunAt(), + Beid: job.GetBeid(), + } +} + +// qualityResultToTypes 将 rpc 的 QualityResult 逐字段转换为网关类型。 +func qualityResultToTypes(result *qualityClient.QualityResult) *types.QualityResult { + if result == nil { + return &types.QualityResult{} + } + return &types.QualityResult{ + Id: result.GetId(), + JobId: result.GetJobId(), + RuleId: result.GetRuleId(), + RuleType: result.GetRuleType(), + CheckValue: result.GetCheckValue(), + PassFlag: result.GetPassFlag(), + DetailJson: result.GetDetailJson(), + RunAt: result.GetRunAt(), + } +} diff --git a/internal/logic/quality/createqualityjoblogic.go b/internal/logic/quality/createqualityjoblogic.go new file mode 100644 index 0000000..e2b16eb --- /dev/null +++ b/internal/logic/quality/createqualityjoblogic.go @@ -0,0 +1,46 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/quality/rpc/qualityClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateQualityJobLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建质量任务 +func NewCreateQualityJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateQualityJobLogic { + return &CreateQualityJobLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 创建质量任务: 网关请求逐字段映射为 rpc 请求, 转发后把任务转为网关类型返回 +func (l *CreateQualityJobLogic) CreateQualityJob(req *types.CreateQualityJobReq) (resp *types.QualityJob, err error) { + reply, err := l.svcCtx.QualityRpc.CreateJob(l.ctx, &qualityClient.CreateJobReq{ + Name: req.Name, + DatasourceId: req.DatasourceId, + DbName: req.DbName, + RuleIds: req.RuleIds, + Cron: req.Cron, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return qualityJobToTypes(reply.GetJob()), nil +} diff --git a/internal/logic/quality/createqualityrulelogic.go b/internal/logic/quality/createqualityrulelogic.go new file mode 100644 index 0000000..dbeaf05 --- /dev/null +++ b/internal/logic/quality/createqualityrulelogic.go @@ -0,0 +1,48 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/quality/rpc/qualityClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateQualityRuleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建质量规则 +func NewCreateQualityRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateQualityRuleLogic { + return &CreateQualityRuleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 创建质量规则: 网关请求逐字段映射为 rpc 请求, 转发后把规则转为网关类型返回 +func (l *CreateQualityRuleLogic) CreateQualityRule(req *types.CreateQualityRuleReq) (resp *types.QualityRule, err error) { + reply, err := l.svcCtx.QualityRpc.CreateRule(l.ctx, &qualityClient.CreateRuleReq{ + Name: req.Name, + RuleType: req.RuleType, + TargetTable: req.TargetTable, + TargetColumn: req.TargetColumn, + ParamsJson: req.ParamsJson, + ThresholdJson: req.ThresholdJson, + Owner: req.Owner, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return qualityRuleToTypes(reply.GetRule()), nil +} diff --git a/internal/logic/quality/deletequalityrulelogic.go b/internal/logic/quality/deletequalityrulelogic.go new file mode 100644 index 0000000..66ec986 --- /dev/null +++ b/internal/logic/quality/deletequalityrulelogic.go @@ -0,0 +1,38 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/quality/rpc/qualityClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteQualityRuleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除质量规则 +func NewDeleteQualityRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteQualityRuleLogic { + return &DeleteQualityRuleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 删除质量规则: 网关请求逐字段映射为 rpc 请求后转发; pb DeleteRule 虽返回 IdReq, 网关无返回体, 仅透传错误 +func (l *DeleteQualityRuleLogic) DeleteQualityRule(req *types.QualityIdReq) error { + _, err := l.svcCtx.QualityRpc.DeleteRule(l.ctx, &qualityClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + return err +} diff --git a/internal/logic/quality/qualityjoblistlogic.go b/internal/logic/quality/qualityjoblistlogic.go new file mode 100644 index 0000000..a0f8a2a --- /dev/null +++ b/internal/logic/quality/qualityjoblistlogic.go @@ -0,0 +1,57 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/quality/rpc/qualityClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QualityJobListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 质量任务列表 +func NewQualityJobListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QualityJobListLogic { + return &QualityJobListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 质量任务列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *QualityJobListLogic) QualityJobList(req *types.JobListReq) (resp *types.JobListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.QualityRpc.ListJobs(l.ctx, &qualityClient.JobListReq{ + Beid: req.Beid, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.QualityJob, 0, len(reply.GetList())) + for _, item := range reply.GetList() { + list = append(list, *qualityJobToTypes(item)) + } + return &types.JobListResp{ + List: list, + Total: reply.GetTotal(), + }, nil +} diff --git a/internal/logic/quality/qualityresultlistlogic.go b/internal/logic/quality/qualityresultlistlogic.go new file mode 100644 index 0000000..16cb9c8 --- /dev/null +++ b/internal/logic/quality/qualityresultlistlogic.go @@ -0,0 +1,59 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/quality/rpc/qualityClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QualityResultListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 质量结果列表 +func NewQualityResultListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QualityResultListLogic { + return &QualityResultListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 质量结果列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发; +// pass_filter 取值 0 表示不过滤, 直接透传由 rpc 侧处理 +func (l *QualityResultListLogic) QualityResultList(req *types.ResultListReq) (resp *types.ResultListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.QualityRpc.ListResults(l.ctx, &qualityClient.ResultListReq{ + JobId: req.JobId, + PassFilter: req.PassFilter, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.QualityResult, 0, len(reply.GetList())) + for _, item := range reply.GetList() { + list = append(list, *qualityResultToTypes(item)) + } + return &types.ResultListResp{ + List: list, + Total: reply.GetTotal(), + }, nil +} diff --git a/internal/logic/quality/qualityrulelistlogic.go b/internal/logic/quality/qualityrulelistlogic.go new file mode 100644 index 0000000..f66ed3f --- /dev/null +++ b/internal/logic/quality/qualityrulelistlogic.go @@ -0,0 +1,58 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/quality/rpc/qualityClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QualityRuleListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 质量规则列表 +func NewQualityRuleListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QualityRuleListLogic { + return &QualityRuleListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 质量规则列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *QualityRuleListLogic) QualityRuleList(req *types.QualityRuleListReq) (resp *types.QualityRuleListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.QualityRpc.ListRules(l.ctx, &qualityClient.RuleListReq{ + Beid: req.Beid, + RuleType: req.RuleType, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.QualityRule, 0, len(reply.GetList())) + for _, item := range reply.GetList() { + list = append(list, *qualityRuleToTypes(item)) + } + return &types.QualityRuleListResp{ + List: list, + Total: reply.GetTotal(), + }, nil +} diff --git a/internal/logic/quality/runqualityjoblogic.go b/internal/logic/quality/runqualityjoblogic.go new file mode 100644 index 0000000..641719a --- /dev/null +++ b/internal/logic/quality/runqualityjoblogic.go @@ -0,0 +1,47 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package quality + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/quality/rpc/qualityClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RunQualityJobLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 执行质量任务 +func NewRunQualityJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RunQualityJobLogic { + return &RunQualityJobLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 执行质量任务: 网关请求逐字段映射为 rpc 请求, 转发后把执行结果转为网关类型返回 +func (l *RunQualityJobLogic) RunQualityJob(req *types.QualityIdReq) (resp *types.RunJobResp, err error) { + reply, err := l.svcCtx.QualityRpc.RunJob(l.ctx, &qualityClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.RunJobResp{ + JobId: reply.GetJobId(), + TotalRules: reply.GetTotalRules(), + Passed: reply.GetPassed(), + LastStatus: reply.GetLastStatus(), + }, nil +} diff --git a/internal/logic/tag/convert.go b/internal/logic/tag/convert.go new file mode 100644 index 0000000..bc6c3a4 --- /dev/null +++ b/internal/logic/tag/convert.go @@ -0,0 +1,41 @@ +package tag + +import ( + "datacenter/internal/types" + "datacenter/tag/rpc/tagClient" +) + +// tagObjectToTypes 将 rpc 的 TagObject 逐字段转换为网关类型。 +func tagObjectToTypes(object *tagClient.TagObject) *types.TagObject { + if object == nil { + return &types.TagObject{} + } + return &types.TagObject{ + Id: object.GetId(), + ObjectType: object.GetObjectType(), + Name: object.GetName(), + DsId: object.GetDsId(), + DbName: object.GetDbName(), + TableName: object.GetTableName(), + PkColumn: object.GetPkColumn(), + Beid: object.GetBeid(), + } +} + +// tagDefToTypes 将 rpc 的 TagDef 逐字段转换为网关类型。 +func tagDefToTypes(def *tagClient.TagDef) *types.TagDef { + if def == nil { + return &types.TagDef{} + } + return &types.TagDef{ + Id: def.GetId(), + ObjectId: def.GetObjectId(), + TagKey: def.GetTagKey(), + TagName: def.GetTagName(), + TagType: def.GetTagType(), + RuleSql: def.GetRuleSql(), + RefreshCron: def.GetRefreshCron(), + Status: def.GetStatus(), + Beid: def.GetBeid(), + } +} diff --git a/internal/logic/tag/createtagdeflogic.go b/internal/logic/tag/createtagdeflogic.go new file mode 100644 index 0000000..ed1c21a --- /dev/null +++ b/internal/logic/tag/createtagdeflogic.go @@ -0,0 +1,51 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/tag/rpc/tagClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateTagDefLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建标签定义 +func NewCreateTagDefLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateTagDefLogic { + return &CreateTagDefLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 创建标签定义: 网关请求逐字段映射为 rpc 请求, 标签类型缺省为 rule(规则标签), 转发后把定义转为网关类型返回 +func (l *CreateTagDefLogic) CreateTagDef(req *types.CreateTagDefReq) (resp *types.TagDef, err error) { + tagType := req.TagType + if tagType == "" { + tagType = "rule" + } + reply, err := l.svcCtx.TagRpc.CreateTag(l.ctx, &tagClient.CreateTagReq{ + ObjectId: req.ObjectId, + TagKey: req.TagKey, + TagName: req.TagName, + TagType: tagType, + RuleSql: req.RuleSql, + RefreshCron: req.RefreshCron, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return tagDefToTypes(reply.GetDef()), nil +} diff --git a/internal/logic/tag/createtagobjectlogic.go b/internal/logic/tag/createtagobjectlogic.go new file mode 100644 index 0000000..4e9cf93 --- /dev/null +++ b/internal/logic/tag/createtagobjectlogic.go @@ -0,0 +1,51 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/tag/rpc/tagClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateTagObjectLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建标签对象 +func NewCreateTagObjectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateTagObjectLogic { + return &CreateTagObjectLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 创建标签对象: 网关请求逐字段映射为 rpc 请求, 对象类型缺省为 user, 转发后把对象转为网关类型返回 +func (l *CreateTagObjectLogic) CreateTagObject(req *types.CreateTagObjectReq) (resp *types.TagObject, err error) { + objectType := req.ObjectType + if objectType == "" { + objectType = "user" + } + reply, err := l.svcCtx.TagRpc.CreateObject(l.ctx, &tagClient.CreateObjectReq{ + ObjectType: objectType, + Name: req.Name, + DsId: req.DsId, + DbName: req.DbName, + TableName: req.TableName, + PkColumn: req.PkColumn, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return tagObjectToTypes(reply.GetObject()), nil +} diff --git a/internal/logic/tag/deletetagdeflogic.go b/internal/logic/tag/deletetagdeflogic.go new file mode 100644 index 0000000..425fe0c --- /dev/null +++ b/internal/logic/tag/deletetagdeflogic.go @@ -0,0 +1,38 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/tag/rpc/tagClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteTagDefLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除标签定义 +func NewDeleteTagDefLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteTagDefLogic { + return &DeleteTagDefLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 删除标签定义: 网关请求逐字段映射为 rpc 请求后转发; pb DeleteTag 虽返回 IdReq, 网关无返回体, 仅透传错误 +func (l *DeleteTagDefLogic) DeleteTagDef(req *types.TagDefIdReq) error { + _, err := l.svcCtx.TagRpc.DeleteTag(l.ctx, &tagClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + return err +} diff --git a/internal/logic/tag/objecttagslogic.go b/internal/logic/tag/objecttagslogic.go new file mode 100644 index 0000000..f9f0fe0 --- /dev/null +++ b/internal/logic/tag/objecttagslogic.go @@ -0,0 +1,54 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/tag/rpc/tagClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ObjectTagsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 对象标签画像 +func NewObjectTagsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ObjectTagsLogic { + return &ObjectTagsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 对象标签画像: 网关请求逐字段映射为 rpc 请求, 转发后把标签值列表逐项转为网关类型返回 +func (l *ObjectTagsLogic) ObjectTags(req *types.ObjectTagsReq) (resp *types.ObjectTagsResp, err error) { + reply, err := l.svcCtx.TagRpc.GetObjectTags(l.ctx, &tagClient.ObjectTagsReq{ + ObjectPk: req.ObjectPk, + ObjectId: req.ObjectId, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + tags := make([]types.TagValueItem, 0, len(reply.GetTags())) + for _, item := range reply.GetTags() { + tags = append(tags, types.TagValueItem{ + TagKey: item.GetTagKey(), + TagName: item.GetTagName(), + TagValue: item.GetTagValue(), + TaggedAt: item.GetTaggedAt(), + }) + } + return &types.ObjectTagsResp{ + Tags: tags, + }, nil +} diff --git a/internal/logic/tag/tagdeflistlogic.go b/internal/logic/tag/tagdeflistlogic.go new file mode 100644 index 0000000..fd7c87a --- /dev/null +++ b/internal/logic/tag/tagdeflistlogic.go @@ -0,0 +1,58 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/tag/rpc/tagClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TagDefListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 标签定义列表 +func NewTagDefListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TagDefListLogic { + return &TagDefListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 标签定义列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *TagDefListLogic) TagDefList(req *types.TagDefListReq) (resp *types.TagDefListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.TagRpc.ListTags(l.ctx, &tagClient.TagDefListReq{ + Beid: req.Beid, + ObjectId: req.ObjectId, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.TagDef, 0, len(reply.GetList())) + for _, item := range reply.GetList() { + list = append(list, *tagDefToTypes(item)) + } + return &types.TagDefListResp{ + List: list, + Total: reply.GetTotal(), + }, nil +} diff --git a/internal/logic/tag/tagginglogic.go b/internal/logic/tag/tagginglogic.go new file mode 100644 index 0000000..4cc316d --- /dev/null +++ b/internal/logic/tag/tagginglogic.go @@ -0,0 +1,46 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/tag/rpc/tagClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TaggingLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 执行打标 +func NewTaggingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TaggingLogic { + return &TaggingLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 执行打标: 网关请求逐字段映射为 rpc 请求, 转发后把打标结果转为网关类型返回 +func (l *TaggingLogic) Tagging(req *types.TaggingReq) (resp *types.TaggingResp, err error) { + reply, err := l.svcCtx.TagRpc.Tagging(l.ctx, &tagClient.TaggingReq{ + TagId: req.TagId, + ObjectId: req.ObjectId, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.TaggingResp{ + TaggedRows: reply.GetTaggedRows(), + TagId: reply.GetTagId(), + }, nil +} diff --git a/internal/logic/tag/tagobjectlistlogic.go b/internal/logic/tag/tagobjectlistlogic.go new file mode 100644 index 0000000..54d85c4 --- /dev/null +++ b/internal/logic/tag/tagobjectlistlogic.go @@ -0,0 +1,58 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package tag + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/tag/rpc/tagClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TagObjectListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 标签对象列表 +func NewTagObjectListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TagObjectListLogic { + return &TagObjectListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 标签对象列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *TagObjectListLogic) TagObjectList(req *types.TagObjectListReq) (resp *types.TagObjectListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.TagRpc.ListObjects(l.ctx, &tagClient.ObjectListReq{ + Beid: req.Beid, + ObjectType: req.ObjectType, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.TagObject, 0, len(reply.GetList())) + for _, item := range reply.GetList() { + list = append(list, *tagObjectToTypes(item)) + } + return &types.TagObjectListResp{ + List: list, + Total: reply.GetTotal(), + }, nil +} diff --git a/internal/svc/servicecontext.go b/internal/svc/servicecontext.go index 0219cf7..a1f962e 100644 --- a/internal/svc/servicecontext.go +++ b/internal/svc/servicecontext.go @@ -9,9 +9,12 @@ import ( "datacenter/internal/config" "datacenter/internal/middleware" "datacenter/metadata/rpc/metadataClient" + "datacenter/metric/rpc/metricsvc" + "datacenter/quality/rpc/qualityClient" "datacenter/questions/rpc/questionsclient" "datacenter/search/rpc/searchclient" "datacenter/shared" + "datacenter/tag/rpc/tagClient" "datacenter/user/rpc/userclient" "datacenter/votes/rpc/votesclient" "fmt" @@ -42,6 +45,9 @@ type ServiceContext struct { MetadataRpc metadataClient.Metadata //元数据(Phase 1) DataSvcRpc dataSvcClient.DataSvc //数据服务(Phase 1) CatalogRpc catalogClient.Catalog //资产目录(Phase 1) + QualityRpc qualityClient.Quality //数据质量(Phase 2) + MetricRpc metricsvc.MetricSvc //数据指标(Phase 2) + TagRpc tagClient.Tag //数据标签(Phase 2) Cache cache.Cache RedisConn *redis.Redis } @@ -67,6 +73,9 @@ func NewServiceContext(c config.Config) *ServiceContext { mr := metadataClient.NewMetadata(zrpc.MustNewClient(c.MetadataRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) dvr := dataSvcClient.NewDataSvc(zrpc.MustNewClient(c.DataSvcRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) cr2 := catalogClient.NewCatalog(zrpc.MustNewClient(c.CatalogRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + qr2 := qualityClient.NewQuality(zrpc.MustNewClient(c.QualityRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + mr2 := metricsvc.NewMetricSvc(zrpc.MustNewClient(c.MetricRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + tr2 := tagClient.NewTag(zrpc.MustNewClient(c.TagRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) //缓存 ca := cache.New(c.CacheRedis, syncx.NewSingleFlight(), cache.NewStat("dc"), shared.ErrNotFound) rcon := redis.MustNewRedis(c.CacheRedis[0].RedisConf) @@ -85,6 +94,9 @@ func NewServiceContext(c config.Config) *ServiceContext { MetadataRpc: mr, DataSvcRpc: dvr, CatalogRpc: cr2, + QualityRpc: qr2, + MetricRpc: mr2, + TagRpc: tr2, Cache: ca, RedisConn: rcon, } diff --git a/internal/types/types.go b/internal/types/types.go index 725c5ba..da3be47 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -139,6 +139,12 @@ type Beid struct { Beid int64 `json:"beid,optional" form:"beid,optional"` } +type BindAtomsReq struct { + DeriveMetricId int64 `json:"derive_metric_id"` + AtomMetricIds []int64 `json:"atom_metric_ids"` + Beid int64 `json:"beid,optional"` +} + type CategoryListReq struct { Beid int64 `json:"beid,optional" form:"beid,optional"` } @@ -196,6 +202,59 @@ type CreateDataSourceReq struct { Beid int64 `json:"beid,optional"` } +type CreateMetricReq struct { + NameEn string `json:"name_en"` + NameCn string `json:"name_cn"` + MetricType string `json:"metric_type"` + BizField string `json:"biz_field,optional"` + AggType string `json:"agg_type,optional"` + Expr string `json:"expr,optional"` + DimensionJson string `json:"dimension_json,optional"` + Comment string `json:"comment,optional"` + Owner string `json:"owner,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateQualityJobReq struct { + Name string `json:"name"` + DatasourceId int64 `json:"datasource_id"` + DbName string `json:"db_name,optional"` + RuleIds string `json:"rule_ids"` + Cron string `json:"cron,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateQualityRuleReq struct { + Name string `json:"name"` + RuleType string `json:"rule_type"` + TargetTable string `json:"target_table"` + TargetColumn string `json:"target_column,optional"` + ParamsJson string `json:"params_json,optional"` + ThresholdJson string `json:"threshold_json,optional"` + Owner string `json:"owner,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateTagDefReq struct { + ObjectId int64 `json:"object_id"` + TagKey string `json:"tag_key"` + TagName string `json:"tag_name"` + TagType string `json:"tag_type,optional"` + RuleSql string `json:"rule_sql,optional"` + RefreshCron string `json:"refresh_cron,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateTagObjectReq struct { + ObjectType string `json:"object_type"` + Name string `json:"name"` + DsId int64 `json:"ds_id,optional"` + DbName string `json:"db_name,optional"` + TableName string `json:"table_name,optional"` + PkColumn string `json:"pk_column,optional"` + Beid int64 `json:"beid,optional"` +} + type DataSourceIdReq struct { Id int64 `json:"id,optional" form:"id,optional"` Beid int64 `json:"beid,optional" form:"beid,optional"` @@ -278,6 +337,17 @@ type GrantApiReq struct { Beid int64 `json:"beid,optional"` } +type JobListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type JobListResp struct { + List []QualityJob `json:"list"` + Total int64 `json:"total"` +} + type JwtToken struct { AccessToken string `json:"access_token,omitempty"` AccessExpire int64 `json:"access_expire,omitempty"` @@ -368,6 +438,100 @@ type MetaVersion struct { PublishedAt int64 `json:"published_at,optional"` } +type MetricIdReq struct { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type MetricInfo struct { + Id int64 `json:"id,optional"` + NameEn string `json:"name_en,optional"` + NameCn string `json:"name_cn,optional"` + MetricType string `json:"metric_type,optional"` + BizField string `json:"biz_field,optional"` + AggType string `json:"agg_type,optional"` + Expr string `json:"expr,optional"` + DimensionJson string `json:"dimension_json,optional"` + Comment string `json:"comment,optional"` + Owner string `json:"owner,optional"` + Status int32 `json:"status,optional"` +} + +type MetricListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + MetricType string `json:"metric_type,optional" form:"metric_type,optional"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type MetricListResp struct { + List []MetricInfo `json:"list"` + Total int64 `json:"total"` +} + +type ObjectTagsReq struct { + ObjectPk string `json:"object_pk,optional" form:"object_pk,optional"` + ObjectId int64 `json:"object_id,optional" form:"object_id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type ObjectTagsResp struct { + Tags []TagValueItem `json:"tags"` +} + +type QualityIdReq struct { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type QualityJob struct { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + DatasourceId int64 `json:"datasource_id,optional"` + DbName string `json:"db_name,optional"` + RuleIds string `json:"rule_ids,optional"` + Cron string `json:"cron,optional"` + LastStatus string `json:"last_status,optional"` + LastRunAt int64 `json:"last_run_at,optional"` + Beid int64 `json:"beid,optional"` +} + +type QualityResult struct { + Id int64 `json:"id,optional"` + JobId int64 `json:"job_id,optional"` + RuleId int64 `json:"rule_id,optional"` + RuleType string `json:"rule_type,optional"` + CheckValue string `json:"check_value,optional"` + PassFlag bool `json:"pass_flag,optional"` + DetailJson string `json:"detail_json,optional"` + RunAt int64 `json:"run_at,optional"` +} + +type QualityRule struct { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + RuleType string `json:"rule_type,optional"` + TargetTable string `json:"target_table,optional"` + TargetColumn string `json:"target_column,optional"` + ParamsJson string `json:"params_json,optional"` + ThresholdJson string `json:"threshold_json,optional"` + Status int32 `json:"status,optional"` + Owner string `json:"owner,optional"` +} + +type QualityRuleListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + RuleType string `json:"rule_type,optional" form:"rule_type,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type QualityRuleListResp struct { + List []QualityRule `json:"list"` + Total int64 `json:"total"` +} + type QuestionsAwardReq struct { ActivityId int64 `json:"actid"` AnswerId int64 `json:"answerid"` @@ -394,6 +558,25 @@ type Response struct { Message string `json:"message"` } +type ResultListReq struct { + JobId int64 `json:"job_id,optional" form:"job_id,optional"` + PassFilter int64 `json:"pass_filter,optional" form:"pass_filter,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type ResultListResp struct { + List []QualityResult `json:"list"` + Total int64 `json:"total"` +} + +type RunJobResp struct { + JobId int64 `json:"job_id"` + TotalRules int64 `json:"total_rules"` + Passed int64 `json:"passed"` + LastStatus string `json:"last_status"` +} + type SearchReq struct { Keyword string `json:"keyword,optional" form:"keyword,optional"` Page string `json:"page,optional" form:"page,optional"` @@ -430,6 +613,76 @@ type TagAssetReq struct { Beid int64 `json:"beid,optional"` } +type TagDef struct { + Id int64 `json:"id,optional"` + ObjectId int64 `json:"object_id,optional"` + TagKey string `json:"tag_key,optional"` + TagName string `json:"tag_name,optional"` + TagType string `json:"tag_type,optional"` + RuleSql string `json:"rule_sql,optional"` + RefreshCron string `json:"refresh_cron,optional"` + Status int32 `json:"status,optional"` + Beid int64 `json:"beid,optional"` +} + +type TagDefIdReq struct { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type TagDefListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + ObjectId int64 `json:"object_id,optional" form:"object_id,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type TagDefListResp struct { + List []TagDef `json:"list"` + Total int64 `json:"total"` +} + +type TagObject struct { + Id int64 `json:"id,optional"` + ObjectType string `json:"object_type,optional"` + Name string `json:"name,optional"` + DsId int64 `json:"ds_id,optional"` + DbName string `json:"db_name,optional"` + TableName string `json:"table_name,optional"` + PkColumn string `json:"pk_column,optional"` + Beid int64 `json:"beid,optional"` +} + +type TagObjectListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + ObjectType string `json:"object_type,optional" form:"object_type,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type TagObjectListResp struct { + List []TagObject `json:"list"` + Total int64 `json:"total"` +} + +type TagValueItem struct { + TagKey string `json:"tag_key,optional"` + TagName string `json:"tag_name,optional"` + TagValue string `json:"tag_value,optional"` + TaggedAt int64 `json:"tagged_at,optional"` +} + +type TaggingReq struct { + TagId int64 `json:"tag_id,optional"` + ObjectId int64 `json:"object_id,optional"` + Beid int64 `json:"beid,optional"` +} + +type TaggingResp struct { + TaggedRows int64 `json:"tagged_rows"` + TagId int64 `json:"tag_id"` +} + type TestConnResp struct { Ok bool `json:"ok"` LatencyMs int64 `json:"latency_ms"` @@ -464,6 +717,16 @@ type UpdateDataSourceReq struct { Owner string `json:"owner,optional"` } +type UpdateMetricReq struct { + Id int64 `json:"id"` + Beid int64 `json:"beid,optional"` + NameCn string `json:"name_cn,optional"` + Expr string `json:"expr,optional"` + DimensionJson string `json:"dimension_json,optional"` + Comment string `json:"comment,optional"` + Status int32 `json:"status,optional"` +} + type UserReply struct { Auid int64 `json:"auid"` Uid int64 `json:"uid"` diff --git a/metric.api b/metric.api new file mode 100644 index 0000000..39a789e --- /dev/null +++ b/metric.api @@ -0,0 +1,103 @@ +syntax = "v1" + +//数据指标(Phase 2) +type MetricInfo { + Id int64 `json:"id,optional"` + NameEn string `json:"name_en,optional"` + NameCn string `json:"name_cn,optional"` + MetricType string `json:"metric_type,optional"` + BizField string `json:"biz_field,optional"` + AggType string `json:"agg_type,optional"` + Expr string `json:"expr,optional"` + DimensionJson string `json:"dimension_json,optional"` + Comment string `json:"comment,optional"` + Owner string `json:"owner,optional"` + Status int32 `json:"status,optional"` +} + +type CreateMetricReq { + NameEn string `json:"name_en"` + NameCn string `json:"name_cn"` + MetricType string `json:"metric_type"` + BizField string `json:"biz_field,optional"` + AggType string `json:"agg_type,optional"` + Expr string `json:"expr,optional"` + DimensionJson string `json:"dimension_json,optional"` + Comment string `json:"comment,optional"` + Owner string `json:"owner,optional"` + Beid int64 `json:"beid,optional"` +} + +type UpdateMetricReq { + Id int64 `json:"id"` + Beid int64 `json:"beid,optional"` + NameCn string `json:"name_cn,optional"` + Expr string `json:"expr,optional"` + DimensionJson string `json:"dimension_json,optional"` + Comment string `json:"comment,optional"` + Status int32 `json:"status,optional"` +} + +type MetricIdReq { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type MetricListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + MetricType string `json:"metric_type,optional" form:"metric_type,optional"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type MetricListResp { + List []MetricInfo `json:"list"` + Total int64 `json:"total"` +} + +type BindAtomsReq { + DeriveMetricId int64 `json:"derive_metric_id"` + AtomMetricIds []int64 `json:"atom_metric_ids"` + Beid int64 `json:"beid,optional"` +} + +@server( + group: metric +) +service datacenter-api { + @doc( + summary: "创建指标" + ) + @handler createMetric + post /metric/create (CreateMetricReq) returns (MetricInfo) + @doc( + summary: "更新指标" + ) + @handler updateMetric + post /metric/update (UpdateMetricReq) + @doc( + summary: "删除指标" + ) + @handler deleteMetric + post /metric/delete (MetricIdReq) + @doc( + summary: "指标详情" + ) + @handler metricInfo + get /metric/info (MetricIdReq) returns (MetricInfo) + @doc( + summary: "指标列表" + ) + @handler metricList + get /metric/list (MetricListReq) returns (MetricListResp) + @doc( + summary: "派生指标绑定原子指标" + ) + @handler bindAtoms + post /metric/bind/atoms (BindAtomsReq) + @doc( + summary: "派生指标引用的原子指标" + ) + @handler metricAtoms + get /metric/atoms (MetricIdReq) returns (MetricListResp) +} diff --git a/metric/model/metricmodel.go b/metric/model/metricmodel.go new file mode 100644 index 0000000..5f23342 --- /dev/null +++ b/metric/model/metricmodel.go @@ -0,0 +1,261 @@ +package model + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +const ( + metricTable = "`dc_metric`" + metricInsertSize = 11 // 插入字段数(与 metricInsertColumns 对应) + metricIdChunk = 500 // id in 查询分片大小 + metricPageSize = 10 // 默认分页大小 +) + +// 指标类型 +const ( + MetricTypeAtom = "atom" // 原子指标 + MetricTypeDerive = "derive" // 派生指标 +) + +var ( + // metricFieldNames RawFieldNames 返回带反引号的列名(如 `id`),stringx.Remove 移除时必须同样带反引号 + metricFieldNames = builder.RawFieldNames(&Metric{}) + // metricInsertColumns 插入列(不含自增 id 与时间列) + metricInsertColumns = strings.Join(stringx.Remove(metricFieldNames, "`id`", "`created_at`", "`updated_at`"), ",") +) + +// metricRows 查询列(与 Metric 字段一一对应,列数必须与 struct 字段数一致, +// 否则少列查询扫宽 struct 会报 not matching destination to scan; +// dimension_json 允许 NULL,用 ifnull 兜底避免扫描报错) +const metricRows = "`id`,`name_en`,`name_cn`,`metric_type`,`biz_field`,`agg_type`,`expr`," + + "ifnull(dimension_json,'') as dimension_json,`comment`,`owner`,`status`,`beid`,`created_at`,`updated_at`" + +// 数据指标(atom 原子指标 / derive 派生指标) +type ( + // Metric dc_metric 行 + Metric struct { + Id int64 `db:"id"` + NameEn string `db:"name_en"` + NameCn string `db:"name_cn"` + MetricType string `db:"metric_type"` + BizField string `db:"biz_field"` + AggType string `db:"agg_type"` + Expr string `db:"expr"` + DimensionJson string `db:"dimension_json"` + Comment string `db:"comment"` + Owner string `db:"owner"` + Status int64 `db:"status"` + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + MetricModel interface { + // Insert 写入指标(uk_beid_name: name_en+beid 唯一),返回新指标 id + Insert(ctx context.Context, data *Metric) (int64, error) + // FindOneByNameEn 按 beid + 英文名查询(创建前查重) + FindOneByNameEn(ctx context.Context, beid int64, nameEn string) (*Metric, error) + // FindOne 按主键查询(beid>0 时追加多租户过滤) + FindOne(ctx context.Context, id, beid int64) (*Metric, error) + // FindList 条件分页查询(metric_type/keyword 均可选,keyword 对 name_en/name_cn 模糊匹配),返回列表与总数 + FindList(ctx context.Context, beid int64, metricType, keyword string, page, size int64) ([]*Metric, int64, error) + // FindListByIds 按主键批量查询(自动去重并分片,beid>0 时追加多租户过滤) + FindListByIds(ctx context.Context, beid int64, ids []int64) ([]*Metric, error) + // Update 部分更新:仅覆盖非空字段(name_cn/expr/dimension_json/comment) + Update(ctx context.Context, data *Metric) error + // UpdateStatus 更新指标状态 + UpdateStatus(ctx context.Context, id, beid int64, status int64) error + // Delete 按主键删除(session 可空,级联删 rel 时随事务传入) + Delete(ctx context.Context, session sqlx.Session, id, beid int64) error + } + + defaultMetricModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewMetricModel 创建数据指标 model +func NewMetricModel(conn sqlx.SqlConn) MetricModel { + return &defaultMetricModel{ + conn: conn, + table: metricTable, + } +} + +// Insert 写入指标,返回自增主键 +func (m *defaultMetricModel) Insert(ctx context.Context, data *Metric) (int64, error) { + query := fmt.Sprintf("insert into %s (%s) values (%s)", m.table, metricInsertColumns, placeholders(metricInsertSize)) + ret, err := m.conn.ExecCtx(ctx, query, data.NameEn, data.NameCn, data.MetricType, data.BizField, + data.AggType, data.Expr, data.DimensionJson, data.Comment, data.Owner, data.Status, data.Beid) + if err != nil { + return 0, err + } + id, err := ret.LastInsertId() + if err != nil { + return 0, err + } + return id, nil +} + +// FindOneByNameEn 按 beid + 英文名查询指标(uk_beid_name 查重) +func (m *defaultMetricModel) FindOneByNameEn(ctx context.Context, beid int64, nameEn string) (*Metric, error) { + if beid <= 0 { + return nil, ErrBeidRequired + } + query := fmt.Sprintf("select %s from %s where `beid` = ? and `name_en` = ? limit 1", metricRows, m.table) + var resp Metric + err := m.conn.QueryRowCtx(ctx, &resp, query, beid, nameEn) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindOne 按主键查询指标 +func (m *defaultMetricModel) FindOne(ctx context.Context, id, beid int64) (*Metric, error) { + query := fmt.Sprintf("select %s from %s where `id` = ?", metricRows, m.table) + args := []any{id} + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var resp Metric + err := m.conn.QueryRowCtx(ctx, &resp, query+" limit 1", args...) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindList 条件分页查询指标,返回列表与总数 +func (m *defaultMetricModel) FindList(ctx context.Context, beid int64, metricType, keyword string, + page, size int64) ([]*Metric, int64, error) { + if beid <= 0 { + return nil, 0, ErrBeidRequired + } + where := "`beid` = ?" + args := []any{beid} + if metricType != "" { + where += " and `metric_type` = ?" + args = append(args, metricType) + } + if kw := strings.TrimSpace(keyword); kw != "" { + where += " and (`name_en` like ? or `name_cn` like ?)" + like := "%" + escapeLike(kw) + "%" + args = append(args, like, like) + } + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) as total from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + if page <= 0 { + page = 1 + } + if size <= 0 { + size = metricPageSize + } + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ?, ?", metricRows, m.table, where) + args = append(args, (page-1)*size, size) + var list []*Metric + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} + +// FindListByIds 按主键批量查询指标(自动去重并分片) +func (m *defaultMetricModel) FindListByIds(ctx context.Context, beid int64, ids []int64) ([]*Metric, error) { + uniq := UniqInt64s(ids) + if len(uniq) == 0 { + return []*Metric{}, nil + } + + list := make([]*Metric, 0, len(uniq)) + for start := 0; start < len(uniq); start += metricIdChunk { + end := start + metricIdChunk + if end > len(uniq) { + end = len(uniq) + } + part := uniq[start:end] + query := fmt.Sprintf("select %s from %s where `id` in (%s)", metricRows, m.table, placeholders(len(part))) + args := make([]any, 0, len(part)+1) + for _, id := range part { + args = append(args, id) + } + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var rows []*Metric + if err := m.conn.QueryRowsCtx(ctx, &rows, query, args...); err != nil { + return nil, err + } + list = append(list, rows...) + } + return list, nil +} + +// Update 部分更新指标:动态拼接非空字段(name_cn/expr/dimension_json/comment),空字段保持原值 +func (m *defaultMetricModel) Update(ctx context.Context, data *Metric) error { + sets := make([]string, 0, 4) + args := make([]any, 0, 6) + if data.NameCn != "" { + sets = append(sets, "`name_cn` = ?") + args = append(args, data.NameCn) + } + if data.Expr != "" { + sets = append(sets, "`expr` = ?") + args = append(args, data.Expr) + } + if data.DimensionJson != "" { + sets = append(sets, "`dimension_json` = ?") + args = append(args, data.DimensionJson) + } + if data.Comment != "" { + sets = append(sets, "`comment` = ?") + args = append(args, data.Comment) + } + if len(sets) == 0 { + // 无可更新字段,直接返回,避免生成非法 SQL + return nil + } + query := fmt.Sprintf("update %s set %s where `id` = ? and `beid` = ?", m.table, strings.Join(sets, ",")) + args = append(args, data.Id, data.Beid) + _, err := m.conn.ExecCtx(ctx, query, args...) + return err +} + +// UpdateStatus 更新指标状态(1 启用 / 0 停用) +func (m *defaultMetricModel) UpdateStatus(ctx context.Context, id, beid int64, status int64) error { + query := fmt.Sprintf("update %s set `status` = ? where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, status, id, beid) + return err +} + +// Delete 按主键删除指标(session 可空:派生指标级联删 rel 时随事务传入) +func (m *defaultMetricModel) Delete(ctx context.Context, session sqlx.Session, id, beid int64) error { + query := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + _, err := execCtx(ctx, m.conn, session, query, id, beid) + return err +} diff --git a/metric/model/metricrelmodel.go b/metric/model/metricrelmodel.go new file mode 100644 index 0000000..6450644 --- /dev/null +++ b/metric/model/metricrelmodel.go @@ -0,0 +1,111 @@ +package model + +import ( + "context" + "fmt" + "strings" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + metricRelTable = "`dc_metric_rel`" +) + +// metricRelInsertColumns 插入列(不含自增 id) +const metricRelInsertColumns = "`derive_metric_id`,`atom_metric_id`,`beid`" + +// 指标关系(派生指标 → 原子指标引用) +type ( + // MetricRel dc_metric_rel 行 + MetricRel struct { + Id int64 `db:"id"` + DeriveMetricId int64 `db:"derive_metric_id"` + AtomMetricId int64 `db:"atom_metric_id"` + Beid int64 `db:"beid"` + } + + MetricRelModel interface { + // BindAll 覆盖式绑定派生指标的原子引用:事务内先删后插,幂等覆盖 + BindAll(ctx context.Context, session sqlx.Session, deriveMetricId, beid int64, atomIds []int64) error + // DeleteByDeriveId 删除派生指标的全部引用(删除派生指标级联用) + DeleteByDeriveId(ctx context.Context, session sqlx.Session, deriveMetricId int64) error + // ListAtoms 查询派生指标引用的原子指标 id 列表 + ListAtoms(ctx context.Context, deriveMetricId int64) ([]int64, error) + // CountByAtomId 统计原子指标被派生引用的次数(删除原子指标前置校验) + CountByAtomId(ctx context.Context, atomMetricId int64) (int64, error) + } + + defaultMetricRelModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewMetricRelModel 创建指标关系 model +func NewMetricRelModel(conn sqlx.SqlConn) MetricRelModel { + return &defaultMetricRelModel{ + conn: conn, + table: metricRelTable, + } +} + +// BindAll 覆盖式绑定:事务内先删该派生指标的全部旧引用,再批量插入新引用(atomIds 自动去重); +// uk_rel(derive_metric_id,atom_metric_id) 冲突时 insert ignore 兜底,保证重复调用幂等 +func (m *defaultMetricRelModel) BindAll(ctx context.Context, session sqlx.Session, + deriveMetricId, beid int64, atomIds []int64) error { + if err := m.DeleteByDeriveId(ctx, session, deriveMetricId); err != nil { + return err + } + uniq := UniqInt64s(atomIds) + if len(uniq) == 0 { + return nil + } + rows := strings.TrimSuffix(strings.Repeat("(?,?,?),", len(uniq)), ",") + query := fmt.Sprintf("insert ignore into %s (%s) values %s", m.table, metricRelInsertColumns, rows) + args := make([]any, 0, len(uniq)*3) + for _, id := range uniq { + args = append(args, deriveMetricId, id, beid) + } + if _, err := execCtx(ctx, m.conn, session, query, args...); err != nil { + return fmt.Errorf("批量绑定原子指标失败: %w", err) + } + return nil +} + +// DeleteByDeriveId 删除派生指标的全部原子引用 +func (m *defaultMetricRelModel) DeleteByDeriveId(ctx context.Context, session sqlx.Session, deriveMetricId int64) error { + query := fmt.Sprintf("delete from %s where `derive_metric_id` = ?", m.table) + _, err := execCtx(ctx, m.conn, session, query, deriveMetricId) + return err +} + +// atomIdRow ListAtoms 专用扫描结构(与 select atom_metric_id 单列对齐: +// go-zero 对少列查询扫宽 struct 会报 not matching destination to scan) +type atomIdRow struct { + AtomMetricId int64 `db:"atom_metric_id"` +} + +// ListAtoms 查询派生指标引用的原子指标 id 列表 +func (m *defaultMetricRelModel) ListAtoms(ctx context.Context, deriveMetricId int64) ([]int64, error) { + query := fmt.Sprintf("select `atom_metric_id` from %s where `derive_metric_id` = ?", m.table) + var rows []*atomIdRow + if err := m.conn.QueryRowsCtx(ctx, &rows, query, deriveMetricId); err != nil { + return nil, err + } + ids := make([]int64, 0, len(rows)) + for _, r := range rows { + ids = append(ids, r.AtomMetricId) + } + return ids, nil +} + +// CountByAtomId 统计原子指标被派生指标引用的次数(>0 表示被引用) +func (m *defaultMetricRelModel) CountByAtomId(ctx context.Context, atomMetricId int64) (int64, error) { + query := fmt.Sprintf("select count(*) as cnt from %s where `atom_metric_id` = ?", m.table) + var cnt int64 + if err := m.conn.QueryRowCtx(ctx, &cnt, query, atomMetricId); err != nil { + return 0, err + } + return cnt, nil +} diff --git a/metric/model/vars.go b/metric/model/vars.go new file mode 100644 index 0000000..9004580 --- /dev/null +++ b/metric/model/vars.go @@ -0,0 +1,56 @@ +package model + +import ( + "context" + "database/sql" + "errors" + "strings" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// 通用错误 +var ( + // ErrNotFound 记录不存在 + ErrNotFound = sqlx.ErrNotFound + // ErrBeidRequired 多租户 beid 缺失 + ErrBeidRequired = errors.New("beid 不能为空") +) + +// execCtx 优先在事务 session 上执行写语句,session 为空时退回普通连接 +func execCtx(ctx context.Context, conn sqlx.SqlConn, session sqlx.Session, query string, args ...any) (sql.Result, error) { + if session != nil { + return session.ExecCtx(ctx, query, args...) + } + return conn.ExecCtx(ctx, query, args...) +} + +// placeholders 生成 n 个 ? 占位符(逗号分隔) +func placeholders(n int) string { + return strings.TrimSuffix(strings.Repeat("?,", n), ",") +} + +// escapeLike 转义 like 通配符(%/_/\),防止用户输入干扰模糊匹配 +func escapeLike(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `%`, `\%`) + s = strings.ReplaceAll(s, `_`, `\_`) + return s +} + +// UniqInt64s int64 切片去重(保持原顺序,跳过非正值) +func UniqInt64s(ids []int64) []int64 { + seen := make(map[int64]struct{}, len(ids)) + uniq := make([]int64, 0, len(ids)) + for _, id := range ids { + if id <= 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + uniq = append(uniq, id) + } + return uniq +} diff --git a/metric/rpc/internal/logic/bindAtomsLogic.go b/metric/rpc/internal/logic/bindAtomsLogic.go index 65f956d..0f173f5 100644 --- a/metric/rpc/internal/logic/bindAtomsLogic.go +++ b/metric/rpc/internal/logic/bindAtomsLogic.go @@ -2,11 +2,14 @@ package logic import ( "context" + "errors" + "datacenter/metric/model" "datacenter/metric/rpc/internal/svc" "datacenter/metric/rpc/metric" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type BindAtomsLogic struct { @@ -23,9 +26,54 @@ func NewBindAtomsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindAto } } -// 派生指标绑定原子指标(幂等覆盖) +// 派生指标绑定原子指标(幂等覆盖):校验派生指标存在且 type=derive, +// 每个原子指标 id 存在且 type=atom,事务内先删旧引用再批量插入 func (l *BindAtomsLogic) BindAtoms(in *metric.BindAtomReq) (*metric.IdReq, error) { - // todo: add your logic here and delete this line + if in.DeriveMetricId <= 0 { + return nil, ErrMetricNotFound + } + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + if len(in.AtomMetricIds) == 0 { + return nil, ErrAtomIdsRequired + } + + // 1. 派生指标存在且 type=derive + derive, err := l.svcCtx.MetricModel.FindOne(l.ctx, in.DeriveMetricId, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrMetricNotFound + } + return nil, err + } + if derive.MetricType != model.MetricTypeDerive { + return nil, ErrNotDeriveMetric + } + + // 2. 原子指标 id 去重后批量校验:存在且 type=atom + atomIds := model.UniqInt64s(in.AtomMetricIds) + atoms, err := l.svcCtx.MetricModel.FindListByIds(l.ctx, in.Beid, atomIds) + if err != nil { + return nil, err + } + if len(atoms) != len(atomIds) { + return nil, ErrAtomMetricMissing + } + for _, a := range atoms { + if a.MetricType != model.MetricTypeAtom { + return nil, ErrNotAtomMetric + } + } + + // 3. 单事务覆盖绑定:先删旧引用再批量插入(幂等) + err = l.svcCtx.Conn.TransactCtx(l.ctx, func(txCtx context.Context, session sqlx.Session) error { + return l.svcCtx.MetricRelModel.BindAll(txCtx, session, in.DeriveMetricId, in.Beid, atomIds) + }) + if err != nil { + return nil, err + } - return &metric.IdReq{}, nil + l.Infof("[BindAtoms] derive_id=%d atoms=%d beid=%d", in.DeriveMetricId, len(atomIds), in.Beid) + return &metric.IdReq{Id: in.DeriveMetricId, Beid: in.Beid}, nil } diff --git a/metric/rpc/internal/logic/convert.go b/metric/rpc/internal/logic/convert.go new file mode 100644 index 0000000..3016350 --- /dev/null +++ b/metric/rpc/internal/logic/convert.go @@ -0,0 +1,33 @@ +package logic + +import ( + "datacenter/metric/model" + "datacenter/metric/rpc/metric" +) + +// metricToPb 指标 model -> pb +func metricToPb(m *model.Metric) *metric.Metric { + return &metric.Metric{ + Id: m.Id, + NameEn: m.NameEn, + NameCn: m.NameCn, + MetricType: m.MetricType, + BizField: m.BizField, + AggType: m.AggType, + Expr: m.Expr, + DimensionJson: m.DimensionJson, + Comment: m.Comment, + Owner: m.Owner, + Status: int32(m.Status), + Beid: m.Beid, + } +} + +// metricListToPb 指标列表 model -> pb(空列表返回空切片,避免 JSON 序列化成 null) +func metricListToPb(list []*model.Metric) []*metric.Metric { + out := make([]*metric.Metric, 0, len(list)) + for _, m := range list { + out = append(out, metricToPb(m)) + } + return out +} diff --git a/metric/rpc/internal/logic/createMetricLogic.go b/metric/rpc/internal/logic/createMetricLogic.go index efda2ff..196bf7c 100644 --- a/metric/rpc/internal/logic/createMetricLogic.go +++ b/metric/rpc/internal/logic/createMetricLogic.go @@ -2,7 +2,10 @@ package logic import ( "context" + "errors" + "strings" + "datacenter/metric/model" "datacenter/metric/rpc/internal/svc" "datacenter/metric/rpc/metric" @@ -23,9 +26,57 @@ func NewCreateMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Crea } } -// 创建指标 +// 创建指标:name_en/name_cn/metric_type 必填,按类型校验口径字段,name_en+beid 查重 func (l *CreateMetricLogic) CreateMetric(in *metric.CreateMetricReq) (*metric.MetricReply, error) { - // todo: add your logic here and delete this line + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + nameEn := strings.TrimSpace(in.NameEn) + if nameEn == "" { + return nil, ErrNameEnRequired + } + if strings.TrimSpace(in.NameCn) == "" { + return nil, ErrNameCnRequired + } + if in.MetricType == "" { + return nil, ErrMetricTypeRequired + } + if !IsValidMetricType(in.MetricType) { + return nil, ErrInvalidMetricType + } + // 按类型校验口径字段:atom 需 biz_field+agg_type(白名单),derive 需 expr + if err := validateMetricSpec(in.MetricType, in.BizField, in.AggType, in.Expr); err != nil { + return nil, err + } + + // 英文名查重(uk_beid_name) + _, err := l.svcCtx.MetricModel.FindOneByNameEn(l.ctx, in.Beid, nameEn) + switch { + case err == nil: + return nil, ErrDuplicateMetric + case !errors.Is(err, model.ErrNotFound): + return nil, err + } + + m := &model.Metric{ + NameEn: nameEn, + NameCn: in.NameCn, + MetricType: in.MetricType, + BizField: in.BizField, + AggType: in.AggType, + Expr: in.Expr, + DimensionJson: in.DimensionJson, + Comment: in.Comment, + Owner: in.Owner, + Status: 1, // 默认启用 + Beid: in.Beid, + } + id, err := l.svcCtx.MetricModel.Insert(l.ctx, m) + if err != nil { + return nil, err + } + m.Id = id - return &metric.MetricReply{}, nil + l.Infof("[CreateMetric] id=%d name_en=%s type=%s beid=%d", id, m.NameEn, m.MetricType, m.Beid) + return &metric.MetricReply{Metric: metricToPb(m)}, nil } diff --git a/metric/rpc/internal/logic/deleteMetricLogic.go b/metric/rpc/internal/logic/deleteMetricLogic.go index 3e84134..65e859f 100644 --- a/metric/rpc/internal/logic/deleteMetricLogic.go +++ b/metric/rpc/internal/logic/deleteMetricLogic.go @@ -2,11 +2,14 @@ package logic import ( "context" + "errors" + "datacenter/metric/model" "datacenter/metric/rpc/internal/svc" "datacenter/metric/rpc/metric" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type DeleteMetricLogic struct { @@ -23,9 +26,46 @@ func NewDeleteMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Dele } } -// 删除指标 +// 删除指标:派生指标级联删 rel;原子指标被派生引用时返回业务错误拒绝删除 func (l *DeleteMetricLogic) DeleteMetric(in *metric.IdReq) (*metric.IdReq, error) { - // todo: add your logic here and delete this line + if in.Id <= 0 { + return nil, ErrMetricNotFound + } + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + m, err := l.svcCtx.MetricModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrMetricNotFound + } + return nil, err + } + + // 原子指标:仍被派生指标引用时禁止删除 + if m.MetricType == model.MetricTypeAtom { + cnt, err := l.svcCtx.MetricRelModel.CountByAtomId(l.ctx, m.Id) + if err != nil { + return nil, err + } + if cnt > 0 { + return nil, ErrAtomReferenced + } + } + + // 派生指标:单事务先删 rel 再删本体;原子指标直接删本体 + err = l.svcCtx.Conn.TransactCtx(l.ctx, func(txCtx context.Context, session sqlx.Session) error { + if m.MetricType == model.MetricTypeDerive { + if err := l.svcCtx.MetricRelModel.DeleteByDeriveId(txCtx, session, m.Id); err != nil { + return err + } + } + return l.svcCtx.MetricModel.Delete(txCtx, session, m.Id, in.Beid) + }) + if err != nil { + return nil, err + } - return &metric.IdReq{}, nil + l.Infof("[DeleteMetric] id=%d type=%s beid=%d", m.Id, m.MetricType, in.Beid) + return &metric.IdReq{Id: in.Id, Beid: in.Beid}, nil } diff --git a/metric/rpc/internal/logic/error.go b/metric/rpc/internal/logic/error.go new file mode 100644 index 0000000..1647638 --- /dev/null +++ b/metric/rpc/internal/logic/error.go @@ -0,0 +1,39 @@ +package logic + +import "errors" + +// 业务错误(对齐 metadata 服务写法:errors.New 中文语义错误) +var ( + // ErrBeidRequired 多租户 beid 缺失 + ErrBeidRequired = errors.New("beid 不能为空") + // ErrNameEnRequired 指标英文名必填 + ErrNameEnRequired = errors.New("指标英文名 name_en 不能为空") + // ErrNameCnRequired 指标中文名必填 + ErrNameCnRequired = errors.New("指标中文名 name_cn 不能为空") + // ErrMetricTypeRequired 指标类型必填 + ErrMetricTypeRequired = errors.New("指标类型 metric_type 不能为空") + // ErrInvalidMetricType 指标类型白名单外(仅支持 atom/derive) + ErrInvalidMetricType = errors.New("指标类型 metric_type 仅支持 atom/derive") + // ErrBizFieldRequired 原子指标业务字段必填 + ErrBizFieldRequired = errors.New("原子指标必须指定业务字段 biz_field") + // ErrAggTypeRequired 原子指标聚合方式必填 + ErrAggTypeRequired = errors.New("原子指标必须指定聚合方式 agg_type") + // ErrInvalidAggType 聚合方式白名单外 + ErrInvalidAggType = errors.New("聚合方式 agg_type 仅支持 sum/count/avg/max/min/distinct") + // ErrExprRequired 派生指标表达式必填 + ErrExprRequired = errors.New("派生指标必须指定表达式 expr") + // ErrDuplicateMetric 指标英文名重复(同 beid 下 name_en 唯一) + ErrDuplicateMetric = errors.New("指标英文名 name_en 已存在") + // ErrMetricNotFound 指标不存在 + ErrMetricNotFound = errors.New("指标不存在") + // ErrNotDeriveMetric 目标指标不是派生指标 + ErrNotDeriveMetric = errors.New("该指标不是派生指标") + // ErrNotAtomMetric 存在非原子指标,无法绑定为原子引用 + ErrNotAtomMetric = errors.New("绑定的指标中存在非原子指标") + // ErrAtomIdsRequired 绑定的原子指标列表为空 + ErrAtomIdsRequired = errors.New("绑定的原子指标 id 不能为空") + // ErrAtomMetricMissing 绑定的原子指标不存在(或不属于当前 beid) + ErrAtomMetricMissing = errors.New("绑定的原子指标不存在") + // ErrAtomReferenced 原子指标被派生指标引用,禁止删除 + ErrAtomReferenced = errors.New("原子指标已被派生指标引用,禁止删除") +) diff --git a/metric/rpc/internal/logic/getMetricLogic.go b/metric/rpc/internal/logic/getMetricLogic.go index a22123c..d1c5448 100644 --- a/metric/rpc/internal/logic/getMetricLogic.go +++ b/metric/rpc/internal/logic/getMetricLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "errors" + "datacenter/metric/model" "datacenter/metric/rpc/internal/svc" "datacenter/metric/rpc/metric" @@ -25,7 +27,15 @@ func NewGetMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMetr // 指标详情 func (l *GetMetricLogic) GetMetric(in *metric.IdReq) (*metric.MetricReply, error) { - // todo: add your logic here and delete this line - - return &metric.MetricReply{}, nil + if in.Id <= 0 { + return nil, ErrMetricNotFound + } + m, err := l.svcCtx.MetricModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrMetricNotFound + } + return nil, err + } + return &metric.MetricReply{Metric: metricToPb(m)}, nil } diff --git a/metric/rpc/internal/logic/listAtomsOfDeriveLogic.go b/metric/rpc/internal/logic/listAtomsOfDeriveLogic.go index 33072f8..6d6cc40 100644 --- a/metric/rpc/internal/logic/listAtomsOfDeriveLogic.go +++ b/metric/rpc/internal/logic/listAtomsOfDeriveLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "errors" + "datacenter/metric/model" "datacenter/metric/rpc/internal/svc" "datacenter/metric/rpc/metric" @@ -23,9 +25,33 @@ func NewListAtomsOfDeriveLogic(ctx context.Context, svcCtx *svc.ServiceContext) } } -// 取派生指标引用的原子指标 +// 取派生指标引用的原子指标:先确认指标存在且 type=derive,再按 rel 反查原子指标回填 func (l *ListAtomsOfDeriveLogic) ListAtomsOfDerive(in *metric.IdReq) (*metric.MetricListReply, error) { - // todo: add your logic here and delete this line + if in.Id <= 0 { + return nil, ErrMetricNotFound + } + m, err := l.svcCtx.MetricModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrMetricNotFound + } + return nil, err + } + if m.MetricType != model.MetricTypeDerive { + return nil, ErrNotDeriveMetric + } - return &metric.MetricListReply{}, nil + atomIds, err := l.svcCtx.MetricRelModel.ListAtoms(l.ctx, m.Id) + if err != nil { + return nil, err + } + if len(atomIds) == 0 { + return &metric.MetricListReply{List: []*metric.Metric{}}, nil + } + + atoms, err := l.svcCtx.MetricModel.FindListByIds(l.ctx, in.Beid, atomIds) + if err != nil { + return nil, err + } + return &metric.MetricListReply{List: metricListToPb(atoms), Total: int64(len(atoms))}, nil } diff --git a/metric/rpc/internal/logic/listMetricsByIdsLogic.go b/metric/rpc/internal/logic/listMetricsByIdsLogic.go index dc94ae6..ee50776 100644 --- a/metric/rpc/internal/logic/listMetricsByIdsLogic.go +++ b/metric/rpc/internal/logic/listMetricsByIdsLogic.go @@ -23,9 +23,14 @@ func NewListMetricsByIdsLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } } -// 按 id 批量取指标 +// 按 id 批量取指标(model 内自动去重并分片,total 返回实际命中条数) func (l *ListMetricsByIdsLogic) ListMetricsByIds(in *metric.MetricListByIdsReq) (*metric.MetricListReply, error) { - // todo: add your logic here and delete this line - - return &metric.MetricListReply{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + list, err := l.svcCtx.MetricModel.FindListByIds(l.ctx, in.Beid, in.Ids) + if err != nil { + return nil, err + } + return &metric.MetricListReply{List: metricListToPb(list), Total: int64(len(list))}, nil } diff --git a/metric/rpc/internal/logic/listMetricsLogic.go b/metric/rpc/internal/logic/listMetricsLogic.go index c15b208..9ffbede 100644 --- a/metric/rpc/internal/logic/listMetricsLogic.go +++ b/metric/rpc/internal/logic/listMetricsLogic.go @@ -23,9 +23,14 @@ func NewListMetricsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListM } } -// 指标列表 +// 指标列表:分页 + metric_type 过滤 + keyword 对 name_en/name_cn 模糊匹配 func (l *ListMetricsLogic) ListMetrics(in *metric.MetricListReq) (*metric.MetricListReply, error) { - // todo: add your logic here and delete this line - - return &metric.MetricListReply{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + list, total, err := l.svcCtx.MetricModel.FindList(l.ctx, in.Beid, in.MetricType, in.Keyword, in.Page, in.Size) + if err != nil { + return nil, err + } + return &metric.MetricListReply{List: metricListToPb(list), Total: total}, nil } diff --git a/metric/rpc/internal/logic/updateMetricLogic.go b/metric/rpc/internal/logic/updateMetricLogic.go index 280e2ad..d81c756 100644 --- a/metric/rpc/internal/logic/updateMetricLogic.go +++ b/metric/rpc/internal/logic/updateMetricLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "errors" + "datacenter/metric/model" "datacenter/metric/rpc/internal/svc" "datacenter/metric/rpc/metric" @@ -23,9 +25,42 @@ func NewUpdateMetricLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Upda } } -// 更新指标 +// 更新指标:部分更新,字符串字段(name_cn/expr/dimension_json/comment)非空才覆盖, +// status 约定仅非 0 时更新(proto3 无法区分未传与 0) func (l *UpdateMetricLogic) UpdateMetric(in *metric.UpdateMetricReq) (*metric.IdReq, error) { - // todo: add your logic here and delete this line + if in.Id <= 0 { + return nil, ErrMetricNotFound + } + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + // 存在性校验(同时限定多租户) + if _, err := l.svcCtx.MetricModel.FindOne(l.ctx, in.Id, in.Beid); err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrMetricNotFound + } + return nil, err + } + + // 字符串字段部分更新(model 内非空才拼 set) + if err := l.svcCtx.MetricModel.Update(l.ctx, &model.Metric{ + Id: in.Id, + Beid: in.Beid, + NameCn: in.NameCn, + Expr: in.Expr, + DimensionJson: in.DimensionJson, + Comment: in.Comment, + }); err != nil { + return nil, err + } + + // 状态单独更新(仅非 0 视为需要变更) + if in.Status != 0 { + if err := l.svcCtx.MetricModel.UpdateStatus(l.ctx, in.Id, in.Beid, int64(in.Status)); err != nil { + return nil, err + } + } - return &metric.IdReq{}, nil + l.Infof("[UpdateMetric] id=%d beid=%d status=%d", in.Id, in.Beid, in.Status) + return &metric.IdReq{Id: in.Id, Beid: in.Beid}, nil } diff --git a/metric/rpc/internal/logic/validate.go b/metric/rpc/internal/logic/validate.go new file mode 100644 index 0000000..afb77b9 --- /dev/null +++ b/metric/rpc/internal/logic/validate.go @@ -0,0 +1,47 @@ +package logic + +import "datacenter/metric/model" + +// aggTypeWhitelist 聚合方式白名单(对应 dc_metric.agg_type 注释口径) +var aggTypeWhitelist = map[string]struct{}{ + "sum": {}, + "count": {}, + "avg": {}, + "max": {}, + "min": {}, + "distinct": {}, +} + +// IsValidMetricType 指标类型校验(仅支持 atom/derive) +func IsValidMetricType(metricType string) bool { + return metricType == model.MetricTypeAtom || metricType == model.MetricTypeDerive +} + +// IsValidAggType 聚合方式白名单校验 +func IsValidAggType(aggType string) bool { + _, ok := aggTypeWhitelist[aggType] + return ok +} + +// validateMetricSpec 按指标类型校验口径字段(纯函数,创建指标复用,单测覆盖): +// - atom:biz_field + agg_type 必填,且 agg_type 必须在白名单内 +// - derive:expr 必填 +func validateMetricSpec(metricType, bizField, aggType, expr string) error { + if metricType == model.MetricTypeAtom { + if bizField == "" { + return ErrBizFieldRequired + } + if aggType == "" { + return ErrAggTypeRequired + } + if !IsValidAggType(aggType) { + return ErrInvalidAggType + } + return nil + } + // derive + if expr == "" { + return ErrExprRequired + } + return nil +} diff --git a/metric/rpc/internal/logic/validate_test.go b/metric/rpc/internal/logic/validate_test.go new file mode 100644 index 0000000..8bb0ee3 --- /dev/null +++ b/metric/rpc/internal/logic/validate_test.go @@ -0,0 +1,64 @@ +package logic + +import ( + "errors" + "testing" +) + +// 聚合方式白名单:6 个合法取值全部通过 +func TestIsValidAggTypeWhitelist(t *testing.T) { + for _, agg := range []string{"sum", "count", "avg", "max", "min", "distinct"} { + if !IsValidAggType(agg) { + t.Fatalf("IsValidAggType(%q) = false, want true", agg) + } + } +} + +// 聚合方式白名单外的取值一律拒绝(含大小写变体、空白、空串) +func TestIsValidAggTypeRejectsInvalid(t *testing.T) { + for _, agg := range []string{"", "SUM", "Sum", "AVG", "median", "top", " sum", "sum "} { + if IsValidAggType(agg) { + t.Fatalf("IsValidAggType(%q) = true, want false", agg) + } + } +} + +// 指标类型校验:仅 atom/derive 合法,其余(空串/大小写变体/近似词)拒绝 +func TestIsValidMetricType(t *testing.T) { + for _, mt := range []string{"atom", "derive"} { + if !IsValidMetricType(mt) { + t.Fatalf("IsValidMetricType(%q) = false, want true", mt) + } + } + for _, mt := range []string{"", "ATOM", "DERIVE", "atomic", "derived", "atom "} { + if IsValidMetricType(mt) { + t.Fatalf("IsValidMetricType(%q) = true, want false", mt) + } + } +} + +// 原子指标口径校验:biz_field/agg_type 必填且 agg_type 必须在白名单内 +func TestValidateMetricSpecAtom(t *testing.T) { + if err := validateMetricSpec("atom", "pay_amount", "sum", ""); err != nil { + t.Fatalf("合法原子指标校验失败: %v", err) + } + if err := validateMetricSpec("atom", "", "sum", ""); !errors.Is(err, ErrBizFieldRequired) { + t.Fatalf("缺 biz_field 应报 ErrBizFieldRequired, got %v", err) + } + if err := validateMetricSpec("atom", "pay_amount", "", ""); !errors.Is(err, ErrAggTypeRequired) { + t.Fatalf("缺 agg_type 应报 ErrAggTypeRequired, got %v", err) + } + if err := validateMetricSpec("atom", "pay_amount", "median", ""); !errors.Is(err, ErrInvalidAggType) { + t.Fatalf("白名单外 agg_type 应报 ErrInvalidAggType, got %v", err) + } +} + +// 派生指标口径校验:expr 必填(biz_field/agg_type 不参与校验) +func TestValidateMetricSpecDerive(t *testing.T) { + if err := validateMetricSpec("derive", "", "", "sum(pay_amount) where status=1 by day"); err != nil { + t.Fatalf("合法派生指标校验失败: %v", err) + } + if err := validateMetricSpec("derive", "pay_amount", "sum", ""); !errors.Is(err, ErrExprRequired) { + t.Fatalf("缺 expr 应报 ErrExprRequired, got %v", err) + } +} diff --git a/metric/rpc/internal/svc/serviceContext.go b/metric/rpc/internal/svc/serviceContext.go index 6987423..0f78378 100644 --- a/metric/rpc/internal/svc/serviceContext.go +++ b/metric/rpc/internal/svc/serviceContext.go @@ -1,13 +1,25 @@ package svc -import "datacenter/metric/rpc/internal/config" +import ( + "datacenter/metric/model" + "datacenter/metric/rpc/internal/config" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) type ServiceContext struct { - Config config.Config + Config config.Config + Conn sqlx.SqlConn // 主库连接(跨表事务编排用:级联删 rel/覆盖绑定) + MetricModel model.MetricModel + MetricRelModel model.MetricRelModel } func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ - Config: c, + Config: c, + Conn: conn, + MetricModel: model.NewMetricModel(conn), + MetricRelModel: model.NewMetricRelModel(conn), } } diff --git a/quality.api b/quality.api new file mode 100644 index 0000000..6a8e993 --- /dev/null +++ b/quality.api @@ -0,0 +1,139 @@ +syntax = "v1" + +//数据质量(Phase 2) +type QualityRule { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + RuleType string `json:"rule_type,optional"` + TargetTable string `json:"target_table,optional"` + TargetColumn string `json:"target_column,optional"` + ParamsJson string `json:"params_json,optional"` + ThresholdJson string `json:"threshold_json,optional"` + Status int32 `json:"status,optional"` + Owner string `json:"owner,optional"` +} + +type CreateQualityRuleReq { + Name string `json:"name"` + RuleType string `json:"rule_type"` + TargetTable string `json:"target_table"` + TargetColumn string `json:"target_column,optional"` + ParamsJson string `json:"params_json,optional"` + ThresholdJson string `json:"threshold_json,optional"` + Owner string `json:"owner,optional"` + Beid int64 `json:"beid,optional"` +} + +type QualityIdReq { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type QualityRuleListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + RuleType string `json:"rule_type,optional" form:"rule_type,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type QualityRuleListResp { + List []QualityRule `json:"list"` + Total int64 `json:"total"` +} + +type CreateQualityJobReq { + Name string `json:"name"` + DatasourceId int64 `json:"datasource_id"` + DbName string `json:"db_name,optional"` + RuleIds string `json:"rule_ids"` + Cron string `json:"cron,optional"` + Beid int64 `json:"beid,optional"` +} +type QualityJob { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + DatasourceId int64 `json:"datasource_id,optional"` + DbName string `json:"db_name,optional"` + RuleIds string `json:"rule_ids,optional"` + Cron string `json:"cron,optional"` + LastStatus string `json:"last_status,optional"` + LastRunAt int64 `json:"last_run_at,optional"` + Beid int64 `json:"beid,optional"` +} +type JobListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type JobListResp { + List []QualityJob `json:"list"` + Total int64 `json:"total"` +} + +type RunJobResp { + JobId int64 `json:"job_id"` + TotalRules int64 `json:"total_rules"` + Passed int64 `json:"passed"` + LastStatus string `json:"last_status"` +} + +type QualityResult { + Id int64 `json:"id,optional"` + JobId int64 `json:"job_id,optional"` + RuleId int64 `json:"rule_id,optional"` + RuleType string `json:"rule_type,optional"` + CheckValue string `json:"check_value,optional"` + PassFlag bool `json:"pass_flag,optional"` + DetailJson string `json:"detail_json,optional"` + RunAt int64 `json:"run_at,optional"` +} +type ResultListReq { + JobId int64 `json:"job_id,optional" form:"job_id,optional"` + PassFilter int64 `json:"pass_filter,optional" form:"pass_filter,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type ResultListResp { + List []QualityResult `json:"list"` + Total int64 `json:"total"` +} + +@server( + group: quality +) +service datacenter-api { + @doc( + summary: "创建质量规则" + ) + @handler createQualityRule + post /quality/rule/create (CreateQualityRuleReq) returns (QualityRule) + @doc( + summary: "删除质量规则" + ) + @handler deleteQualityRule + post /quality/rule/delete (QualityIdReq) + @doc( + summary: "质量规则列表" + ) + @handler qualityRuleList + get /quality/rule/list (QualityRuleListReq) returns (QualityRuleListResp) + @doc( + summary: "创建质量任务" + ) + @handler createQualityJob + post /quality/job/create (CreateQualityJobReq) returns (QualityJob) + @doc( + summary: "质量任务列表" + ) + @handler qualityJobList + get /quality/job/list (JobListReq) returns (JobListResp) + @doc( + summary: "执行质量任务" + ) + @handler runQualityJob + post /quality/job/run (QualityIdReq) returns (RunJobResp) + @doc( + summary: "质量结果列表" + ) + @handler qualityResultList + get /quality/result/list (ResultListReq) returns (ResultListResp) +} diff --git a/quality/model/jobmodel.go b/quality/model/jobmodel.go new file mode 100644 index 0000000..bd294f1 --- /dev/null +++ b/quality/model/jobmodel.go @@ -0,0 +1,153 @@ +package model + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + jobTable = "`dc_quality_job`" + jobInsertFieldSize = 6 // 插入字段数(与 jobInsertColumns 对应) + // jobInsertColumns 插入字段(不含自增 id、时间列与运行状态列,last_status/last_run_at 由 RunJob 回填) + jobInsertColumns = "name,datasource_id,db_name,rule_ids,cron,beid" + // jobRows 查询列(last_run_at 允许 NULL,转毫秒时间戳并以 0 兜底避免扫描报错; + // 查询列数与 Job 字段数一一对应,规避 go-zero 少列扫宽 struct 的 not matching destination 报错) + jobRows = "id,name,datasource_id,db_name,rule_ids,cron,last_status," + + "ifnull(cast(unix_timestamp(last_run_at) * 1000 as signed), 0) as last_run_at," + + "beid,created_at,updated_at" +) + +// 质量任务(dc_quality_job) +type ( + // Job 质量任务行 + Job struct { + Id int64 `db:"id"` + Name string `db:"name"` + DatasourceId int64 `db:"datasource_id"` // 目标数据源 + DbName string `db:"db_name"` // 目标库(空则使用数据源默认库) + RuleIds string `db:"rule_ids"` // 规则id,逗号分隔 + Cron string `db:"cron"` // cron 表达式(Phase 3 调度接入,本期仅存档) + LastStatus string `db:"last_status"` // success/failed/partial + LastRunAt int64 `db:"last_run_at"` // 最近执行时间(毫秒时间戳,未运行为 0) + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + // JobModel 质量任务表操作接口 + JobModel interface { + // Insert 新增任务,返回自增 id + Insert(ctx context.Context, data *Job) (int64, error) + // FindOne 按主键查询(beid>0 时追加多租户过滤) + FindOne(ctx context.Context, id, beid int64) (*Job, error) + // Update 更新任务可编辑字段(按 id+beid 定位) + Update(ctx context.Context, data *Job) error + // Delete 删除任务(按 id+beid 定位) + Delete(ctx context.Context, id, beid int64) error + // FindList 任务分页列表:beid 必带;返回列表与总数 + FindList(ctx context.Context, beid, page, size int64) ([]*Job, int64, error) + // UpdateRunStatus 任务执行完成后回填最近状态与执行时间 + UpdateRunStatus(ctx context.Context, id, beid int64, status string, runAt time.Time) error + } + + defaultJobModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewJobModel 创建质量任务 model +func NewJobModel(conn sqlx.SqlConn) JobModel { + return &defaultJobModel{ + conn: conn, + table: jobTable, + } +} + +// Insert 新增质量任务 +func (m *defaultJobModel) Insert(ctx context.Context, data *Job) (int64, error) { + query := fmt.Sprintf("insert into %s (%s) values (%s)", + m.table, jobInsertColumns, placeholders(jobInsertFieldSize)) + ret, err := m.conn.ExecCtx(ctx, query, + data.Name, data.DatasourceId, data.DbName, data.RuleIds, data.Cron, data.Beid) + if err != nil { + return 0, err + } + return ret.LastInsertId() +} + +// FindOne 按主键查询任务 +func (m *defaultJobModel) FindOne(ctx context.Context, id, beid int64) (*Job, error) { + query := fmt.Sprintf("select %s from %s where `id` = ?", jobRows, m.table) + args := []any{id} + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var resp Job + err := m.conn.QueryRowCtx(ctx, &resp, query+" limit 1", args...) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// Update 更新任务(按 id+beid 定位,运行状态列由 UpdateRunStatus 维护) +func (m *defaultJobModel) Update(ctx context.Context, data *Job) error { + query := fmt.Sprintf("update %s set `name` = ?, `datasource_id` = ?, `db_name` = ?, "+ + "`rule_ids` = ?, `cron` = ? where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, + data.Name, data.DatasourceId, data.DbName, data.RuleIds, data.Cron, + data.Id, data.Beid) + return err +} + +// Delete 删除任务(结果历史不级联清理,供问题追溯) +func (m *defaultJobModel) Delete(ctx context.Context, id, beid int64) error { + query := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, id, beid) + return err +} + +// FindList 任务列表(beid 多租户必带),返回列表与总数 +func (m *defaultJobModel) FindList(ctx context.Context, beid, page, size int64) ([]*Job, int64, error) { + if beid <= 0 { + return nil, 0, ErrBeidRequired + } + page, size = normalizePage(page, size) + + where := "`beid` = ?" + args := []any{beid} + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页数据 + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ?, ?", + jobRows, m.table, where) + args = append(args, (page-1)*size, size) + var list []*Job + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} + +// UpdateRunStatus 任务执行完成后回填 last_status/last_run_at +func (m *defaultJobModel) UpdateRunStatus(ctx context.Context, id, beid int64, status string, runAt time.Time) error { + query := fmt.Sprintf("update %s set `last_status` = ?, `last_run_at` = ? where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, status, runAt, id, beid) + return err +} diff --git a/quality/model/resultmodel.go b/quality/model/resultmodel.go new file mode 100644 index 0000000..4755072 --- /dev/null +++ b/quality/model/resultmodel.go @@ -0,0 +1,116 @@ +package model + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + resultTable = "`dc_quality_result`" + resultInsertFieldSize = 7 // 插入字段数(与 resultInsertColumns 对应) + resultInsertChunk = 500 // 批量插入分片大小 + // resultInsertColumns 插入字段(不含自增 id,run_at 显式传入保证批量运行时间一致) + resultInsertColumns = "job_id,rule_id,rule_type,check_value,pass_flag,detail_json,run_at" + // resultRows 查询列(detail_json 允许 NULL 用 ifnull 兜底;run_at 转毫秒时间戳并以 0 兜底, + // 查询列数与 Result 字段数一一对应,规避 go-zero 少列扫宽 struct 的 not matching destination 报错) + resultRows = "id,job_id,rule_id,rule_type,check_value,pass_flag,ifnull(detail_json,'') as detail_json," + + "ifnull(cast(unix_timestamp(run_at) * 1000 as signed), 0) as run_at" +) + +// 质量检查结果(dc_quality_result) +type ( + // Result 质量检查结果行 + Result struct { + Id int64 `db:"id"` + JobId int64 `db:"job_id"` + RuleId int64 `db:"rule_id"` + RuleType string `db:"rule_type"` + CheckValue string `db:"check_value"` // 检查实测值 + PassFlag int64 `db:"pass_flag"` // 1.通过 0.不通过 + DetailJson string `db:"detail_json"` // 详情(阈值/实测/差异/错误原因) + RunAt int64 `db:"run_at"` // 执行时间(毫秒时间戳) + } + + // ResultModel 质量结果表操作接口 + ResultModel interface { + // BatchInsert 批量写入检查结果(一次任务的所有规则结果一次落库) + BatchInsert(ctx context.Context, results []*Result) error + // FindListByJob 按任务分页查询结果:passFilter 0.全部 1.仅通过 2.仅不通过;返回列表与总数 + FindListByJob(ctx context.Context, jobId, passFilter, page, size int64) ([]*Result, int64, error) + } + + defaultResultModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewResultModel 创建质量结果 model +func NewResultModel(conn sqlx.SqlConn) ResultModel { + return &defaultResultModel{ + conn: conn, + table: resultTable, + } +} + +// BatchInsert 批量写入检查结果:分片拼接多行 values +func (m *defaultResultModel) BatchInsert(ctx context.Context, results []*Result) error { + if len(results) == 0 { + return nil + } + prefix := fmt.Sprintf("insert into %s (%s) values ", m.table, resultInsertColumns) + for start := 0; start < len(results); start += resultInsertChunk { + end := start + resultInsertChunk + if end > len(results) { + end = len(results) + } + batch := results[start:end] + query := prefix + strings.TrimSuffix(strings.Repeat("("+placeholders(resultInsertFieldSize)+"),", len(batch)), ",") + args := make([]any, 0, len(batch)*resultInsertFieldSize) + for _, r := range batch { + args = append(args, r.JobId, r.RuleId, r.RuleType, r.CheckValue, r.PassFlag, r.DetailJson, time.UnixMilli(r.RunAt)) + } + if _, err := m.conn.ExecCtx(ctx, query, args...); err != nil { + return fmt.Errorf("批量写入质量结果失败: %w", err) + } + } + return nil +} + +// FindListByJob 按任务分页查询结果(passFilter 0.全部 1.仅通过 2.仅不通过),返回列表与总数 +func (m *defaultResultModel) FindListByJob(ctx context.Context, jobId, passFilter, page, size int64) ([]*Result, int64, error) { + if jobId <= 0 { + return nil, 0, ErrJobIdRequired + } + page, size = normalizePage(page, size) + + where := "`job_id` = ?" + args := []any{jobId} + switch passFilter { + case 1: // 仅通过 + where += " and `pass_flag` = 1" + case 2: // 仅不通过 + where += " and `pass_flag` = 0" + } + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页数据 + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ?, ?", + resultRows, m.table, where) + args = append(args, (page-1)*size, size) + var list []*Result + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} diff --git a/quality/model/rulemodel.go b/quality/model/rulemodel.go new file mode 100644 index 0000000..5634006 --- /dev/null +++ b/quality/model/rulemodel.go @@ -0,0 +1,186 @@ +package model + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + ruleTable = "`dc_quality_rule`" + ruleInsertFieldSize = 9 // 插入字段数(与 ruleInsertColumns 对应) + // ruleInsertColumns 插入字段(不含自增 id 与时间列),与 Insert 传参顺序一一对应 + ruleInsertColumns = "name,rule_type,target_table,target_column,params_json,threshold_json,status,owner,beid" + // ruleRows 查询列(params_json/threshold_json 允许 NULL,用 ifnull 兜底避免扫描报错; + // 查询列数与 Rule 字段数一一对应,规避 go-zero 少列扫宽 struct 的 not matching destination 报错) + ruleRows = "id,name,rule_type,target_table,target_column," + + "ifnull(params_json,'') as params_json,ifnull(threshold_json,'') as threshold_json," + + "status,owner,beid,created_at,updated_at" +) + +// 质量规则(dc_quality_rule) +type ( + // Rule 质量规则行 + Rule struct { + Id int64 `db:"id"` + Name string `db:"name"` + RuleType string `db:"rule_type"` // row_count/not_null/unique/value_range/consistency/fluctuation + TargetTable string `db:"target_table"` + TargetColumn string `db:"target_column"` + ParamsJson string `db:"params_json"` // value_range={min,max}; consistency={ref_table,ref_column}; fluctuation={baseline,ratio_pct} + ThresholdJson string `db:"threshold_json"` // 期望阈值: {op:gt|gte|lt|lte|eq|between, value:..}(row_count 用) + Status int64 `db:"status"` // 1.启用 0.停用 + Owner string `db:"owner"` + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + // RuleModel 质量规则表操作接口 + RuleModel interface { + // Insert 新增规则,返回自增 id + Insert(ctx context.Context, data *Rule) (int64, error) + // FindOne 按主键查询(beid>0 时追加多租户过滤) + FindOne(ctx context.Context, id, beid int64) (*Rule, error) + // Update 更新规则可编辑字段(按 id+beid 定位) + Update(ctx context.Context, data *Rule) error + // Delete 删除规则(按 id+beid 定位) + Delete(ctx context.Context, id, beid int64) error + // FindList 条件分页查询:beid 必带,ruleType 可选过滤;返回列表与总数 + FindList(ctx context.Context, beid int64, ruleType string, page, size int64) ([]*Rule, int64, error) + // FindListByIds 按主键批量查询(RunJob 加载任务规则用,自动去重分片) + FindListByIds(ctx context.Context, beid int64, ids []int64) ([]*Rule, error) + } + + defaultRuleModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewRuleModel 创建质量规则 model +func NewRuleModel(conn sqlx.SqlConn) RuleModel { + return &defaultRuleModel{ + conn: conn, + table: ruleTable, + } +} + +// Insert 新增质量规则 +func (m *defaultRuleModel) Insert(ctx context.Context, data *Rule) (int64, error) { + query := fmt.Sprintf("insert into %s (%s) values (%s)", + m.table, ruleInsertColumns, placeholders(ruleInsertFieldSize)) + ret, err := m.conn.ExecCtx(ctx, query, + data.Name, data.RuleType, data.TargetTable, data.TargetColumn, + data.ParamsJson, data.ThresholdJson, data.Status, data.Owner, data.Beid) + if err != nil { + return 0, err + } + return ret.LastInsertId() +} + +// FindOne 按主键查询规则 +func (m *defaultRuleModel) FindOne(ctx context.Context, id, beid int64) (*Rule, error) { + query := fmt.Sprintf("select %s from %s where `id` = ?", ruleRows, m.table) + args := []any{id} + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var resp Rule + err := m.conn.QueryRowCtx(ctx, &resp, query+" limit 1", args...) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// Update 更新规则(按 id+beid 定位,时间列由数据库自动维护) +func (m *defaultRuleModel) Update(ctx context.Context, data *Rule) error { + query := fmt.Sprintf("update %s set `name` = ?, `rule_type` = ?, `target_table` = ?, `target_column` = ?, "+ + "`params_json` = ?, `threshold_json` = ?, `status` = ?, `owner` = ? where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, + data.Name, data.RuleType, data.TargetTable, data.TargetColumn, + data.ParamsJson, data.ThresholdJson, data.Status, data.Owner, + data.Id, data.Beid) + return err +} + +// Delete 删除规则 +func (m *defaultRuleModel) Delete(ctx context.Context, id, beid int64) error { + query := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, id, beid) + return err +} + +// FindList 规则列表(beid 多租户必带 + rule_type 可选过滤),返回列表与总数 +func (m *defaultRuleModel) FindList(ctx context.Context, beid int64, ruleType string, + page, size int64) ([]*Rule, int64, error) { + if beid <= 0 { + return nil, 0, ErrBeidRequired + } + page, size = normalizePage(page, size) + + where := "`beid` = ?" + args := []any{beid} + if ruleType != "" { + where += " and `rule_type` = ?" + args = append(args, ruleType) + } + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页数据 + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ?, ?", + ruleRows, m.table, where) + args = append(args, (page-1)*size, size) + var list []*Rule + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} + +// FindListByIds 按主键批量查询规则(自动去重并分片,beid 多租户过滤) +func (m *defaultRuleModel) FindListByIds(ctx context.Context, beid int64, ids []int64) ([]*Rule, error) { + if beid <= 0 { + return nil, ErrBeidRequired + } + if len(ids) == 0 { + return []*Rule{}, nil + } + uniq := dedupeIds(ids) + list := make([]*Rule, 0, len(uniq)) + for start := 0; start < len(uniq); start += idQueryChunk { + end := start + idQueryChunk + if end > len(uniq) { + end = len(uniq) + } + part := uniq[start:end] + query := fmt.Sprintf("select %s from %s where `id` in (%s) and `beid` = ?", + ruleRows, m.table, placeholders(len(part))) + args := make([]any, 0, len(part)+1) + for _, id := range part { + args = append(args, id) + } + args = append(args, beid) + var rows []*Rule + if err := m.conn.QueryRowsCtx(ctx, &rows, query, args...); err != nil { + return nil, err + } + list = append(list, rows...) + } + return list, nil +} diff --git a/quality/model/vars.go b/quality/model/vars.go new file mode 100644 index 0000000..0da17e7 --- /dev/null +++ b/quality/model/vars.go @@ -0,0 +1,71 @@ +package model + +import ( + "errors" + "strings" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// 通用错误 +var ( + // ErrNotFound 记录不存在 + ErrNotFound = sqlx.ErrNotFound + // ErrBeidRequired 多租户 beid 缺失 + ErrBeidRequired = errors.New("beid 不能为空") + // ErrJobIdRequired 任务 id 缺失 + ErrJobIdRequired = errors.New("job_id 不能为空") +) + +// 任务最近一次执行状态(dc_quality_job.last_status) +const ( + JobStatusSuccess = "success" // 全部规则通过 + JobStatusFailed = "failed" // 全部规则不通过 + JobStatusPartial = "partial" // 部分规则通过 +) + +// 规则状态(dc_quality_rule.status) +const ( + RuleStatusEnabled = 1 // 启用 + RuleStatusDisabled = 0 // 停用 +) + +// 分页缺省值与上限 +const ( + defaultPageSize = 10 + maxPageSize = 200 + idQueryChunk = 500 // id in 查询分片大小 +) + +// placeholders 生成 n 个 ? 占位符(逗号分隔) +func placeholders(n int) string { + return strings.TrimSuffix(strings.Repeat("?,", n), ",") +} + +// normalizePage 分页参数防御:page 缺省 1,size 缺省 10、封顶 200 +func normalizePage(page, size int64) (int64, int64) { + if page < 1 { + page = 1 + } + if size <= 0 { + size = defaultPageSize + } + if size > maxPageSize { + size = maxPageSize + } + return page, size +} + +// dedupeIds id 去重并保持原有顺序 +func dedupeIds(ids []int64) []int64 { + seen := make(map[int64]struct{}, len(ids)) + uniq := make([]int64, 0, len(ids)) + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + uniq = append(uniq, id) + } + return uniq +} diff --git a/quality/rpc/internal/logic/checker.go b/quality/rpc/internal/logic/checker.go new file mode 100644 index 0000000..afa2c8d --- /dev/null +++ b/quality/rpc/internal/logic/checker.go @@ -0,0 +1,454 @@ +package logic + +import ( + "encoding/json" + "fmt" + "math" + "regexp" + "strconv" + "strings" +) + +// 质量规则类型(dc_quality_rule.rule_type 白名单) +const ( + RuleTypeRowCount = "row_count" // 行数检查 + RuleTypeNotNull = "not_null" // 非空检查 + RuleTypeUnique = "unique" // 唯一性检查 + RuleTypeValueRange = "value_range" // 值域检查 + RuleTypeConsistency = "consistency" // 一致性检查(外键引用完整性) + RuleTypeFluctuation = "fluctuation" // 波动率检查(与基线行数比较) +) + +// identifierRe 标识符白名单:表名/列名/库名仅允许字母、数字、下划线, +// 不通过者一律拒绝拼接(杜绝 `users; drop table x` 类注入) +var identifierRe = regexp.MustCompile(`^[A-Za-z0-9_]+$`) + +// ruleTypeSet 规则类型白名单集合 +var ruleTypeSet = map[string]struct{}{ + RuleTypeRowCount: {}, + RuleTypeNotNull: {}, + RuleTypeUnique: {}, + RuleTypeValueRange: {}, + RuleTypeConsistency: {}, + RuleTypeFluctuation: {}, +} + +// ruleParams params_json 解析结果(按规则类型取用) +type ruleParams struct { + // value_range + Min, Max float64 + hasMin bool + hasMax bool + // consistency + RefTable, RefColumn string + // fluctuation + Baseline, RatioPct float64 + hasBaseline bool + hasRatio bool +} + +// IsValidRuleType 规则类型是否在白名单内 +func IsValidRuleType(ruleType string) bool { + _, ok := ruleTypeSet[ruleType] + return ok +} + +// ValidIdentifier 标识符(库/表/列名)白名单校验,防 SQL 注入 +func ValidIdentifier(name string) bool { + return identifierRe.MatchString(name) +} + +// QualifyTable 生成 `.` 全限定表名(db 为空时使用 DSN 默认库), +// 两侧标识符均先过白名单校验 +func QualifyTable(db, table string) (string, error) { + if !ValidIdentifier(table) { + return "", fmt.Errorf("%w: 表名 %q", ErrInvalidIdentifier, table) + } + if db == "" { + return table, nil + } + if !ValidIdentifier(db) { + return "", fmt.Errorf("%w: 库名 %q", ErrInvalidIdentifier, db) + } + return db + "." + table, nil +} + +// ValidateRule 创建规则前的静态校验:类型白名单 + 标识符白名单 + +// 按类型的 target_column/params_json 必填性(not_null/unique/value_range/consistency 需 target_column; +// value_range 需 {min,max};consistency 需 {ref_table,ref_column};fluctuation 需 {baseline,ratio_pct}) +func ValidateRule(ruleType, targetTable, targetColumn, paramsJson string) error { + if !IsValidRuleType(ruleType) { + return ErrInvalidRuleType + } + if strings.TrimSpace(targetTable) == "" { + return ErrTargetTableRequired + } + if !ValidIdentifier(targetTable) { + return fmt.Errorf("%w: target_table %q", ErrInvalidIdentifier, targetTable) + } + // not_null/unique/value_range/consistency 的检查 SQL 均需拼接目标列 + if ruleNeedsColumn(ruleType) && targetColumn == "" { + return fmt.Errorf("%w: %s 需要 target_column", ErrInvalidRuleParams, ruleType) + } + if targetColumn != "" && !ValidIdentifier(targetColumn) { + return fmt.Errorf("%w: target_column %q", ErrInvalidIdentifier, targetColumn) + } + + switch ruleType { + case RuleTypeValueRange: + p, err := parseRuleParams(paramsJson) + if err != nil { + return err + } + if !p.hasMin || !p.hasMax { + return fmt.Errorf("%w: value_range 需要 params_json 提供数字 min/max", ErrInvalidRuleParams) + } + if p.Min > p.Max { + return fmt.Errorf("%w: value_range 的 min 不能大于 max", ErrInvalidRuleParams) + } + case RuleTypeConsistency: + p, err := parseRuleParams(paramsJson) + if err != nil { + return err + } + if p.RefTable == "" || p.RefColumn == "" { + return fmt.Errorf("%w: consistency 需要 params_json 提供 ref_table/ref_column", ErrInvalidRuleParams) + } + if !ValidIdentifier(p.RefTable) { + return fmt.Errorf("%w: ref_table %q", ErrInvalidIdentifier, p.RefTable) + } + if !ValidIdentifier(p.RefColumn) { + return fmt.Errorf("%w: ref_column %q", ErrInvalidIdentifier, p.RefColumn) + } + case RuleTypeFluctuation: + if _, err := parseFluctuationParams(paramsJson); err != nil { + return err + } + } + return nil +} + +// ruleNeedsColumn 该规则类型的检查 SQL 是否必须拼接目标列 +func ruleNeedsColumn(ruleType string) bool { + switch ruleType { + case RuleTypeNotNull, RuleTypeUnique, RuleTypeValueRange, RuleTypeConsistency: + return true + default: + return false + } +} + +// ParseRuleIds 解析任务配置的规则id串(逗号分隔):去空白、去重、保持顺序, +// 空串或含非法片段报错 +func ParseRuleIds(s string) ([]int64, error) { + s = strings.TrimSpace(s) + if s == "" { + return nil, ErrEmptyRuleIds + } + seen := make(map[int64]struct{}) + ids := make([]int64, 0) + for _, part := range strings.Split(s, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + id, err := strconv.ParseInt(part, 10, 64) + if err != nil || id <= 0 { + return nil, fmt.Errorf("%w: 片段 %q", ErrInvalidRuleIds, part) + } + if _, dup := seen[id]; dup { + continue + } + seen[id] = struct{}{} + ids = append(ids, id) + } + if len(ids) == 0 { + return nil, ErrEmptyRuleIds + } + return ids, nil +} + +// BuildCheckSQL 生成单条规则的检查 SQL(纯函数)。 +// 表名/列名等标识符先过白名单校验才允许拼接;数值参数一律走占位符(返回 args)。 +// db 为任务目标库(空则使用 DSN 默认库),非空时表名加 `.` 前缀。 +// 各类型 SQL: +// +// row_count → select count(*) from
+// not_null → select count(*) from
where is null or =''(实测 0 为通过) +// unique → select count(*) from (select from
group by having count(*)>1) t(实测 0 为通过) +// value_range → select count(*) from
where not between min and max(实测 0 为通过) +// consistency → select count(*) from
t left join r on t.=r. +// where r. is null(实测 0 为通过) +// fluctuation → select count(*) from
(与 baseline 比较,偏差 > ratio_pct% 不通过) +func BuildCheckSQL(ruleType, db, table, column, paramsJson string) (string, []any, error) { + target, err := QualifyTable(db, table) + if err != nil { + return "", nil, err + } + + // 需要拼列的类型先校验列名白名单 + if ruleNeedsColumn(ruleType) && !ValidIdentifier(column) { + return "", nil, fmt.Errorf("%w: target_column %q", ErrInvalidIdentifier, column) + } + + switch ruleType { + case RuleTypeRowCount: + return fmt.Sprintf("select count(*) from %s", target), nil, nil + case RuleTypeNotNull: + // null 或空串均计为脏数据 + return fmt.Sprintf("select count(*) from %s where `%s` is null or `%s` = ''", target, column, column), nil, nil + case RuleTypeUnique: + return fmt.Sprintf("select count(*) from (select `%s` from %s group by `%s` having count(*) > 1) t", + column, target, column), nil, nil + case RuleTypeValueRange: + p, err := parseRuleParams(paramsJson) + if err != nil { + return "", nil, err + } + if !p.hasMin || !p.hasMax { + return "", nil, fmt.Errorf("%w: value_range 需要 params_json 提供数字 min/max", ErrInvalidRuleParams) + } + // min/max 数值参数走占位符 + return fmt.Sprintf("select count(*) from %s where `%s` not between ? and ?", target, column), + []any{p.Min, p.Max}, nil + case RuleTypeConsistency: + p, err := parseRuleParams(paramsJson) + if err != nil { + return "", nil, err + } + refTable, err := QualifyTable(db, p.RefTable) + if err != nil { + return "", nil, fmt.Errorf("%w: ref_table %q", ErrInvalidIdentifier, p.RefTable) + } + if !ValidIdentifier(p.RefColumn) { + return "", nil, fmt.Errorf("%w: ref_column %q", ErrInvalidIdentifier, p.RefColumn) + } + // 左连接无匹配(孤儿引用)计数,t/r 为固定别名字面量 + return fmt.Sprintf("select count(*) from %s t left join %s r on t.`%s` = r.`%s` where r.`%s` is null", + target, refTable, column, p.RefColumn, p.RefColumn), nil, nil + case RuleTypeFluctuation: + // 当前实现取当前行数与基线比较,参数在判定阶段使用,这里提前校验缺参 + if _, err := parseFluctuationParams(paramsJson); err != nil { + return "", nil, err + } + return fmt.Sprintf("select count(*) from %s", target), nil, nil + default: + return "", nil, ErrInvalidRuleType + } +} + +// JudgeCheck 检查结果判定(纯函数):checkValue 为检查 SQL 实测值(命中数/行数), +// 返回是否通过与 detail_json(阈值/实测/差异对比详情)。 +// 判定口径: +// +// not_null/unique/value_range/consistency:命中脏数据 0 条即通过 +// fluctuation:|实测行数-基线|/基线 ≤ ratio_pct% 通过;基线为 0 时实测也为 0 才通过 +// row_count:threshold_json 提供阈值时按阈值比较,缺省要求表非空(>0) +func JudgeCheck(ruleType string, checkValue int64, paramsJson, thresholdJson string) (bool, string, error) { + var ( + pass bool + detail map[string]any + err error + ) + switch ruleType { + case RuleTypeRowCount: + pass, detail, err = judgeRowCount(checkValue, thresholdJson) + case RuleTypeNotNull, RuleTypeUnique, RuleTypeConsistency: + pass = checkValue == 0 + detail = map[string]any{"actual": checkValue, "expect": 0} + case RuleTypeValueRange: + pass = checkValue == 0 + detail = map[string]any{"actual": checkValue, "expect": 0} + if p, perr := parseRuleParams(paramsJson); perr == nil && p.hasMin && p.hasMax { + detail["min"] = p.Min + detail["max"] = p.Max + } + case RuleTypeFluctuation: + pass, detail, err = judgeFluctuation(checkValue, paramsJson) + default: + return false, "", ErrInvalidRuleType + } + if err != nil { + return false, "", err + } + detail["pass"] = pass + return pass, marshalDetail(detail), nil +} + +// judgeRowCount 行数判定:threshold_json 为空时要求表非空;提供时按 {op,value} 阈值比较 +func judgeRowCount(count int64, thresholdJson string) (bool, map[string]any, error) { + detail := map[string]any{"actual": count} + s := strings.TrimSpace(thresholdJson) + if s == "" { + // 缺省口径:表非空(>0)即通过 + return count > 0, detail, nil + } + var th struct { + Op string `json:"op"` + Value json.RawMessage `json:"value"` + } + if err := json.Unmarshal([]byte(s), &th); err != nil { + return false, nil, fmt.Errorf("%w: threshold_json 解析失败: %v", ErrInvalidRuleParams, err) + } + op := strings.ToLower(strings.TrimSpace(th.Op)) + values, err := parseThresholdValues(th.Value) + if err != nil { + return false, nil, err + } + detail["op"] = op + detail["value"] = values + return compareThreshold(count, op, values), detail, nil +} + +// parseThresholdValues 解析阈值 value:单个数字或 between 的 [lo, hi] 数组 +func parseThresholdValues(raw json.RawMessage) ([]float64, error) { + var single float64 + if err := json.Unmarshal(raw, &single); err == nil { + return []float64{single}, nil + } + var arr []float64 + if err := json.Unmarshal(raw, &arr); err != nil || len(arr) == 0 { + return nil, fmt.Errorf("%w: threshold_json.value 需为数字或数字数组", ErrInvalidRuleParams) + } + return arr, nil +} + +// compareThreshold 按阈值操作符比较实测行数(between 区间两端自动纠正为 lo<=hi) +func compareThreshold(count int64, op string, values []float64) bool { + if len(values) == 0 { + return false + } + v := float64(count) + switch op { + case "gt": + return v > values[0] + case "gte": + return v >= values[0] + case "lt": + return v < values[0] + case "lte": + return v <= values[0] + case "eq": + return v == values[0] + case "between": + if len(values) < 2 { + return false + } + lo, hi := values[0], values[1] + if lo > hi { + lo, hi = hi, lo + } + return v >= lo && v <= hi + default: + return false + } +} + +// judgeFluctuation 波动判定:|当前行数-基线|/基线*100 与 ratio_pct% 比较,超限不通过; +// 基线为 0 时数学上无法算偏差,约定当前行数也为 0 才通过(detail 里 deviation_pct 记 0) +func judgeFluctuation(current int64, paramsJson string) (bool, map[string]any, error) { + p, err := parseFluctuationParams(paramsJson) + if err != nil { + return false, nil, err + } + detail := map[string]any{ + "actual": current, + "baseline": p.Baseline, + "ratio_pct": p.RatioPct, + } + var deviation float64 + pass := false + switch { + case p.Baseline > 0: + deviation = math.Abs(float64(current)-p.Baseline) / p.Baseline * 100 + pass = deviation <= p.RatioPct + default: + // 基线 <= 0:无偏差可算,仅当前也为 0 时视为无波动 + pass = current == 0 + } + detail["deviation_pct"] = deviation + return pass, detail, nil +} + +// parseFluctuationParams 解析并校验 fluctuation 参数(baseline/ratio_pct 必须为数字) +func parseFluctuationParams(paramsJson string) (*ruleParams, error) { + p, err := parseRuleParams(paramsJson) + if err != nil { + return nil, err + } + if !p.hasBaseline || !p.hasRatio { + return nil, fmt.Errorf("%w: fluctuation 需要 params_json 提供数字 baseline/ratio_pct", ErrInvalidRuleParams) + } + if p.RatioPct < 0 { + return nil, fmt.Errorf("%w: fluctuation 的 ratio_pct 不能为负数", ErrInvalidRuleParams) + } + return p, nil +} + +// parseRuleParams 解析 params_json(空串视为空对象),宽容兼容 json 数值与字符串数字 +func parseRuleParams(paramsJson string) (*ruleParams, error) { + m, err := parseParamsJson(paramsJson) + if err != nil { + return nil, err + } + p := &ruleParams{} + p.Min, p.hasMin = paramNumber(m, "min") + p.Max, p.hasMax = paramNumber(m, "max") + p.RefTable, _ = paramString(m, "ref_table") + p.RefColumn, _ = paramString(m, "ref_column") + p.Baseline, p.hasBaseline = paramNumber(m, "baseline") + p.RatioPct, p.hasRatio = paramNumber(m, "ratio_pct") + return p, nil +} + +// parseParamsJson 解析 params_json 为 map(空串/空白视为空对象) +func parseParamsJson(paramsJson string) (map[string]any, error) { + s := strings.TrimSpace(paramsJson) + if s == "" { + return map[string]any{}, nil + } + var m map[string]any + if err := json.Unmarshal([]byte(s), &m); err != nil { + return nil, fmt.Errorf("%w: params_json 解析失败: %v", ErrInvalidRuleParams, err) + } + return m, nil +} + +// paramNumber 取数字参数:兼容 json 数值 / 字符串数字 / json.Number +func paramNumber(m map[string]any, key string) (float64, bool) { + v, ok := m[key] + if !ok || v == nil { + return 0, false + } + switch n := v.(type) { + case float64: + return n, true + case json.Number: + f, err := n.Float64() + return f, err == nil + case string: + f, err := strconv.ParseFloat(strings.TrimSpace(n), 64) + return f, err == nil + default: + return 0, false + } +} + +// paramString 取字符串参数 +func paramString(m map[string]any, key string) (string, bool) { + v, ok := m[key] + if !ok || v == nil { + return "", false + } + s, ok := v.(string) + return s, ok +} + +// marshalDetail 详情 map 序列化为 json 串(序列化失败降级为空串,不阻断主流程) +func marshalDetail(m map[string]any) string { + b, err := json.Marshal(m) + if err != nil { + return "" + } + return string(b) +} diff --git a/quality/rpc/internal/logic/checker_test.go b/quality/rpc/internal/logic/checker_test.go new file mode 100644 index 0000000..dadd2e8 --- /dev/null +++ b/quality/rpc/internal/logic/checker_test.go @@ -0,0 +1,340 @@ +package logic + +import ( + "errors" + "reflect" + "testing" +) + +// 测试 6 种规则的检查 SQL 生成(含库前缀与占位参数) +func TestBuildCheckSQL(t *testing.T) { + cases := []struct { + name string + ruleType string + db string + table string + column string + paramsJson string + wantSQL string + wantArgs []any + }{ + { + name: "row_count", + ruleType: RuleTypeRowCount, + table: "orders", + wantSQL: "select count(*) from orders", + }, + { + name: "not_null", + ruleType: RuleTypeNotNull, + table: "orders", + column: "user_id", + wantSQL: "select count(*) from orders where `user_id` is null or `user_id` = ''", + }, + { + name: "unique", + ruleType: RuleTypeUnique, + table: "orders", + column: "order_no", + wantSQL: "select count(*) from (select `order_no` from orders group by `order_no` having count(*) > 1) t", + }, + { + name: "value_range 带库前缀与占位参数", + ruleType: RuleTypeValueRange, + db: "mall", + table: "orders", + column: "amount", + paramsJson: `{"min":0,"max":10000}`, + wantSQL: "select count(*) from mall.orders where `amount` not between ? and ?", + wantArgs: []any{float64(0), float64(10000)}, + }, + { + name: "consistency", + ruleType: RuleTypeConsistency, + db: "mall", + table: "orders", + column: "user_id", + paramsJson: `{"ref_table":"users","ref_column":"id"}`, + wantSQL: "select count(*) from mall.orders t left join mall.users r on t.`user_id` = r.`id` where r.`id` is null", + }, + { + name: "fluctuation", + ruleType: RuleTypeFluctuation, + table: "orders", + paramsJson: `{"baseline":1000,"ratio_pct":5}`, + wantSQL: "select count(*) from orders", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + gotSQL, gotArgs, err := BuildCheckSQL(c.ruleType, c.db, c.table, c.column, c.paramsJson) + if err != nil { + t.Fatalf("BuildCheckSQL error: %v", err) + } + if gotSQL != c.wantSQL { + t.Fatalf("sql = %q, want %q", gotSQL, c.wantSQL) + } + if len(gotArgs) != len(c.wantArgs) { + t.Fatalf("args = %v, want %v", gotArgs, c.wantArgs) + } + for i := range gotArgs { + if gotArgs[i] != c.wantArgs[i] { + t.Fatalf("args[%d] = %v, want %v", i, gotArgs[i], c.wantArgs[i]) + } + } + }) + } +} + +// 测试 SQL 注入防护:表名/列名/库名未通过白名单的必须被拒 +func TestBuildCheckSQLRejectsInjection(t *testing.T) { + badTargets := []string{ + "users; drop table x", + "users;drop table x", + "users--", + "users or 1=1", + "`users`", + "users where 1=1", + "user name", // 空格 + "", // 空表名 + } + for _, target := range badTargets { + if _, _, err := BuildCheckSQL(RuleTypeRowCount, "", target, "", ""); !errors.Is(err, ErrInvalidIdentifier) { + t.Fatalf("表名 %q 应被拒绝, got err = %v", target, err) + } + } + // 列名注入 + if _, _, err := BuildCheckSQL(RuleTypeNotNull, "", "orders", "amount; drop table x", ""); !errors.Is(err, ErrInvalidIdentifier) { + t.Fatalf("列名注入应被拒绝, got err = %v", err) + } + // 库名注入(含点号与分号均不允许) + if _, _, err := BuildCheckSQL(RuleTypeRowCount, "mall; drop table x", "orders", "", ""); !errors.Is(err, ErrInvalidIdentifier) { + t.Fatalf("库名注入应被拒绝, got err = %v", err) + } + if _, _, err := BuildCheckSQL(RuleTypeRowCount, "mall.orders", "orders", "", ""); !errors.Is(err, ErrInvalidIdentifier) { + t.Fatalf("库名含点号应被拒绝(前缀由 db 参数承担), got err = %v", err) + } + // consistency 的 ref_table/ref_column 注入 + if _, _, err := BuildCheckSQL(RuleTypeConsistency, "", "orders", "user_id", `{"ref_table":"users; drop table x","ref_column":"id"}`); !errors.Is(err, ErrInvalidIdentifier) { + t.Fatalf("ref_table 注入应被拒绝, got err = %v", err) + } + if _, _, err := BuildCheckSQL(RuleTypeConsistency, "", "orders", "user_id", `{"ref_table":"users","ref_column":"id; delete from users"}`); !errors.Is(err, ErrInvalidIdentifier) { + t.Fatalf("ref_column 注入应被拒绝, got err = %v", err) + } +} + +// 测试 value_range/consistency/fluctuation 缺参数时提前报错 +func TestBuildCheckSQLRequiresParams(t *testing.T) { + cases := []struct { + name string + ruleType string + column string + paramsJson string + }{ + {"value_range 缺 min/max", RuleTypeValueRange, "amount", `{}`}, + {"value_range 参数非数字", RuleTypeValueRange, "amount", `{"min":"a","max":10}`}, + {"consistency 缺 ref", RuleTypeConsistency, "user_id", `{"ref_table":"users"}`}, + {"fluctuation 缺 baseline", RuleTypeFluctuation, "", `{"ratio_pct":5}`}, + {"not_null 缺列", RuleTypeNotNull, "", ""}, + } + for _, c := range cases { + if _, _, err := BuildCheckSQL(c.ruleType, "", "orders", c.column, c.paramsJson); err == nil { + t.Fatalf("%s 应报错", c.name) + } + } +} + +// 测试判定口径:命中数/行数的通过规则 +func TestJudgeCheck(t *testing.T) { + // not_null/unique/value_range/consistency:0 通过、非 0 不通过 + for _, ruleType := range []string{RuleTypeNotNull, RuleTypeUnique, RuleTypeValueRange, RuleTypeConsistency} { + pass, _, err := JudgeCheck(ruleType, 0, `{"min":0,"max":1}`, "") + if err != nil || !pass { + t.Fatalf("%s 实测 0 应通过, pass=%v err=%v", ruleType, pass, err) + } + pass, detail, err := JudgeCheck(ruleType, 3, `{"min":0,"max":1}`, "") + if err != nil || pass { + t.Fatalf("%s 实测 3 应不通过, pass=%v err=%v", ruleType, pass, err) + } + if detail == "" { + t.Fatalf("%s 应返回 detail_json", ruleType) + } + } +} + +// 测试 row_count 阈值判定:缺省要求表非空,提供 threshold_json 时按 op 比较 +func TestJudgeRowCountThreshold(t *testing.T) { + cases := []struct { + name string + count int64 + thresholdJson string + wantPass bool + }{ + {"缺省阈值-非空表通过", 100, "", true}, + {"缺省阈值-空表不通过", 0, "", false}, + {"gt 满足", 100, `{"op":"gt","value":99}`, true}, + {"gt 不满足", 100, `{"op":"gt","value":100}`, false}, + {"gte 满足", 100, `{"op":"gte","value":100}`, true}, + {"lt 满足", 5, `{"op":"lt","value":10}`, true}, + {"eq 不满足", 5, `{"op":"eq","value":10}`, false}, + {"between 满足", 50, `{"op":"between","value":[10,100]}`, true}, + {"between 逆序自动纠正", 50, `{"op":"between","value":[100,10]}`, true}, + {"between 越界", 101, `{"op":"between","value":[10,100]}`, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + pass, _, err := JudgeCheck(RuleTypeRowCount, c.count, "", c.thresholdJson) + if err != nil { + t.Fatalf("JudgeCheck error: %v", err) + } + if pass != c.wantPass { + t.Fatalf("count=%d pass = %v, want %v", c.count, pass, c.wantPass) + } + }) + } + // 非法阈值 op 一律不通过(不 panic) + if pass, _, _ := JudgeCheck(RuleTypeRowCount, 100, "", `{"op":"hack","value":1}`); pass { + t.Fatalf("未知 op 应不通过") + } +} + +// 测试波动率判定:偏差比例与阈值比较、基线为 0 的约定 +func TestJudgeFluctuation(t *testing.T) { + cases := []struct { + name string + current int64 + paramsJson string + wantPass bool + }{ + {"偏差 2% 在 5% 内", 1020, `{"baseline":1000,"ratio_pct":5}`, true}, + {"偏差恰好等于阈值", 1050, `{"baseline":1000,"ratio_pct":5}`, true}, + {"偏差超阈值", 1060, `{"baseline":1000,"ratio_pct":5}`, false}, + {"行数下降同样超限", 900, `{"baseline":1000,"ratio_pct":5}`, false}, + {"行数下降在限内", 980, `{"baseline":1000,"ratio_pct":5}`, true}, + {"基线为 0 当前行也为 0", 0, `{"baseline":0,"ratio_pct":5}`, true}, + {"基线为 0 当前行非 0", 10, `{"baseline":0,"ratio_pct":5}`, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + pass, detail, err := JudgeCheck(RuleTypeFluctuation, c.current, c.paramsJson, "") + if err != nil { + t.Fatalf("JudgeCheck error: %v", err) + } + if pass != c.wantPass { + t.Fatalf("current=%d pass = %v, want %v", c.current, pass, c.wantPass) + } + if detail == "" { + t.Fatalf("应返回 detail_json") + } + }) + } + // 缺参数报错 + if _, _, err := JudgeCheck(RuleTypeFluctuation, 10, `{}`, ""); !errors.Is(err, ErrInvalidRuleParams) { + t.Fatalf("缺 baseline/ratio_pct 应报参数错误, got %v", err) + } +} + +// 测试创建规则的静态校验(类型白名单 + 按类型必填性) +func TestValidateRule(t *testing.T) { + valid := []struct { + ruleType string + column string + paramsJson string + }{ + {RuleTypeRowCount, "", ""}, + {RuleTypeNotNull, "user_id", ""}, + {RuleTypeUnique, "order_no", ""}, + {RuleTypeValueRange, "amount", `{"min":0,"max":100}`}, + {RuleTypeConsistency, "user_id", `{"ref_table":"users","ref_column":"id"}`}, + {RuleTypeFluctuation, "", `{"baseline":100,"ratio_pct":10}`}, + } + for _, c := range valid { + if err := ValidateRule(c.ruleType, "orders", c.column, c.paramsJson); err != nil { + t.Fatalf("%s 合法用例不应报错, got %v", c.ruleType, err) + } + } + + invalid := []struct { + name string + ruleType string + targetTable string + column string + paramsJson string + wantErr error + }{ + {"类型不在白名单", "select", "orders", "", "", ErrInvalidRuleType}, + {"缺目标表", RuleTypeNotNull, "", "id", "", ErrTargetTableRequired}, + {"目标表注入", RuleTypeRowCount, "users; drop table x", "", "", ErrInvalidIdentifier}, + {"not_null 缺列", RuleTypeNotNull, "orders", "", "", ErrInvalidRuleParams}, + {"unique 缺列", RuleTypeUnique, "orders", "", "", ErrInvalidRuleParams}, + {"value_range 缺列", RuleTypeValueRange, "orders", "", `{"min":0,"max":1}`, ErrInvalidRuleParams}, + {"value_range 缺参数", RuleTypeValueRange, "orders", "amount", `{}`, ErrInvalidRuleParams}, + {"value_range min>max", RuleTypeValueRange, "orders", "amount", `{"min":10,"max":1}`, ErrInvalidRuleParams}, + {"consistency 缺 ref_column", RuleTypeConsistency, "orders", "user_id", `{"ref_table":"users"}`, ErrInvalidRuleParams}, + {"fluctuation 缺 ratio_pct", RuleTypeFluctuation, "orders", "", `{"baseline":100}`, ErrInvalidRuleParams}, + {"fluctuation 负阈值", RuleTypeFluctuation, "orders", "", `{"baseline":100,"ratio_pct":-1}`, ErrInvalidRuleParams}, + } + for _, c := range invalid { + err := ValidateRule(c.ruleType, c.targetTable, c.column, c.paramsJson) + if err == nil { + t.Fatalf("%s 应报错", c.name) + } + if !errors.Is(err, c.wantErr) { + t.Fatalf("%s 错误不匹配: got %v, want %v", c.name, err, c.wantErr) + } + } +} + +// 测试规则 id 串解析:容错空白、去重保序、拒非法片段 +func TestParseRuleIds(t *testing.T) { + got, err := ParseRuleIds(" 3, 1 ,2,1 ") + if err != nil { + t.Fatalf("ParseRuleIds error: %v", err) + } + if !reflect.DeepEqual(got, []int64{3, 1, 2}) { + t.Fatalf("ids = %v, want [3 1 2]", got) + } + if _, err := ParseRuleIds(""); !errors.Is(err, ErrEmptyRuleIds) { + t.Fatalf("空串应报 ErrEmptyRuleIds, got %v", err) + } + if _, err := ParseRuleIds(",,"); !errors.Is(err, ErrEmptyRuleIds) { + t.Fatalf("纯分隔符应报 ErrEmptyRuleIds, got %v", err) + } + if _, err := ParseRuleIds("1,a,3"); !errors.Is(err, ErrInvalidRuleIds) { + t.Fatalf("非法片段应报 ErrInvalidRuleIds, got %v", err) + } + if _, err := ParseRuleIds("1,0,-2"); !errors.Is(err, ErrInvalidRuleIds) { + t.Fatalf("非正整数应报 ErrInvalidRuleIds, got %v", err) + } +} + +// 测试标识符白名单正则的边界 +func TestValidIdentifier(t *testing.T) { + valid := []string{"users", "t_1", "_tmp", "OrderItems", "a"} + for _, s := range valid { + if !ValidIdentifier(s) { + t.Fatalf("%q 应为合法标识符", s) + } + } + invalid := []string{"", "users; drop table x", "a b", "a-b", "a.b", "`a`", "'a'", "a*", "中文名"} + for _, s := range invalid { + if ValidIdentifier(s) { + t.Fatalf("%q 应为非法标识符", s) + } + } +} + +// 测试库前缀生成 +func TestQualifyTable(t *testing.T) { + got, err := QualifyTable("mall", "orders") + if err != nil || got != "mall.orders" { + t.Fatalf("QualifyTable = %q, %v", got, err) + } + got, err = QualifyTable("", "orders") + if err != nil || got != "orders" { + t.Fatalf("空库名应返回裸表名, got %q, %v", got, err) + } + if _, err := QualifyTable("mall", "orders; drop table x"); !errors.Is(err, ErrInvalidIdentifier) { + t.Fatalf("表名注入应被拒绝, got %v", err) + } +} diff --git a/quality/rpc/internal/logic/convert.go b/quality/rpc/internal/logic/convert.go new file mode 100644 index 0000000..0398a10 --- /dev/null +++ b/quality/rpc/internal/logic/convert.go @@ -0,0 +1,51 @@ +package logic + +import ( + "datacenter/quality/model" + "datacenter/quality/rpc/quality" +) + +// ruleToPb 规则 model -> pb(status 为 int32 收窄转换) +func ruleToPb(r *model.Rule) *quality.QualityRule { + return &quality.QualityRule{ + Id: r.Id, + Name: r.Name, + RuleType: r.RuleType, + TargetTable: r.TargetTable, + TargetColumn: r.TargetColumn, + ParamsJson: r.ParamsJson, + ThresholdJson: r.ThresholdJson, + Status: int32(r.Status), + Owner: r.Owner, + Beid: r.Beid, + } +} + +// jobToPb 任务 model -> pb(last_run_at 为毫秒时间戳,未运行为 0) +func jobToPb(j *model.Job) *quality.QualityJob { + return &quality.QualityJob{ + Id: j.Id, + Name: j.Name, + DatasourceId: j.DatasourceId, + DbName: j.DbName, + RuleIds: j.RuleIds, + Cron: j.Cron, + LastStatus: j.LastStatus, + LastRunAt: j.LastRunAt, + Beid: j.Beid, + } +} + +// resultToPb 结果 model -> pb(pass_flag tinyint -> bool,run_at 为毫秒时间戳) +func resultToPb(r *model.Result) *quality.QualityResult { + return &quality.QualityResult{ + Id: r.Id, + JobId: r.JobId, + RuleId: r.RuleId, + RuleType: r.RuleType, + CheckValue: r.CheckValue, + PassFlag: r.PassFlag == 1, + DetailJson: r.DetailJson, + RunAt: r.RunAt, + } +} diff --git a/quality/rpc/internal/logic/createJobLogic.go b/quality/rpc/internal/logic/createJobLogic.go index 903d232..1992e2a 100644 --- a/quality/rpc/internal/logic/createJobLogic.go +++ b/quality/rpc/internal/logic/createJobLogic.go @@ -2,7 +2,10 @@ package logic import ( "context" + "fmt" + "strings" + "datacenter/quality/model" "datacenter/quality/rpc/internal/svc" "datacenter/quality/rpc/quality" @@ -23,9 +26,61 @@ func NewCreateJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateJ } } -// 创建任务 +// 创建任务:校验 rule_ids 非空且每条规则存在(同 beid 租户内) func (l *CreateJobLogic) CreateJob(in *quality.CreateJobReq) (*quality.JobReply, error) { - // todo: add your logic here and delete this line + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + if strings.TrimSpace(in.Name) == "" { + return nil, ErrJobNameRequired + } + if in.DatasourceId <= 0 { + return nil, ErrDatasourceRequired + } + ruleIds, err := ParseRuleIds(in.RuleIds) + if err != nil { + return nil, err + } + // 规则必须存在且属于同租户(FindListByIds 内部按 beid 过滤) + if err := l.checkRulesExist(in.Beid, ruleIds); err != nil { + return nil, err + } + id, err := l.svcCtx.JobModel.Insert(l.ctx, &model.Job{ + Name: in.Name, + DatasourceId: in.DatasourceId, + DbName: in.DbName, + RuleIds: in.RuleIds, + Cron: in.Cron, + Beid: in.Beid, + }) + if err != nil { + return nil, err + } + job, err := l.svcCtx.JobModel.FindOne(l.ctx, id, in.Beid) + if err != nil { + return nil, err + } + return &quality.JobReply{Job: jobToPb(job)}, nil +} - return &quality.JobReply{}, nil +// checkRulesExist 校验规则 id 全部存在,缺失时返回缺失明细 +func (l *CreateJobLogic) checkRulesExist(beid int64, ruleIds []int64) error { + rules, err := l.svcCtx.RuleModel.FindListByIds(l.ctx, beid, ruleIds) + if err != nil { + return err + } + found := make(map[int64]struct{}, len(rules)) + for _, r := range rules { + found[r.Id] = struct{}{} + } + var missing []int64 + for _, id := range ruleIds { + if _, ok := found[id]; !ok { + missing = append(missing, id) + } + } + if len(missing) > 0 { + return fmt.Errorf("%w: 缺失id %v", ErrRuleNotFound, missing) + } + return nil } diff --git a/quality/rpc/internal/logic/createRuleLogic.go b/quality/rpc/internal/logic/createRuleLogic.go index af009c9..3ea025b 100644 --- a/quality/rpc/internal/logic/createRuleLogic.go +++ b/quality/rpc/internal/logic/createRuleLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "strings" + "datacenter/quality/model" "datacenter/quality/rpc/internal/svc" "datacenter/quality/rpc/quality" @@ -25,7 +27,33 @@ func NewCreateRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Create // 创建规则 func (l *CreateRuleLogic) CreateRule(in *quality.CreateRuleReq) (*quality.RuleReply, error) { - // todo: add your logic here and delete this line - - return &quality.RuleReply{}, nil + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + if strings.TrimSpace(in.Name) == "" { + return nil, ErrRuleNameRequired + } + // 类型白名单 + 标识符白名单 + 按类型的 target_column/params_json 必填校验 + if err := ValidateRule(in.RuleType, in.TargetTable, in.TargetColumn, in.ParamsJson); err != nil { + return nil, err + } + id, err := l.svcCtx.RuleModel.Insert(l.ctx, &model.Rule{ + Name: in.Name, + RuleType: in.RuleType, + TargetTable: in.TargetTable, + TargetColumn: in.TargetColumn, + ParamsJson: in.ParamsJson, + ThresholdJson: in.ThresholdJson, + Status: model.RuleStatusEnabled, + Owner: in.Owner, + Beid: in.Beid, + }) + if err != nil { + return nil, err + } + rule, err := l.svcCtx.RuleModel.FindOne(l.ctx, id, in.Beid) + if err != nil { + return nil, err + } + return &quality.RuleReply{Rule: ruleToPb(rule)}, nil } diff --git a/quality/rpc/internal/logic/deleteRuleLogic.go b/quality/rpc/internal/logic/deleteRuleLogic.go index 25718cb..4801d32 100644 --- a/quality/rpc/internal/logic/deleteRuleLogic.go +++ b/quality/rpc/internal/logic/deleteRuleLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/quality/model" "datacenter/quality/rpc/internal/svc" "datacenter/quality/rpc/quality" @@ -23,9 +24,16 @@ func NewDeleteRuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delete } } -// 删除规则 +// 删除规则(幂等:规则不存在时不报错) func (l *DeleteRuleLogic) DeleteRule(in *quality.IdReq) (*quality.IdReq, error) { - // todo: add your logic here and delete this line - - return &quality.IdReq{}, nil + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + if in.Id <= 0 { + return nil, ErrInvalidRuleParams + } + if err := l.svcCtx.RuleModel.Delete(l.ctx, in.Id, in.Beid); err != nil { + return nil, err + } + return &quality.IdReq{Id: in.Id, Beid: in.Beid}, nil } diff --git a/quality/rpc/internal/logic/error.go b/quality/rpc/internal/logic/error.go new file mode 100644 index 0000000..b66e4eb --- /dev/null +++ b/quality/rpc/internal/logic/error.go @@ -0,0 +1,29 @@ +package logic + +import "errors" + +// 业务错误(对齐 metadata 服务写法:errors.New 中文语义错误,调用方用 errors.Is 判断) +var ( + // ErrInvalidRuleType 规则类型不在白名单 + ErrInvalidRuleType = errors.New("rule_type 非法:仅支持 row_count/not_null/unique/value_range/consistency/fluctuation") + // ErrInvalidRuleParams 规则参数缺失或非法(按 rule_type 校验 target_column/params_json 必填性) + ErrInvalidRuleParams = errors.New("规则参数不完整或非法") + // ErrInvalidIdentifier 表名/列名/库名未通过白名单校验(防 SQL 注入) + ErrInvalidIdentifier = errors.New("表名/列名仅允许字母、数字、下划线") + // ErrRuleNameRequired 规则名缺失 + ErrRuleNameRequired = errors.New("规则名不能为空") + // ErrTargetTableRequired 目标表缺失 + ErrTargetTableRequired = errors.New("目标表不能为空") + // ErrRuleNotFound 规则不存在(含跨租户不可见) + ErrRuleNotFound = errors.New("规则不存在") + // ErrJobNameRequired 任务名缺失 + ErrJobNameRequired = errors.New("任务名不能为空") + // ErrDatasourceRequired 数据源缺失 + ErrDatasourceRequired = errors.New("datasource_id 不能为空") + // ErrEmptyRuleIds 任务未配置规则 + ErrEmptyRuleIds = errors.New("任务未配置质量规则:rule_ids 不能为空") + // ErrInvalidRuleIds rule_ids 非法(非逗号分隔的正整数) + ErrInvalidRuleIds = errors.New("rule_ids 非法:需为逗号分隔的正整数id") + // ErrDataSourceDsn 目标数据源连接串不可用 + ErrDataSourceDsn = errors.New("目标数据源连接串不可用") +) diff --git a/quality/rpc/internal/logic/listJobsLogic.go b/quality/rpc/internal/logic/listJobsLogic.go index 7a4402d..3a37cae 100644 --- a/quality/rpc/internal/logic/listJobsLogic.go +++ b/quality/rpc/internal/logic/listJobsLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/quality/model" "datacenter/quality/rpc/internal/svc" "datacenter/quality/rpc/quality" @@ -23,9 +24,19 @@ func NewListJobsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListJobs } } -// 任务列表 +// 任务列表(分页) func (l *ListJobsLogic) ListJobs(in *quality.JobListReq) (*quality.JobListReply, error) { - // todo: add your logic here and delete this line - - return &quality.JobListReply{}, nil + // 多租户条件必填 + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + list, total, err := l.svcCtx.JobModel.FindList(l.ctx, in.Beid, in.Page, in.Size) + if err != nil { + return nil, err + } + jobs := make([]*quality.QualityJob, 0, len(list)) + for _, j := range list { + jobs = append(jobs, jobToPb(j)) + } + return &quality.JobListReply{List: jobs, Total: total}, nil } diff --git a/quality/rpc/internal/logic/listResultsLogic.go b/quality/rpc/internal/logic/listResultsLogic.go index cf12f69..ffb653f 100644 --- a/quality/rpc/internal/logic/listResultsLogic.go +++ b/quality/rpc/internal/logic/listResultsLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/quality/model" "datacenter/quality/rpc/internal/svc" "datacenter/quality/rpc/quality" @@ -23,9 +24,18 @@ func NewListResultsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListR } } -// 结果列表 +// 结果列表(job_id + 可选 pass_filter 分页;pass_filter 0.全部 1.仅通过 2.仅不通过) func (l *ListResultsLogic) ListResults(in *quality.ResultListReq) (*quality.ResultListReply, error) { - // todo: add your logic here and delete this line - - return &quality.ResultListReply{}, nil + if in.JobId <= 0 { + return nil, model.ErrJobIdRequired + } + list, total, err := l.svcCtx.ResultModel.FindListByJob(l.ctx, in.JobId, in.PassFilter, in.Page, in.Size) + if err != nil { + return nil, err + } + results := make([]*quality.QualityResult, 0, len(list)) + for _, r := range list { + results = append(results, resultToPb(r)) + } + return &quality.ResultListReply{List: results, Total: total}, nil } diff --git a/quality/rpc/internal/logic/listRulesLogic.go b/quality/rpc/internal/logic/listRulesLogic.go index 8416e98..2634b62 100644 --- a/quality/rpc/internal/logic/listRulesLogic.go +++ b/quality/rpc/internal/logic/listRulesLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/quality/model" "datacenter/quality/rpc/internal/svc" "datacenter/quality/rpc/quality" @@ -23,9 +24,19 @@ func NewListRulesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListRul } } -// 规则列表 +// 规则列表(分页 + rule_type 可选过滤) func (l *ListRulesLogic) ListRules(in *quality.RuleListReq) (*quality.RuleListReply, error) { - // todo: add your logic here and delete this line - - return &quality.RuleListReply{}, nil + // 多租户条件必填 + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + list, total, err := l.svcCtx.RuleModel.FindList(l.ctx, in.Beid, in.RuleType, in.Page, in.Size) + if err != nil { + return nil, err + } + rules := make([]*quality.QualityRule, 0, len(list)) + for _, r := range list { + rules = append(rules, ruleToPb(r)) + } + return &quality.RuleListReply{List: rules, Total: total}, nil } diff --git a/quality/rpc/internal/logic/runJobLogic.go b/quality/rpc/internal/logic/runJobLogic.go index 63adab5..79388f1 100644 --- a/quality/rpc/internal/logic/runJobLogic.go +++ b/quality/rpc/internal/logic/runJobLogic.go @@ -2,11 +2,17 @@ package logic import ( "context" + "fmt" + "strconv" + "time" + "datacenter/datasource/rpc/datasource" + "datacenter/quality/model" "datacenter/quality/rpc/internal/svc" "datacenter/quality/rpc/quality" "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type RunJobLogic struct { @@ -23,9 +29,149 @@ func NewRunJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RunJobLogi } } -// 执行任务(同步跑完规则并落结果) +// 执行任务(同步跑完规则并落结果): +// 查任务 → 解析 rule_ids → 按数据源服务拿 DSN 建目标库连接 → +// 逐规则生成检查 SQL 实测 → 结果批量落库 → 回填任务最近状态 func (l *RunJobLogic) RunJob(in *quality.IdReq) (*quality.RunJobReply, error) { - // todo: add your logic here and delete this line + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + if in.Id <= 0 { + return nil, model.ErrJobIdRequired + } + job, err := l.svcCtx.JobModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + return nil, err + } + // 解析规则id(去重、保序) + ruleIds, err := ParseRuleIds(job.RuleIds) + if err != nil { + return nil, err + } + // 加载规则,任一规则缺失(已删/跨租户)即中止 + rules, err := l.loadRulesInOrder(in.Beid, ruleIds) + if err != nil { + return nil, err + } + // 库名参与表名拼接,先过白名单 + if job.DbName != "" && !ValidIdentifier(job.DbName) { + return nil, fmt.Errorf("%w: db_name %q", ErrInvalidIdentifier, job.DbName) + } + // 目标库连接:job.datasource_id → 数据源服务取 DSN + conn, err := l.targetConn(job.DatasourceId, in.Beid) + if err != nil { + return nil, err + } + + // 逐规则执行:单规则失败不阻断整体,错误落入结果详情(整体状态按通过数收敛) + runAt := time.Now() + results := make([]*model.Result, 0, len(rules)) + var passed int64 + for _, rule := range rules { + result := l.execRule(conn, job.Id, job.DbName, rule, runAt) + passed += result.PassFlag // PassFlag 为 1 时恰好累加 1 + results = append(results, result) + } + if err := l.svcCtx.ResultModel.BatchInsert(l.ctx, results); err != nil { + return nil, err + } + + // 全部通过 success、全部不通过 failed、部分通过 partial + status := model.JobStatusPartial + switch { + case passed == int64(len(rules)): + status = model.JobStatusSuccess + case passed == 0: + status = model.JobStatusFailed + } + if err := l.svcCtx.JobModel.UpdateRunStatus(l.ctx, job.Id, in.Beid, status, runAt); err != nil { + return nil, err + } + return &quality.RunJobReply{ + JobId: job.Id, + TotalRules: int64(len(rules)), + Passed: passed, + LastStatus: status, + }, nil +} + +// loadRulesInOrder 按任务配置的 rule_ids 顺序加载规则 +func (l *RunJobLogic) loadRulesInOrder(beid int64, ruleIds []int64) ([]*model.Rule, error) { + rules, err := l.svcCtx.RuleModel.FindListByIds(l.ctx, beid, ruleIds) + if err != nil { + return nil, err + } + byId := make(map[int64]*model.Rule, len(rules)) + for _, r := range rules { + byId[r.Id] = r + } + ordered := make([]*model.Rule, 0, len(ruleIds)) + var missing []int64 + for _, id := range ruleIds { + r, ok := byId[id] + if !ok { + missing = append(missing, id) + continue + } + ordered = append(ordered, r) + } + if len(missing) > 0 { + return nil, fmt.Errorf("%w: 缺失id %v", ErrRuleNotFound, missing) + } + return ordered, nil +} - return &quality.RunJobReply{}, nil +// targetConn 从数据源服务获取 DSN 并建立目标库连接(内部调用,with_secret=true 才返回连接串) +func (l *RunJobLogic) targetConn(datasourceId, beid int64) (sqlx.SqlConn, error) { + reply, err := l.svcCtx.DataSourceCli.GetDataSource(l.ctx, &datasource.GetDataSourceReq{ + Id: datasourceId, + Beid: beid, + WithSecret: true, + }) + if err != nil { + return nil, fmt.Errorf("获取数据源失败: %w", err) + } + if reply == nil || reply.Datasource == nil || reply.Dsn == "" { + return nil, ErrDataSourceDsn + } + if reply.Datasource.Type != "mysql" { + return nil, fmt.Errorf("%w: 质量检查仅支持 mysql 数据源,当前 %q", ErrDataSourceDsn, reply.Datasource.Type) + } + // DSN 已含库名;job.db_name 非空时由 BuildCheckSQL 在表名前加 `.` 前缀 + return sqlx.NewMysql(reply.Dsn), nil +} + +// execRule 执行单条规则:生成检查 SQL → 目标库实测 → 阈值判定。 +// 任一环节失败均记为不通过并把原因写入 detail_json,不阻断其余规则 +func (l *RunJobLogic) execRule(conn sqlx.SqlConn, jobId int64, dbName string, rule *model.Rule, runAt time.Time) *model.Result { + result := &model.Result{ + JobId: jobId, + RuleId: rule.Id, + RuleType: rule.RuleType, + RunAt: runAt.UnixMilli(), + } + // 生成检查 SQL(标识符白名单校验 + 数值参数占位符化) + query, args, err := BuildCheckSQL(rule.RuleType, dbName, rule.TargetTable, rule.TargetColumn, rule.ParamsJson) + if err != nil { + result.DetailJson = marshalDetail(map[string]any{"error": err.Error()}) + return result + } + // 目标库实测 + var checkValue int64 + if err := conn.QueryRowCtx(l.ctx, &checkValue, query, args...); err != nil { + result.DetailJson = marshalDetail(map[string]any{"error": err.Error(), "sql": query}) + return result + } + result.CheckValue = strconv.FormatInt(checkValue, 10) + // 阈值判定 + pass, detail, err := JudgeCheck(rule.RuleType, checkValue, rule.ParamsJson, rule.ThresholdJson) + if err != nil { + result.DetailJson = marshalDetail(map[string]any{"error": err.Error()}) + return result + } + if pass { + result.PassFlag = 1 + } + result.DetailJson = detail + return result } diff --git a/quality/rpc/internal/svc/serviceContext.go b/quality/rpc/internal/svc/serviceContext.go index 4462f7c..1d2bacc 100644 --- a/quality/rpc/internal/svc/serviceContext.go +++ b/quality/rpc/internal/svc/serviceContext.go @@ -1,13 +1,36 @@ package svc -import "datacenter/quality/rpc/internal/config" +import ( + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/quality/model" + "datacenter/quality/rpc/internal/config" + + "github.com/zeromicro/go-zero/core/discov" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/zrpc" +) type ServiceContext struct { - Config config.Config + Config config.Config + Conn sqlx.SqlConn // 中台主库连接(质量规则/任务/结果三表) + RuleModel model.RuleModel + JobModel model.JobModel + ResultModel model.ResultModel + DataSourceCli dataSourceClient.DataSource // 数据源服务客户端(RunJob 取目标库 DSN) } func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ - Config: c, + Config: c, + Conn: conn, + RuleModel: model.NewRuleModel(conn), + JobModel: model.NewJobModel(conn), + ResultModel: model.NewResultModel(conn), + // 数据源服务走 etcd 发现(key 固定 datasource.rpc);NonBlock 启动不阻塞,首次调用才建连 + DataSourceCli: dataSourceClient.NewDataSource(zrpc.MustNewClient(zrpc.RpcClientConf{ + Etcd: discov.EtcdConf{Hosts: c.Etcd.Hosts, Key: "datasource.rpc"}, + NonBlock: true, + })), } } diff --git a/restart.sh b/restart.sh index 71f4243..5456c6c 100644 --- a/restart.sh +++ b/restart.sh @@ -59,6 +59,12 @@ RpcServerPlus ${getewayPath} metadata RpcServerPlus ${getewayPath} datasvc #资产目录服务(Phase 1) RpcServerPlus ${getewayPath} catalog +#数据质量服务(Phase 2) +RpcServerPlus ${getewayPath} quality +#数据指标服务(Phase 2) +RpcServerPlus ${getewayPath} metric +#数据标签服务(Phase 2) +RpcServerPlus ${getewayPath} tag #问答抽奖服务 RpcServerPlus ${getewayPath} questions #Api服务 diff --git a/tag.api b/tag.api new file mode 100644 index 0000000..80daa5a --- /dev/null +++ b/tag.api @@ -0,0 +1,138 @@ +syntax = "v1" + +//数据标签(Phase 2) +type TagObject { + Id int64 `json:"id,optional"` + ObjectType string `json:"object_type,optional"` + Name string `json:"name,optional"` + DsId int64 `json:"ds_id,optional"` + DbName string `json:"db_name,optional"` + TableName string `json:"table_name,optional"` + PkColumn string `json:"pk_column,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateTagObjectReq { + ObjectType string `json:"object_type"` + Name string `json:"name"` + DsId int64 `json:"ds_id,optional"` + DbName string `json:"db_name,optional"` + TableName string `json:"table_name,optional"` + PkColumn string `json:"pk_column,optional"` + Beid int64 `json:"beid,optional"` +} + +type TagObjectListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + ObjectType string `json:"object_type,optional" form:"object_type,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type TagObjectListResp { + List []TagObject `json:"list"` + Total int64 `json:"total"` +} + +type TagDef { + Id int64 `json:"id,optional"` + ObjectId int64 `json:"object_id,optional"` + TagKey string `json:"tag_key,optional"` + TagName string `json:"tag_name,optional"` + TagType string `json:"tag_type,optional"` + RuleSql string `json:"rule_sql,optional"` + RefreshCron string `json:"refresh_cron,optional"` + Status int32 `json:"status,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateTagDefReq { + ObjectId int64 `json:"object_id"` + TagKey string `json:"tag_key"` + TagName string `json:"tag_name"` + TagType string `json:"tag_type,optional"` + RuleSql string `json:"rule_sql,optional"` + RefreshCron string `json:"refresh_cron,optional"` + Beid int64 `json:"beid,optional"` +} + +type TagDefIdReq { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type TagDefListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + ObjectId int64 `json:"object_id,optional" form:"object_id,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type TagDefListResp { + List []TagDef `json:"list"` + Total int64 `json:"total"` +} + +type TaggingReq { + TagId int64 `json:"tag_id,optional"` + ObjectId int64 `json:"object_id,optional"` + Beid int64 `json:"beid,optional"` +} +type TaggingResp { + TaggedRows int64 `json:"tagged_rows"` + TagId int64 `json:"tag_id"` +} + +type ObjectTagsReq { + ObjectPk string `json:"object_pk,optional" form:"object_pk,optional"` + ObjectId int64 `json:"object_id,optional" form:"object_id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} +type TagValueItem { + TagKey string `json:"tag_key,optional"` + TagName string `json:"tag_name,optional"` + TagValue string `json:"tag_value,optional"` + TaggedAt int64 `json:"tagged_at,optional"` +} +type ObjectTagsResp { + Tags []TagValueItem `json:"tags"` +} + +@server( + group: tag +) +service datacenter-api { + @doc( + summary: "创建标签对象" + ) + @handler createTagObject + post /tag/object/create (CreateTagObjectReq) returns (TagObject) + @doc( + summary: "标签对象列表" + ) + @handler tagObjectList + get /tag/object/list (TagObjectListReq) returns (TagObjectListResp) + @doc( + summary: "创建标签定义" + ) + @handler createTagDef + post /tag/def/create (CreateTagDefReq) returns (TagDef) + @doc( + summary: "标签定义列表" + ) + @handler tagDefList + get /tag/def/list (TagDefListReq) returns (TagDefListResp) + @doc( + summary: "删除标签定义" + ) + @handler deleteTagDef + post /tag/def/delete (TagDefIdReq) + @doc( + summary: "执行打标" + ) + @handler tagging + post /tag/tagging (TaggingReq) returns (TaggingResp) + @doc( + summary: "对象标签画像" + ) + @handler objectTags + get /tag/object/tags (ObjectTagsReq) returns (ObjectTagsResp) +} diff --git a/tag/model/objectmodel.go b/tag/model/objectmodel.go new file mode 100644 index 0000000..2179d11 --- /dev/null +++ b/tag/model/objectmodel.go @@ -0,0 +1,160 @@ +package model + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + // 注意坑:builder.RawFieldNames 返回的是带反引号的列名(如 "`id`"), + // stringx.Remove 剔除自增/时间列时必须用 "`id`" 形式,裸写 "id" 剔不掉 + objectFieldNames = builder.RawFieldNames(&TagObject{}) + objectRows = strings.Join(objectFieldNames, ",") + objectRowsExpectAutoSet = strings.Join(stringx.Remove(objectFieldNames, "`id`", "`created_at`"), ",") + objectRowsWithPlaceHolder = strings.Join(stringx.Remove(objectFieldNames, "`id`", "`created_at`"), "=?,") + "=?" +) + +const ( + objectTable = "`dc_tag_object`" + objectInsertFieldSize = 7 // 插入字段数(与 objectRowsExpectAutoSet 对应) + objectDefaultPageSize = 10 + objectMaxPageSize = 200 +) + +type ( + // TagObject dc_tag_object 行(标签对象:打标目标业务主表) + TagObject struct { + Id int64 `db:"id"` + ObjectType string `db:"object_type"` //user/member/goods + Name string `db:"name"` + DsId int64 `db:"ds_id"` //对象主数据所在数据源 + DbName string `db:"db_name"` //对象库 + TableName string `db:"table_name"` //对象主表 + PkColumn string `db:"pk_column"` //主键列 + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + } + + ObjectModel interface { + // Insert 新增标签对象,返回自增 id + Insert(ctx context.Context, data *TagObject) (int64, error) + // FindOne 按主键查询(beid>0 时追加多租户过滤) + FindOne(ctx context.Context, id, beid int64) (*TagObject, error) + // Update 全量更新(不含自增 id 与创建时间) + Update(ctx context.Context, data *TagObject) error + // Delete 按主键删除(仅删对象行,tag_def/tag_value 由业务侧另行处理) + Delete(ctx context.Context, id, beid int64) error + // FindList 条件分页(beid 必带,objectType 可选),返回列表与总数 + FindList(ctx context.Context, beid int64, objectType string, page, size int64) ([]*TagObject, int64, error) + } + + defaultObjectModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewObjectModel 创建标签对象 model +func NewObjectModel(conn sqlx.SqlConn) ObjectModel { + return &defaultObjectModel{ + conn: conn, + table: objectTable, + } +} + +// Insert 新增标签对象,created_at 由库默认值填充 +func (m *defaultObjectModel) Insert(ctx context.Context, data *TagObject) (int64, error) { + query := fmt.Sprintf("insert into %s (%s) values (%s)", m.table, objectRowsExpectAutoSet, placeholders(objectInsertFieldSize)) + ret, err := m.conn.ExecCtx(ctx, query, data.ObjectType, data.Name, data.DsId, data.DbName, + data.TableName, data.PkColumn, data.Beid) + if err != nil { + return 0, err + } + id, err := ret.LastInsertId() + if err != nil { + return 0, err + } + return id, nil +} + +// FindOne 按主键查询标签对象(查询列 objectRows 9 列与 TagObject 9 字段严格对齐, +// 列数与字段数不一致时 go-zero 报 not matching destination to scan) +func (m *defaultObjectModel) FindOne(ctx context.Context, id, beid int64) (*TagObject, error) { + query := fmt.Sprintf("select %s from %s where `id` = ?", objectRows, m.table) + args := []any{id} + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var resp TagObject + err := m.conn.QueryRowCtx(ctx, &resp, query+" limit 1", args...) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// Update 全量更新标签对象(带多租户过滤) +func (m *defaultObjectModel) Update(ctx context.Context, data *TagObject) error { + query := fmt.Sprintf("update %s set %s where `id` = ? and `beid` = ?", m.table, objectRowsWithPlaceHolder) + _, err := m.conn.ExecCtx(ctx, query, data.ObjectType, data.Name, data.DsId, data.DbName, + data.TableName, data.PkColumn, data.Beid, data.Id, data.Beid) + return err +} + +// Delete 按主键删除标签对象 +func (m *defaultObjectModel) Delete(ctx context.Context, id, beid int64) error { + query := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, id, beid) + return err +} + +// FindList 条件分页查询标签对象,返回列表与总数 +func (m *defaultObjectModel) FindList(ctx context.Context, beid int64, objectType string, + page, size int64) ([]*TagObject, int64, error) { + if beid <= 0 { + return nil, 0, ErrBeidRequired + } + where := "`beid` = ?" + args := []any{beid} + if objectType != "" { + where += " and `object_type` = ?" + args = append(args, objectType) + } + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页参数防御 + if page <= 0 { + page = 1 + } + if size <= 0 { + size = objectDefaultPageSize + } + if size > objectMaxPageSize { + size = objectMaxPageSize + } + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ? offset ?", objectRows, m.table, where) + args = append(args, size, (page-1)*size) + var list []*TagObject + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} diff --git a/tag/model/tagdefmodel.go b/tag/model/tagdefmodel.go new file mode 100644 index 0000000..decbf72 --- /dev/null +++ b/tag/model/tagdefmodel.go @@ -0,0 +1,204 @@ +package model + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/builder" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/core/stringx" +) + +var ( + // 注意坑:builder.RawFieldNames 返回带反引号列名,stringx.Remove 必须用 "`id`" 形式 + tagDefFieldNames = builder.RawFieldNames(&TagDef{}) + tagDefRowsExpectAutoSet = strings.Join(stringx.Remove(tagDefFieldNames, "`id`", "`created_at`", "`updated_at`"), ",") + tagDefRowsWithPlaceHolder = strings.Join(stringx.Remove(tagDefFieldNames, "`id`", "`created_at`", "`updated_at`"), "=?,") + "=?" +) + +const ( + tagDefTable = "`dc_tag_def`" + tagDefInsertFieldSize = 8 // 插入字段数(与 tagDefRowsExpectAutoSet 对应) + tagDefDefaultPageSize = 10 + tagDefMaxPageSize = 200 +) + +// tagDefRows 查询列(rule_sql 为 text 可空,用 ifnull 兜底避免扫描报错); +// 列顺序与 TagDef 字段顺序一致(11 列对 11 字段,列数不一致扫宽 struct 会报错) +const tagDefRows = "`id`,`object_id`,`tag_key`,`tag_name`,`tag_type`,ifnull(`rule_sql`,'') as `rule_sql`,`refresh_cron`,`status`,`beid`,`created_at`,`updated_at`" + +type ( + // TagDef dc_tag_def 行(标签定义) + TagDef struct { + Id int64 `db:"id"` + ObjectId int64 `db:"object_id"` + TagKey string `db:"tag_key"` + TagName string `db:"tag_name"` + TagType string `db:"tag_type"` //rule规则/fact事实/model模型 + RuleSql string `db:"rule_sql"` //select as object_id, as tag_value from ... + RefreshCron string `db:"refresh_cron"` + Status int32 `db:"status"` //1.启用 0.停用 + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + TagDefModel interface { + // Create 新增标签定义(object_id+tag_key+beid 唯一查重),返回自增 id + Create(ctx context.Context, data *TagDef) (int64, error) + // FindOne 按主键查询(beid>0 时追加多租户过滤) + FindOne(ctx context.Context, id, beid int64) (*TagDef, error) + // FindList 分页列表(beid 必带,objectId 可选),返回列表与总数 + FindList(ctx context.Context, beid, objectId, page, size int64) ([]*TagDef, int64, error) + // FindEnabledRuleByObject 对象下全部启用中(status=1)的 rule 标签(批量打标用) + FindEnabledRuleByObject(ctx context.Context, objectId, beid int64) ([]*TagDef, error) + // Update 全量更新(不含自增 id 与时间列) + Update(ctx context.Context, data *TagDef) error + // Delete 删除定义并级联删除其全部标签值(同一事务内先删值后删定义) + Delete(ctx context.Context, id, beid int64) error + } + + defaultTagDefModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewTagDefModel 创建标签定义 model +func NewTagDefModel(conn sqlx.SqlConn) TagDefModel { + return &defaultTagDefModel{ + conn: conn, + table: tagDefTable, + } +} + +// Create 新增标签定义:先按 object_id+tag_key+beid 查重,命中即拒绝; +// 并发窗口撞 uk_obj_key 唯一键时按重复键错误统一处理 +func (m *defaultTagDefModel) Create(ctx context.Context, data *TagDef) (int64, error) { + dupQuery := fmt.Sprintf("select `id` from %s where `object_id` = ? and `tag_key` = ? and `beid` = ? limit 1", m.table) + var existId int64 + err := m.conn.QueryRowCtx(ctx, &existId, dupQuery, data.ObjectId, data.TagKey, data.Beid) + switch { + case err == nil: + return 0, ErrDupTagKey + case !errors.Is(err, sqlx.ErrNotFound): + return 0, err + } + + query := fmt.Sprintf("insert into %s (%s) values (%s)", m.table, tagDefRowsExpectAutoSet, placeholders(tagDefInsertFieldSize)) + ret, err := m.conn.ExecCtx(ctx, query, data.ObjectId, data.TagKey, data.TagName, data.TagType, + data.RuleSql, data.RefreshCron, data.Status, data.Beid) + if err != nil { + if isDuplicateEntry(err) { + return 0, ErrDupTagKey + } + return 0, err + } + id, err := ret.LastInsertId() + if err != nil { + return 0, err + } + return id, nil +} + +// FindOne 按主键查询标签定义 +func (m *defaultTagDefModel) FindOne(ctx context.Context, id, beid int64) (*TagDef, error) { + query := fmt.Sprintf("select %s from %s where `id` = ?", tagDefRows, m.table) + args := []any{id} + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var resp TagDef + err := m.conn.QueryRowCtx(ctx, &resp, query+" limit 1", args...) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindList 分页查询标签定义,objectId<=0 时查全部对象 +func (m *defaultTagDefModel) FindList(ctx context.Context, beid, objectId, page, size int64) ([]*TagDef, int64, error) { + if beid <= 0 { + return nil, 0, ErrBeidRequired + } + where := "`beid` = ?" + args := []any{beid} + if objectId > 0 { + where += " and `object_id` = ?" + args = append(args, objectId) + } + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页参数防御 + if page <= 0 { + page = 1 + } + if size <= 0 { + size = tagDefDefaultPageSize + } + if size > tagDefMaxPageSize { + size = tagDefMaxPageSize + } + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ? offset ?", tagDefRows, m.table, where) + args = append(args, size, (page-1)*size) + var list []*TagDef + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} + +// FindEnabledRuleByObject 查对象下全部启用中的 rule 标签(tag_id=0 批量打标用),按 id 升序 +func (m *defaultTagDefModel) FindEnabledRuleByObject(ctx context.Context, objectId, beid int64) ([]*TagDef, error) { + query := fmt.Sprintf("select %s from %s where `object_id` = ? and `tag_type` = 'rule' and `status` = 1", tagDefRows, m.table) + args := []any{objectId} + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var list []*TagDef + if err := m.conn.QueryRowsCtx(ctx, &list, query+" order by `id` asc", args...); err != nil { + return nil, err + } + return list, nil +} + +// Update 全量更新标签定义(带多租户过滤) +func (m *defaultTagDefModel) Update(ctx context.Context, data *TagDef) error { + query := fmt.Sprintf("update %s set %s where `id` = ? and `beid` = ?", m.table, tagDefRowsWithPlaceHolder) + _, err := m.conn.ExecCtx(ctx, query, data.ObjectId, data.TagKey, data.TagName, data.TagType, + data.RuleSql, data.RefreshCron, data.Status, data.Beid, data.Id, data.Beid) + return err +} + +// Delete 删除标签定义并级联删除其全部标签值:同一事务内先删 dc_tag_value 再删 dc_tag_def, +// 保证不留孤儿标签值 +func (m *defaultTagDefModel) Delete(ctx context.Context, id, beid int64) error { + return m.conn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { + // 先级联删除该标签全部打标值 + delValues := "delete from `dc_tag_value` where `tag_id` = ?" + if _, err := session.ExecCtx(ctx, delValues, id); err != nil { + return fmt.Errorf("级联删除标签值失败: %w", err) + } + // 再删标签定义 + delDef := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + if _, err := session.ExecCtx(ctx, delDef, id, beid); err != nil { + return fmt.Errorf("删除标签定义失败: %w", err) + } + return nil + }) +} diff --git a/tag/model/tagvaluemodel.go b/tag/model/tagvaluemodel.go new file mode 100644 index 0000000..63f004d --- /dev/null +++ b/tag/model/tagvaluemodel.go @@ -0,0 +1,112 @@ +package model + +import ( + "context" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + tagValueTable = "`dc_tag_value`" + tagValueInsertChunk = 500 // ReplaceBatch 批量插入分片大小 + tagValueFieldSize = 4 // 每行插入字段数(tag_id/object_pk/tag_value/beid) +) + +type ( + // TagValue dc_tag_value 行(打标结果;ReplaceBatch 的内存行载体) + TagValue struct { + Id int64 `db:"id"` + TagId int64 `db:"tag_id"` + ObjectPk string `db:"object_pk"` //对象主键值 + TagValue string `db:"tag_value"` + Beid int64 `db:"beid"` + TaggedAt time.Time `db:"tagged_at"` //打标时间(落库时由默认值填充) + } + + // TagValueRow ListByObjectPk 专用扫描结构:与 select 的 4 列严格对齐 + // (go-zero 对少列查询扫宽 struct 会报 not matching destination to scan) + TagValueRow struct { + TagKey string `db:"tag_key"` + TagName string `db:"tag_name"` + TagValue string `db:"tag_value"` + TaggedAt time.Time `db:"tagged_at"` + } + + TagValueModel interface { + // ReplaceBatch 事务内先按 tag_id 删除旧值,再分片批量写入新值(整体替换语义) + ReplaceBatch(ctx context.Context, tagId, beid int64, rows []*TagValue) error + // ListByObjectPk 按对象+主键值查标签画像,join dc_tag_def 出 tag_key/tag_name + ListByObjectPk(ctx context.Context, objectId, beid int64, objectPk string) ([]*TagValueRow, error) + } + + defaultTagValueModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewTagValueModel 创建标签值 model +func NewTagValueModel(conn sqlx.SqlConn) TagValueModel { + return &defaultTagValueModel{ + conn: conn, + table: tagValueTable, + } +} + +// ReplaceBatch 整体替换某标签的打标值:同一事务内先 delete by tag_id, +// 再按 tagValueInsertChunk 分片批量 insert(tagged_at 由库默认值填充为当前时间)。 +// rows 为空时仅清空旧值(空结果也是一次有效打标)。 +// 调用方需保证 rows 内 object_pk 已去重,否则会撞 uk_tag_pk 唯一键。 +func (m *defaultTagValueModel) ReplaceBatch(ctx context.Context, tagId, beid int64, rows []*TagValue) error { + if tagId <= 0 { + return ErrBeidRequired + } + return m.conn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { + // 先清空该标签旧值(整体替换语义) + delQuery := "delete from `dc_tag_value` where `tag_id` = ?" + if _, err := session.ExecCtx(ctx, delQuery, tagId); err != nil { + return err + } + if len(rows) == 0 { + return nil + } + // 分片拼接多行 values 批量写入 + prefix := "insert into " + m.table + " (`tag_id`,`object_pk`,`tag_value`,`beid`) values " + for start := 0; start < len(rows); start += tagValueInsertChunk { + end := start + tagValueInsertChunk + if end > len(rows) { + end = len(rows) + } + batch := rows[start:end] + query := prefix + strings.TrimSuffix(strings.Repeat("("+placeholders(tagValueFieldSize)+"),", len(batch)), ",") + args := make([]any, 0, len(batch)*tagValueFieldSize) + for _, r := range batch { + args = append(args, tagId, r.ObjectPk, r.TagValue, beid) + } + if _, err := session.ExecCtx(ctx, query, args...); err != nil { + return err + } + } + return nil + }) +} + +// ListByObjectPk 查某对象某主键值的标签画像:join dc_tag_def 出 tag_key/tag_name; +// tagged_at 可空用 ifnull 兜底避免扫描报错;select 4 列与 TagValueRow 4 字段严格对齐 +func (m *defaultTagValueModel) ListByObjectPk(ctx context.Context, objectId, beid int64, objectPk string) ([]*TagValueRow, error) { + query := "select d.`tag_key`, d.`tag_name`, v.`tag_value`, ifnull(v.`tagged_at`, now(3)) as `tagged_at` " + + "from " + m.table + " v join `dc_tag_def` d on d.`id` = v.`tag_id` " + + "where d.`object_id` = ? and v.`object_pk` = ?" + args := []any{objectId, objectPk} + if beid > 0 { + query += " and d.`beid` = ?" + args = append(args, beid) + } + var list []*TagValueRow + if err := m.conn.QueryRowsCtx(ctx, &list, query+" order by d.`id` asc", args...); err != nil { + return nil, err + } + return list, nil +} diff --git a/tag/model/vars.go b/tag/model/vars.go new file mode 100644 index 0000000..a8c38c7 --- /dev/null +++ b/tag/model/vars.go @@ -0,0 +1,33 @@ +package model + +import ( + "errors" + "strings" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// 通用错误 +var ( + // ErrNotFound 记录不存在 + ErrNotFound = sqlx.ErrNotFound + // ErrBeidRequired 多租户 beid 缺失 + ErrBeidRequired = errors.New("beid 不能为空") + // ErrDupTagKey 同一对象下 tag_key 重复(object_id+tag_key+beid 唯一) + ErrDupTagKey = errors.New("同对象下 tag_key 已存在") +) + +// placeholders 生成 n 个 ? 占位符(逗号分隔) +func placeholders(n int) string { + return strings.TrimSuffix(strings.Repeat("?,", n), ",") +} + +// isDuplicateEntry 识别 MySQL 唯一键冲突(驱动报错文本含 duplicate entry / 错误码 1062), +// 用于 Create 查重后并发插入撞 uk_obj_key 的兜底 +func isDuplicateEntry(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "duplicate entry") || strings.Contains(msg, "1062") +} diff --git a/tag/rpc/internal/logic/convert.go b/tag/rpc/internal/logic/convert.go new file mode 100644 index 0000000..97e6171 --- /dev/null +++ b/tag/rpc/internal/logic/convert.go @@ -0,0 +1,41 @@ +package logic + +import ( + "datacenter/tag/model" + "datacenter/tag/rpc/tag" +) + +// toPbObject model → pb(标签对象) +func toPbObject(o *model.TagObject) *tag.TagObject { + if o == nil { + return nil + } + return &tag.TagObject{ + Id: o.Id, + ObjectType: o.ObjectType, + Name: o.Name, + DsId: o.DsId, + DbName: o.DbName, + TableName: o.TableName, + PkColumn: o.PkColumn, + Beid: o.Beid, + } +} + +// toPbTagDef model → pb(标签定义) +func toPbTagDef(d *model.TagDef) *tag.TagDef { + if d == nil { + return nil + } + return &tag.TagDef{ + Id: d.Id, + ObjectId: d.ObjectId, + TagKey: d.TagKey, + TagName: d.TagName, + TagType: d.TagType, + RuleSql: d.RuleSql, + RefreshCron: d.RefreshCron, + Status: d.Status, + Beid: d.Beid, + } +} diff --git a/tag/rpc/internal/logic/createObjectLogic.go b/tag/rpc/internal/logic/createObjectLogic.go index 5dbe688..935ad4c 100644 --- a/tag/rpc/internal/logic/createObjectLogic.go +++ b/tag/rpc/internal/logic/createObjectLogic.go @@ -2,7 +2,10 @@ package logic import ( "context" + "fmt" + "strings" + "datacenter/tag/model" "datacenter/tag/rpc/internal/svc" "datacenter/tag/rpc/tag" @@ -23,9 +26,38 @@ func NewCreateObjectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Crea } } -// 创建标签对象 +// 创建标签对象:object_type 走白名单(user/member/goods), +// pk_column 未传时落默认值 id,保证后续打标链路可用 func (l *CreateObjectLogic) CreateObject(in *tag.CreateObjectReq) (*tag.ObjectReply, error) { - // todo: add your logic here and delete this line - - return &tag.ObjectReply{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + if !ValidObjectType(in.ObjectType) { + return nil, ErrInvalidObjectType + } + if strings.TrimSpace(in.Name) == "" { + return nil, fmt.Errorf("%w: 对象名不能为空", ErrInvalidParams) + } + pkColumn := strings.TrimSpace(in.PkColumn) + if pkColumn == "" { + pkColumn = defaultPkColumn + } + id, err := l.svcCtx.ObjectModel.Insert(l.ctx, &model.TagObject{ + ObjectType: in.ObjectType, + Name: strings.TrimSpace(in.Name), + DsId: in.DsId, + DbName: in.DbName, + TableName: strings.TrimSpace(in.TableName), + PkColumn: pkColumn, + Beid: in.Beid, + }) + if err != nil { + l.Errorf("创建标签对象失败, name: %s, beid: %d, err: %v", in.Name, in.Beid, err) + return nil, err + } + obj, err := l.svcCtx.ObjectModel.FindOne(l.ctx, id, in.Beid) + if err != nil { + return nil, err + } + return &tag.ObjectReply{Object: toPbObject(obj)}, nil } diff --git a/tag/rpc/internal/logic/createTagLogic.go b/tag/rpc/internal/logic/createTagLogic.go index 61942d9..9ae0140 100644 --- a/tag/rpc/internal/logic/createTagLogic.go +++ b/tag/rpc/internal/logic/createTagLogic.go @@ -2,7 +2,11 @@ package logic import ( "context" + "errors" + "fmt" + "strings" + "datacenter/tag/model" "datacenter/tag/rpc/internal/svc" "datacenter/tag/rpc/tag" @@ -23,9 +27,61 @@ func NewCreateTagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateT } } -// 创建标签定义 +// 创建标签定义:校验对象存在 → tag_type 白名单 → rule 类型强校验 rule_sql +// (必填、select 开头、含 as object_id/as tag_value 两列、禁分号防注入)→ 唯一查重后落库 func (l *CreateTagLogic) CreateTag(in *tag.CreateTagReq) (*tag.TagDefReply, error) { - // todo: add your logic here and delete this line + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + if in.ObjectId <= 0 { + return nil, fmt.Errorf("%w: object_id 不能为空", ErrInvalidParams) + } + tagKey := strings.TrimSpace(in.TagKey) + if tagKey == "" { + return nil, fmt.Errorf("%w: tag_key 不能为空", ErrInvalidParams) + } + if strings.TrimSpace(in.TagName) == "" { + return nil, fmt.Errorf("%w: tag_name 不能为空", ErrInvalidParams) + } + if !ValidTagType(in.TagType) { + return nil, ErrInvalidTagType + } + // 所属对象必须存在(带租户隔离) + if _, err := l.svcCtx.ObjectModel.FindOne(l.ctx, in.ObjectId, in.Beid); err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrObjectNotFound + } + return nil, err + } + // rule 类型标签必须提供可通过校验的规则 SQL;其余类型清空存档 + var ruleSql string + if in.TagType == TagTypeRule { + if err := ValidateRuleSql(in.RuleSql); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidRuleSql, err) + } + ruleSql = strings.TrimSpace(in.RuleSql) + } - return &tag.TagDefReply{}, nil + id, err := l.svcCtx.TagDefModel.Create(l.ctx, &model.TagDef{ + ObjectId: in.ObjectId, + TagKey: tagKey, + TagName: strings.TrimSpace(in.TagName), + TagType: in.TagType, + RuleSql: ruleSql, + RefreshCron: strings.TrimSpace(in.RefreshCron), + Status: 1, //新建默认启用 + Beid: in.Beid, + }) + if err != nil { + if errors.Is(err, model.ErrDupTagKey) { + return nil, ErrDupTagKey + } + l.Errorf("创建标签定义失败, object_id: %d, tag_key: %s, err: %v", in.ObjectId, in.TagKey, err) + return nil, err + } + def, err := l.svcCtx.TagDefModel.FindOne(l.ctx, id, in.Beid) + if err != nil { + return nil, err + } + return &tag.TagDefReply{Def: toPbTagDef(def)}, nil } diff --git a/tag/rpc/internal/logic/deleteTagLogic.go b/tag/rpc/internal/logic/deleteTagLogic.go index 766c96c..fd25ca4 100644 --- a/tag/rpc/internal/logic/deleteTagLogic.go +++ b/tag/rpc/internal/logic/deleteTagLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "errors" + "datacenter/tag/model" "datacenter/tag/rpc/internal/svc" "datacenter/tag/rpc/tag" @@ -23,9 +25,22 @@ func NewDeleteTagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteT } } -// 删除标签定义 +// 删除标签定义(级联删除其全部标签值,事务内先删值后删定义) func (l *DeleteTagLogic) DeleteTag(in *tag.IdReq) (*tag.IdReq, error) { - // todo: add your logic here and delete this line - - return &tag.IdReq{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + // 先确认存在(带租户隔离),避免误删 + def, err := l.svcCtx.TagDefModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrTagNotFound + } + return nil, err + } + if err := l.svcCtx.TagDefModel.Delete(l.ctx, def.Id, def.Beid); err != nil { + l.Errorf("删除标签定义失败, id: %d, beid: %d, err: %v", in.Id, in.Beid, err) + return nil, err + } + return &tag.IdReq{Id: in.Id, Beid: in.Beid}, nil } diff --git a/tag/rpc/internal/logic/error.go b/tag/rpc/internal/logic/error.go new file mode 100644 index 0000000..a9168ef --- /dev/null +++ b/tag/rpc/internal/logic/error.go @@ -0,0 +1,33 @@ +package logic + +import "errors" + +// 业务错误(对齐 metadata/votes 服务写法:errors.New 中文语义错误) +var ( + // ErrBeidRequired 租户 beid 缺失 + ErrBeidRequired = errors.New("租户 beid 不能为空") + // ErrInvalidParams 通用入参缺失 + ErrInvalidParams = errors.New("参数不完整") + // ErrInvalidObjectType 对象类型不在白名单(仅 user/member/goods) + ErrInvalidObjectType = errors.New("object_type 仅支持 user/member/goods") + // ErrInvalidTagType 标签类型不在白名单(仅 rule/fact/model) + ErrInvalidTagType = errors.New("tag_type 仅支持 rule/fact/model") + // ErrObjectNotFound 标签对象不存在 + ErrObjectNotFound = errors.New("标签对象不存在") + // ErrTagNotFound 标签定义不存在 + ErrTagNotFound = errors.New("标签定义不存在") + // ErrDupTagKey 同一对象下 tag_key 重复 + ErrDupTagKey = errors.New("同一对象下 tag_key 已存在") + // ErrInvalidRuleSql 规则 SQL 未通过校验(非 select 开头/缺别名列/含分号等) + ErrInvalidRuleSql = errors.New("rule_sql 校验未通过") + // ErrNotRuleTag 非 rule 类型标签无规则 SQL,不能执行打标 + ErrNotRuleTag = errors.New("仅 rule 类型标签支持打标") + // ErrTagTargetRequired 打标目标缺失(tag_id 与 object_id 至少传一个) + ErrTagTargetRequired = errors.New("tag_id 与 object_id 至少传一个") + // ErrObjectTargetMissing 对象未配置数据源/表,无法打标 + ErrObjectTargetMissing = errors.New("标签对象未配置 ds_id/table_name,无法打标") + // ErrDatasource 数据源 rpc 调用失败或未返回 DSN + ErrDatasource = errors.New("获取目标数据源失败") + // ErrRuleExec 规则 SQL 执行失败 + ErrRuleExec = errors.New("规则 SQL 执行失败") +) diff --git a/tag/rpc/internal/logic/getObjectTagsLogic.go b/tag/rpc/internal/logic/getObjectTagsLogic.go index 3be618c..0d09e72 100644 --- a/tag/rpc/internal/logic/getObjectTagsLogic.go +++ b/tag/rpc/internal/logic/getObjectTagsLogic.go @@ -2,6 +2,8 @@ package logic import ( "context" + "fmt" + "strings" "datacenter/tag/rpc/internal/svc" "datacenter/tag/rpc/tag" @@ -23,9 +25,31 @@ func NewGetObjectTagsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get } } -// 查对象标签画像 +// 查对象标签画像:按 object_pk 查打标值,join tag_def 出 tag_key/tag_name func (l *GetObjectTagsLogic) GetObjectTags(in *tag.ObjectTagsReq) (*tag.ObjectTagsReply, error) { - // todo: add your logic here and delete this line - - return &tag.ObjectTagsReply{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + if in.ObjectId <= 0 { + return nil, fmt.Errorf("%w: object_id 不能为空", ErrInvalidParams) + } + pk := strings.TrimSpace(in.ObjectPk) + if pk == "" { + return nil, fmt.Errorf("%w: object_pk 不能为空", ErrInvalidParams) + } + rows, err := l.svcCtx.TagValueModel.ListByObjectPk(l.ctx, in.ObjectId, in.Beid, pk) + if err != nil { + l.Errorf("查询对象标签画像失败, object_id: %d, object_pk: %s, err: %v", in.ObjectId, pk, err) + return nil, err + } + items := make([]*tag.TagValueItem, 0, len(rows)) + for _, r := range rows { + items = append(items, &tag.TagValueItem{ + TagKey: r.TagKey, + TagName: r.TagName, + TagValue: r.TagValue, + TaggedAt: r.TaggedAt.Unix(), + }) + } + return &tag.ObjectTagsReply{Tags: items}, nil } diff --git a/tag/rpc/internal/logic/listObjectsLogic.go b/tag/rpc/internal/logic/listObjectsLogic.go index 89d16a6..309f98c 100644 --- a/tag/rpc/internal/logic/listObjectsLogic.go +++ b/tag/rpc/internal/logic/listObjectsLogic.go @@ -23,9 +23,19 @@ func NewListObjectsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListO } } -// 对象列表 +// 对象列表:beid 必带,object_type 可选过滤,分页返回 func (l *ListObjectsLogic) ListObjects(in *tag.ObjectListReq) (*tag.ObjectListReply, error) { - // todo: add your logic here and delete this line - - return &tag.ObjectListReply{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + list, total, err := l.svcCtx.ObjectModel.FindList(l.ctx, in.Beid, in.ObjectType, in.Page, in.Size) + if err != nil { + l.Errorf("查询标签对象列表失败, beid: %d, err: %v", in.Beid, err) + return nil, err + } + items := make([]*tag.TagObject, 0, len(list)) + for _, o := range list { + items = append(items, toPbObject(o)) + } + return &tag.ObjectListReply{List: items, Total: total}, nil } diff --git a/tag/rpc/internal/logic/listTagsLogic.go b/tag/rpc/internal/logic/listTagsLogic.go index 1b90f3c..5adb367 100644 --- a/tag/rpc/internal/logic/listTagsLogic.go +++ b/tag/rpc/internal/logic/listTagsLogic.go @@ -23,9 +23,19 @@ func NewListTagsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListTags } } -// 标签定义列表 +// 标签定义列表:beid 必带,object_id<=0 时查全部对象,分页返回 func (l *ListTagsLogic) ListTags(in *tag.TagDefListReq) (*tag.TagDefListReply, error) { - // todo: add your logic here and delete this line - - return &tag.TagDefListReply{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + list, total, err := l.svcCtx.TagDefModel.FindList(l.ctx, in.Beid, in.ObjectId, in.Page, in.Size) + if err != nil { + l.Errorf("查询标签定义列表失败, beid: %d, object_id: %d, err: %v", in.Beid, in.ObjectId, err) + return nil, err + } + items := make([]*tag.TagDef, 0, len(list)) + for _, d := range list { + items = append(items, toPbTagDef(d)) + } + return &tag.TagDefListReply{List: items, Total: total}, nil } diff --git a/tag/rpc/internal/logic/rule.go b/tag/rpc/internal/logic/rule.go new file mode 100644 index 0000000..c6270df --- /dev/null +++ b/tag/rpc/internal/logic/rule.go @@ -0,0 +1,76 @@ +package logic + +import ( + "fmt" + "regexp" + "strings" +) + +// 标签类型 +const ( + TagTypeRule = "rule" //规则标签(rule_sql 跑目标库产出) + TagTypeFact = "fact" //事实标签 + TagTypeModel = "model" //模型标签 +) + +// 标签对象类型 +const ( + ObjectTypeUser = "user" + ObjectTypeMember = "member" + ObjectTypeGoods = "goods" +) + +// defaultPkColumn 对象未显式指定主键列时的默认值 +const defaultPkColumn = "id" + +var ( + // ruleAliasObjectIdRe 规则 SQL 结果集必须包含 as object_id 别名列(忽略大小写、词边界匹配, + // "as object_id2" 这类冒充别名无法通过) + ruleAliasObjectIdRe = regexp.MustCompile(`(?i)\bas\s+object_id\b`) + // ruleAliasTagValueRe 规则 SQL 结果集必须包含 as tag_value 别名列 + ruleAliasTagValueRe = regexp.MustCompile(`(?i)\bas\s+tag_value\b`) +) + +// ValidTagType 标签类型白名单校验(rule/fact/model) +func ValidTagType(typ string) bool { + switch typ { + case TagTypeRule, TagTypeFact, TagTypeModel: + return true + default: + return false + } +} + +// ValidObjectType 标签对象类型白名单校验(user/member/goods) +func ValidObjectType(typ string) bool { + switch typ { + case ObjectTypeUser, ObjectTypeMember, ObjectTypeGoods: + return true + default: + return false + } +} + +// ValidateRuleSql 规则 SQL 校验(纯函数,便于单测): +// 1) 非空;2) 必须以 select 开头(只读限制,杜绝 update/delete 等写语句); +// 3) 必须包含 as object_id 与 as tag_value 两列别名(打标执行按列名取数); +// 4) 禁止分号(单语句限制,天然拒绝 `; drop table ...` 等注入片段)。 +func ValidateRuleSql(sqlText string) error { + s := strings.TrimSpace(sqlText) + if s == "" { + return fmt.Errorf("rule_sql 不能为空") + } + if len(s) < 6 || !strings.EqualFold(s[:6], "select") { + return fmt.Errorf("rule_sql 必须以 select 开头") + } + if strings.Contains(s, ";") { + return fmt.Errorf("rule_sql 仅允许单条语句,禁止分号(拒绝 ; drop 等注入片段)") + } + if !ruleAliasObjectIdRe.MatchString(s) { + return fmt.Errorf("rule_sql 缺少 as object_id 别名列") + } + if !ruleAliasTagValueRe.MatchString(s) { + return fmt.Errorf("rule_sql 缺少 as tag_value 别名列") + } + return nil +} diff --git a/tag/rpc/internal/logic/rule_test.go b/tag/rpc/internal/logic/rule_test.go new file mode 100644 index 0000000..5ce65f1 --- /dev/null +++ b/tag/rpc/internal/logic/rule_test.go @@ -0,0 +1,80 @@ +package logic + +import ( + "strings" + "testing" +) + +// TestValidateRuleSql 规则 SQL 校验:非 select / 缺 as object_id / 缺 as tag_value / +// 注入片段 `; drop` 等必须拒绝,合法规则放行 +func TestValidateRuleSql(t *testing.T) { + valid := "select id as object_id, case when score >= 90 then 'A' else 'B' end as tag_value from ods.t_user where status = 1" + cases := []struct { + name string + sqlText string + wantErr bool + }{ + {"合法规则", valid, false}, + {"别名大小写不敏感", "SELECT id AS OBJECT_ID, 'x' AS TAG_VALUE FROM t", false}, + {"多余空白容忍", " select id as object_id, level as tag_value from t ", false}, + {"空语句", " ", true}, + {"过短语句", "sele", true}, + {"非 select: update", "update t_user set level = 1", true}, + {"非 select: delete", "delete from t_user", true}, + {"非 select: insert", "insert into t(id) values (1)", true}, + {"缺 as object_id", "select id, 'x' as tag_value from t", true}, + {"缺 as tag_value", "select id as object_id from t", true}, + {"两个别名都缺", "select id, name from t", true}, + {"冒充别名不带词边界", "select id as object_id2, 'x' as tag_value2 from t", true}, + {"注入片段 ; drop", "select id as object_id, 'x' as tag_value from t; drop table t", true}, + {"注入片段 ; delete", "select id as object_id, 'x' as tag_value from t;delete from dc_tag_value", true}, + {"结尾分号也拒绝", "select id as object_id, 'x' as tag_value from t;", true}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := ValidateRuleSql(c.sqlText) + if (err != nil) != c.wantErr { + t.Fatalf("ValidateRuleSql(%q) err = %v, wantErr = %v", c.sqlText, err, c.wantErr) + } + }) + } +} + +// TestValidateRuleSqlErrorText 注入片段拒绝时的错误信息应说明分号限制 +func TestValidateRuleSqlErrorText(t *testing.T) { + err := ValidateRuleSql("select id as object_id, 'x' as tag_value from t; drop table t") + if err == nil { + t.Fatal("注入片段 ; drop 必须被拒绝") + } + if !strings.Contains(err.Error(), "分号") { + t.Fatalf("错误信息应提示分号限制, got: %v", err) + } +} + +// TestValidTagType tag_type 白名单:仅 rule/fact/model +func TestValidTagType(t *testing.T) { + for _, typ := range []string{"rule", "fact", "model"} { + if !ValidTagType(typ) { + t.Fatalf("tag_type %q 应在白名单内", typ) + } + } + for _, typ := range []string{"", "sql", "RULE", "model2", " rule"} { + if ValidTagType(typ) { + t.Fatalf("tag_type %q 不应在白名单内", typ) + } + } +} + +// TestValidObjectType object_type 白名单:仅 user/member/goods +func TestValidObjectType(t *testing.T) { + for _, typ := range []string{"user", "member", "goods"} { + if !ValidObjectType(typ) { + t.Fatalf("object_type %q 应在白名单内", typ) + } + } + for _, typ := range []string{"", "order", "User", "shop"} { + if ValidObjectType(typ) { + t.Fatalf("object_type %q 不应在白名单内", typ) + } + } +} diff --git a/tag/rpc/internal/logic/taggingLogic.go b/tag/rpc/internal/logic/taggingLogic.go index 5b5ea2f..8b9c437 100644 --- a/tag/rpc/internal/logic/taggingLogic.go +++ b/tag/rpc/internal/logic/taggingLogic.go @@ -2,7 +2,13 @@ package logic import ( "context" + "database/sql" + "errors" + "fmt" + "strings" + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/tag/model" "datacenter/tag/rpc/internal/svc" "datacenter/tag/rpc/tag" @@ -23,9 +29,166 @@ func NewTaggingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TaggingLo } } -// 执行打标(规则 SQL 跑目标库,结果写 dc_tag_value) +// 执行打标(核心链路): +// tag_id>0 单标签打标(须为 rule 类型);tag_id=0 时对 object 全部 status=1 且 +// tag_type=rule 的标签逐个打标。每个标签: +// 对象表信息(ds_id/db_name/table_name) → datasource rpc 换目标库 DSN → +// 目标库执行 rule_sql(RawDB 手工扫描 object_id/tag_value 两列)→ +// ReplaceBatch 事务内整体替换写 dc_tag_value;累计返回 tagged_rows。 func (l *TaggingLogic) Tagging(in *tag.TaggingReq) (*tag.TaggingReply, error) { - // todo: add your logic here and delete this line + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + + //1. 圈定待打标标签集合 + var defs []*model.TagDef + if in.TagId > 0 { + def, err := l.svcCtx.TagDefModel.FindOne(l.ctx, in.TagId, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrTagNotFound + } + return nil, err + } + //单标签仅要求 rule 类型(显式指定时不限制 status,便于停用标签手动补跑) + if def.TagType != TagTypeRule { + return nil, ErrNotRuleTag + } + defs = []*model.TagDef{def} + } else { + if in.ObjectId <= 0 { + return nil, ErrTagTargetRequired + } + //批量:对象下全部启用中的 rule 标签 + list, err := l.svcCtx.TagDefModel.FindEnabledRuleByObject(l.ctx, in.ObjectId, in.Beid) + if err != nil { + return nil, err + } + if len(list) == 0 { + return &tag.TaggingReply{}, nil //无标签可打,空结果直接返回 + } + defs = list + } + + //2. 对象表信息(同批标签同属一个对象,查一次即可) + objectId := in.ObjectId + if in.TagId > 0 { + objectId = defs[0].ObjectId + } + obj, err := l.svcCtx.ObjectModel.FindOne(l.ctx, objectId, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrObjectNotFound + } + return nil, err + } + if obj.DsId <= 0 || strings.TrimSpace(obj.TableName) == "" { + return nil, ErrObjectTargetMissing + } - return &tag.TaggingReply{}, nil + //3. 构造目标库连接:datasource rpc GetDataSource(with_secret=true) 换 DSN + dsReply, err := l.svcCtx.DataSourceRpc.GetDataSource(l.ctx, &dataSourceClient.GetDataSourceReq{ + Id: obj.DsId, + Beid: in.Beid, + WithSecret: true, + }) + if err != nil { + return nil, fmt.Errorf("%w: 数据源 %d: %v", ErrDatasource, obj.DsId, err) + } + if strings.TrimSpace(dsReply.GetDsn()) == "" { + return nil, fmt.Errorf("%w: 数据源 %d 未返回 DSN", ErrDatasource, obj.DsId) + } + //连接目标库(ServiceContext 按 DSN 缓存连接池);RawDB 不可 Close,底层连接池需复用 + conn, err := l.svcCtx.TargetConn(dsReply.Dsn) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrDatasource, err) + } + db, err := conn.RawDB() + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrDatasource, err) + } + + //4. 逐标签打标:跑 rule_sql → 事务内替换写 dc_tag_value,累计 tagged_rows + var total int64 + for _, def := range defs { + rows, err := execRuleSql(l.ctx, db, def.RuleSql) + if err != nil { + return nil, fmt.Errorf("%w: 标签 %d(%s): %v", ErrRuleExec, def.Id, def.TagKey, err) + } + if err := l.svcCtx.TagValueModel.ReplaceBatch(l.ctx, def.Id, def.Beid, rows); err != nil { + return nil, fmt.Errorf("标签 %d(%s) 写入标签值失败: %w", def.Id, def.TagKey, err) + } + total += int64(len(rows)) + l.Infof("打标完成 object=%d tag=%d(%s) rows=%d", objectId, def.Id, def.TagKey, len(rows)) + } + return &tag.TaggingReply{TaggedRows: total, TagId: in.TagId}, nil +} + +// execRuleSql 在目标库执行规则 SQL(参考 datasvc QueryRowsAsMaps 的手工扫描思路): +// 按结果集列名定位 object_id/tag_value 两列(列顺序不限),驱动返回的 []byte 统一转 string; +// 同批主键重复时后值覆盖前值(dc_tag_value 对 (tag_id,object_pk) 唯一,避免撞键) +func execRuleSql(ctx context.Context, db *sql.DB, ruleSql string) ([]*model.TagValue, error) { + rs, err := db.QueryContext(ctx, ruleSql) + if err != nil { + return nil, err + } + defer rs.Close() + cols, err := rs.Columns() + if err != nil { + return nil, err + } + idIdx, valIdx := -1, -1 + for i, c := range cols { + switch strings.ToLower(strings.TrimSpace(c)) { + case "object_id": + idIdx = i + case "tag_value": + valIdx = i + } + } + if idIdx < 0 || valIdx < 0 { + return nil, fmt.Errorf("结果集缺少 object_id/tag_value 列") + } + + rows := make([]*model.TagValue, 0, 64) + pos := make(map[string]int, 64) //object_pk -> rows 下标(主键去重) + for rs.Next() { + container := make([]interface{}, len(cols)) + ptrs := make([]interface{}, len(cols)) + for i := range container { + ptrs[i] = &container[i] + } + if err := rs.Scan(ptrs...); err != nil { + return nil, err + } + pk := ruleCellToString(container[idIdx]) + if pk == "" { + continue //主键值为空的行跳过 + } + value := ruleCellToString(container[valIdx]) + if idx, ok := pos[pk]; ok { + rows[idx].TagValue = value //后值覆盖前值 + continue + } + pos[pk] = len(rows) + rows = append(rows, &model.TagValue{ObjectPk: pk, TagValue: value}) + } + if err := rs.Err(); err != nil { + return nil, err + } + return rows, nil +} + +// ruleCellToString 单元格值转字符串([]byte/string 原样,其余走 %v,nil 视为空串) +func ruleCellToString(v interface{}) string { + switch t := v.(type) { + case nil: + return "" + case []byte: + return strings.TrimSpace(string(t)) + case string: + return strings.TrimSpace(t) + default: + return strings.TrimSpace(fmt.Sprintf("%v", t)) + } } diff --git a/tag/rpc/internal/svc/serviceContext.go b/tag/rpc/internal/svc/serviceContext.go index 814124b..ccd24f6 100644 --- a/tag/rpc/internal/svc/serviceContext.go +++ b/tag/rpc/internal/svc/serviceContext.go @@ -1,13 +1,62 @@ package svc -import "datacenter/tag/rpc/internal/config" +import ( + "sync" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/tag/model" + "datacenter/tag/rpc/internal/config" + + "github.com/zeromicro/go-zero/core/discov" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/zrpc" +) + +// dataSourceEtcdKey datasource rpc 的 etcd 服务 key(Phase 1 契约固定) +const dataSourceEtcdKey = "datasource.rpc" type ServiceContext struct { - Config config.Config + Config config.Config + Conn sqlx.SqlConn //中台主库连接(dc_tag_* 三表) + ObjectModel model.ObjectModel + TagDefModel model.TagDefModel + TagValueModel model.TagValueModel + DataSourceRpc dataSourceClient.DataSource //数据源 rpc(Tagging 换目标库 DSN) + + connMu sync.Mutex + targetCons map[string]sqlx.SqlConn //目标库连接池缓存:key=dsn,复用底层连接池 } func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) + //datasource rpc 客户端:config.go 不可改,参考 datasvc 内联装配写法, + //etcd key 固定 datasource.rpc;NonBlock 保证 datasource 未就绪时不阻塞本服务启动 + dsRpc := dataSourceClient.NewDataSource(zrpc.MustNewClient(zrpc.RpcClientConf{ + Etcd: discov.EtcdConf{ + Hosts: c.Etcd.Hosts, + Key: dataSourceEtcdKey, + }, + NonBlock: true, + })) return &ServiceContext{ - Config: c, + Config: c, + Conn: conn, + ObjectModel: model.NewObjectModel(conn), + TagDefModel: model.NewTagDefModel(conn), + TagValueModel: model.NewTagValueModel(conn), + DataSourceRpc: dsRpc, + targetCons: make(map[string]sqlx.SqlConn), + } +} + +// TargetConn 获取目标库连接(按 DSN 缓存 sqlx.SqlConn,复用底层连接池) +func (sc *ServiceContext) TargetConn(dsn string) (sqlx.SqlConn, error) { + sc.connMu.Lock() + defer sc.connMu.Unlock() + if conn, ok := sc.targetCons[dsn]; ok { + return conn, nil } + conn := sqlx.NewMysql(dsn) + sc.targetCons[dsn] = conn + return conn, nil } From d5acc3adf5c8521ae56e58fe810325172a24a8ac Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 13:44:23 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E5=AE=89=E5=85=A8=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=9A=E6=81=A2=E5=A4=8D=E8=A2=AB=E6=B3=A8=E9=87=8A=E7=9A=84?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E4=B8=AD=E9=97=B4=E4=BB=B6=EF=BC=88issue=20#?= =?UTF-8?q?18=20P0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UsercheckMiddleware:恢复 x-user-id 与 JWT claims uid 的防越权比对(此前整段注释直接放行) - AdminCheckMiddleware:恢复 BasicAuth,凭据读 ADMIN_USER/ADMIN_PASS 环境变量,未配置 fail-closed 拒绝所有 - 新增 8 个单测;注意:客户端调用 votes/questions 等接口需携带 x-user-id 头 --- internal/middleware/admincheckmiddleware.go | 31 +++++-- internal/middleware/middleware_test.go | 90 +++++++++++++++++++++ internal/middleware/usercheckmiddleware.go | 48 ++++++----- 3 files changed, 139 insertions(+), 30 deletions(-) create mode 100644 internal/middleware/middleware_test.go diff --git a/internal/middleware/admincheckmiddleware.go b/internal/middleware/admincheckmiddleware.go index 8691fc2..0d3d122 100644 --- a/internal/middleware/admincheckmiddleware.go +++ b/internal/middleware/admincheckmiddleware.go @@ -1,8 +1,12 @@ package middleware import ( + "crypto/subtle" "errors" "net/http" + "os" + + "github.com/zeromicro/go-zero/rest/httpx" ) var ( @@ -10,21 +14,32 @@ var ( ) type AdminCheckMiddleware struct { + user string + pass string } +// NewAdminCheckMiddleware BasicAuth 校验,凭据从环境变量 ADMIN_USER/ADMIN_PASS 读取; +// 未配置时拒绝所有请求(fail-closed),生产环境必须显式配置。 func NewAdminCheckMiddleware() *AdminCheckMiddleware { - return &AdminCheckMiddleware{} + return &AdminCheckMiddleware{ + user: os.Getenv("ADMIN_USER"), + pass: os.Getenv("ADMIN_PASS"), + } } func (m *AdminCheckMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // user, pass, _ := r.BasicAuth() - // logx.Info("user, pass", user, pass) - // if user == "xxx" && pass == "xxxxx" { - // next(w, r) - // } else { - // httpx.Error(w, AdminError) - // } + if m.user == "" || m.pass == "" { + httpx.Error(w, AdminError) + return + } + user, pass, ok := r.BasicAuth() + if !ok || + subtle.ConstantTimeCompare([]byte(user), []byte(m.user)) != 1 || + subtle.ConstantTimeCompare([]byte(pass), []byte(m.pass)) != 1 { + httpx.Error(w, AdminError) + return + } next(w, r) } } diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go new file mode 100644 index 0000000..c6f02eb --- /dev/null +++ b/internal/middleware/middleware_test.go @@ -0,0 +1,90 @@ +package middleware + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" +) + +func TestUserCheckMiddleware(t *testing.T) { + m := NewUserCheckMiddleware() + okHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + + // 一致:放行 + r := httptest.NewRequest("POST", "/votes/vote", nil) + r = r.WithContext(context.WithValue(r.Context(), "uid", int64(42))) + r.Header.Set("x-user-id", "42") + w := httptest.NewRecorder() + m.Handle(okHandler)(w, r) + if w.Code != http.StatusOK { + t.Errorf("uid 一致应放行, got %d", w.Code) + } + + // 不一致:拒绝 + r2 := httptest.NewRequest("POST", "/votes/vote", nil) + r2 = r2.WithContext(context.WithValue(r2.Context(), "uid", int64(42))) + r2.Header.Set("x-user-id", "43") + w2 := httptest.NewRecorder() + m.Handle(okHandler)(w2, r2) + if w2.Code == http.StatusOK { + t.Errorf("uid 不一致应拒绝") + } + + // 缺失 header:拒绝 + r3 := httptest.NewRequest("POST", "/votes/vote", nil) + r3 = r3.WithContext(context.WithValue(r3.Context(), "uid", int64(42))) + w3 := httptest.NewRecorder() + m.Handle(okHandler)(w3, r3) + if w3.Code == http.StatusOK { + t.Errorf("缺失 x-user-id 应拒绝") + } + + // 无 JWT claims(未带 token):拒绝 + r4 := httptest.NewRequest("POST", "/votes/vote", nil) + r4.Header.Set("x-user-id", "42") + w4 := httptest.NewRecorder() + m.Handle(okHandler)(w4, r4) + if w4.Code == http.StatusOK { + t.Errorf("无 claims 应拒绝") + } +} + +func TestAdminCheckMiddleware(t *testing.T) { + t.Setenv("ADMIN_USER", "boss") + t.Setenv("ADMIN_PASS", "secret123") + m := NewAdminCheckMiddleware() + okHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + + req := func(user, pass string) int { + r := httptest.NewRequest("GET", "/search/article", nil) + if user != "" || pass != "" { + r.SetBasicAuth(user, pass) + } + w := httptest.NewRecorder() + m.Handle(okHandler)(w, r) + return w.Code + } + + if code := req("boss", "secret123"); code != http.StatusOK { + t.Errorf("正确凭据应放行, got %d", code) + } + if code := req("boss", "wrong"); code == http.StatusOK { + t.Errorf("错误密码应拒绝") + } + if code := req("", ""); code == http.StatusOK { + t.Errorf("无凭据应拒绝") + } + + // 未配置环境变量:fail-closed 拒绝所有 + t.Setenv("ADMIN_USER", "") + t.Setenv("ADMIN_PASS", "") + m2 := NewAdminCheckMiddleware() + r := httptest.NewRequest("GET", "/x", nil) + r.SetBasicAuth("boss", "secret123") + w := httptest.NewRecorder() + m2.Handle(okHandler)(w, r) + if w.Code == http.StatusOK { + t.Errorf("未配置凭据应拒绝所有请求") + } +} diff --git a/internal/middleware/usercheckmiddleware.go b/internal/middleware/usercheckmiddleware.go index b68fa70..3e1e8b1 100644 --- a/internal/middleware/usercheckmiddleware.go +++ b/internal/middleware/usercheckmiddleware.go @@ -1,8 +1,13 @@ package middleware import ( + "encoding/json" "errors" + "fmt" "net/http" + "strconv" + + "github.com/zeromicro/go-zero/rest/httpx" ) var ( @@ -21,30 +26,29 @@ func NewUserCheckMiddleware() *UserCheckMiddleware { return &UserCheckMiddleware{} } +// Handle 防越权校验:请求头 x-user-id 必须与 JWT claims 中的 uid 一致。 +// 需挂在 go-zero jwt 中间件之后(claims 已注入 request context)。 func (m *UserCheckMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - /* - userId := r.Header.Get(userKey) - jwtUserId := r.Context().Value("userId") - logx.Info("r.Context()========", r.Context()) - logx.Info("uid====", r.Context().Value("uid")) - userInt, err := json.Number(userId).Int64() - if err != nil { - httpx.Error(w, errorUserInfo) - return - } - - jwtInt, err := json.Number(fmt.Sprintf("%v", jwtUserId)).Int64() - if err != nil { - httpx.Error(w, errorUserInfo) - return - } - - if jwtInt != userInt { - httpx.Error(w, authDeny) - return - } - */ + userId := r.Header.Get(userKey) + jwtUserId := r.Context().Value("uid") + + userInt, err := strconv.ParseInt(userId, 10, 64) + if err != nil { + httpx.Error(w, errorUserInfo) + return + } + + jwtInt, err := json.Number(fmt.Sprintf("%v", jwtUserId)).Int64() + if err != nil { + httpx.Error(w, errorUserInfo) + return + } + + if jwtInt != userInt { + httpx.Error(w, authDeny) + return + } next(w, r) } From 4302c6c8203d2c33434cd96ddec750cac2f40514 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 13:44:23 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E4=BE=9D=E8=B5=96=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=EF=BC=9Achanxuehong/wechat=EF=BC=88=E5=81=9C=E6=9B=B45?= =?UTF-8?q?=E5=B9=B4=EF=BC=89=E2=86=92=20silenceper/wechat/v2=20v2.1.14?= =?UTF-8?q?=EF=BC=88issue=20#18=20P1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JS-SDK ticket+签名与 OAuth2 登录迁移,jsapi 签名以微信官方向量锁定单测 - 修复存量 bug:Country 误取 City(2 处)、GetUserInfo 错误被掩盖、ticket 缓存 key 多租户串写 - TODO(回归):需真实公众号凭据验证 OAuth 与 JS-SDK(代码已标注) - README:新增数据中台能力区块与快速体验指引 --- README.md | 41 ++++++++ go.mod | 16 ++- go.sum | 108 ++++++++++++++++++-- internal/logic/common/wxticketlogic.go | 85 +++++++++------ internal/logic/common/wxticketlogic_test.go | 60 +++++++++++ user/rpc/internal/logic/snsloginlogic.go | 49 +++++---- 6 files changed, 299 insertions(+), 60 deletions(-) create mode 100644 internal/logic/common/wxticketlogic_test.go diff --git a/README.md b/README.md index ec6b5d3..5d9cf51 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ syslog查询、syslog实时查看、登录日志、审计日志 后端:Java springboot2.7 springcloud/alibaba mybatis plus hutool 等常见技术 前端:vue elementui vite 等常见技术 中间件:doris,mysql,redis,rabbitmq,minio,zookeeper。 +开源版后端:Go 1.26 + go-zero v1.10.3 + MySQL 8.4 + Redis 8 + etcd 3.5 + Elasticsearch 7.17 有演示环境和方案PPT,需要加V:jackluo1989 @@ -95,6 +96,46 @@ syslog查询、syslog实时查看、登录日志、审计日志 - [ ] 微信支付宝登陆 - [ ] 微信支付宝支付 +## 数据中台能力(建设中) +> 项目正从上面的活动业务中台向真正的数据中台演进,Phase 1 四服务已实测闭环,Phase 2 开发中。 + +### Phase 1 已完成(datasource / metadata / datasvc / catalog) +- **数据源管理**(datasource):统一登记各类型数据源,凭证 AES-GCM 加密存储,支持连接探活(时延/错误信息) +- **元数据管理**(metadata):基于 information_schema 自动采集库表结构,表级血缘登记与查询,元数据定版快照与多版本对比 +- **数据服务 API**(datasvc,OneService 模式:配置出 API、统一管控):HMAC 签名校验 → 应用限流 → 授权校验 → 参数白名单 → 结果缓存 → 出口字段脱敏 +- **资产目录**(catalog):资产分类、资产登记、资产申请与审批 + +### 快速体验 +```shell +sh server.sh # 1. docker 起依赖(mysql 8.4 / redis 8 / etcd 3.5 / es 7.17) +mysql -uroot -padmin # 2. 导入 Phase 1 表结构:source docs/schema/phase1.sql +sh restart.sh # 3. 重启全部服务(网关端口 8887,含 datasource/metadata/datasvc/catalog) +``` + +下面给出关键步骤的 curl 示例(网关 `http://127.0.0.1:8887`),完整流程为:登记数据源 → 采集元数据 → 建 API(`/datasvc/api/create`)→ 注册应用 → 授权(`/datasvc/grant`)→ 发布(`/datasvc/api/publish`)→ 执行: + +```shell +# 登记数据源(凭证 AES-GCM 加密入库) +curl -XPOST http://127.0.0.1:8887/datasource/create -d '{"name":"业务库","type":"mysql","host":"127.0.0.1","port":3306,"db_name":"datacenter","username":"root","password":"admin"}' + +# 采集元数据(information_schema → 元数据实体) +curl -XPOST http://127.0.0.1:8887/metadata/collect -d '{"ds_id":1}' + +# 注册应用(返回 app_key/app_secret,secret 仅注册时返回一次) +curl -XPOST http://127.0.0.1:8887/datasvc/app/register -d '{"name":"demo-app","rate_limit_qps":10}' + +# 执行 API(sign = hex(hmac-sha256(app_secret, app_key+ts)),返回 rows_json/cost_ms/cache_hit) +curl -XPOST http://127.0.0.1:8887/datasvc/execute -d '{"app_key":"","ts":"","sign":"","path":"/api/v1/demo","params_json":"{}"}' +``` + +### Phase 2 进行中(quality / metric / tag) +- 数据质量(规则/任务/结果)、数据指标(指标定义/地图)、数据标签(对象/圈群/画像)三服务开发中,表结构见 `docs/schema/phase2.sql` + +### 相关文档 +- [docs/ROADMAP.md](docs/ROADMAP.md):演进路线 +- [docs/UPGRADE.md](docs/UPGRADE.md):升级记录(Go 1.26 / go-zero v1.10.3 全栈现代化) +- [docs/PHASE1.md](docs/PHASE1.md):Phase 1 开发契约(服务端口、表结构、架构约定) + ## 如何运行 ### 先启动mysql redis etcd 服务 diff --git a/go.mod b/go.mod index 0b12961..dffda0b 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module datacenter go 1.26.0 require ( - github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd github.com/golang-jwt/jwt/v4 v4.5.2 github.com/golang/protobuf v1.5.4 github.com/olivere/elastic/v7 v7.0.32 github.com/qiniu/go-sdk/v7 v7.29.0 + github.com/silenceper/wechat/v2 v2.1.14 golang.org/x/time v0.16.0 google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 @@ -17,14 +17,18 @@ require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect + github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structs v1.1.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/grafana/pyroscope-go v1.3.0 // indirect @@ -38,6 +42,11 @@ require ( github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/redis/go-redis/v9 v9.21.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/tidwall/gjson v1.14.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/titanous/json5 v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -51,6 +60,7 @@ require ( go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/term v0.45.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect @@ -66,13 +76,11 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973 // indirect - github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect - github.com/go-sql-driver/mysql v1.10.0 // indirect + github.com/go-sql-driver/mysql v1.10.0 github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index dd57003..d43e35c 100644 --- a/go.sum +++ b/go.sum @@ -6,26 +6,28 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.30.0/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q= github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw= github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw= +github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973 h1:Js/7nHtkpvUW62passc9FOflXyLQDjSoYclkFeDdTEM= -github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973/go.mod h1:9+sJ9zvvkXC5sPjPEZM3Jpb9n2Q2VtcrGZly0UHYF5I= -github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13 h1:c1vUDbnwvu5d2ucfzXvMzBWzeu5IxPvtESOFPl3CieA= -github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13/go.mod h1:XEYt99iTxMqkv+gW85JX/DdUINHUe43Sbe5AtqSaDAQ= -github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd h1:TM3wjEWel4U31J72dlhnwCBqPC0+FA0Ejm2NCbn5a5U= -github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd/go.mod h1:/dvhOIRCjjiZu6NV0QTTiMcc5XwoORbxfDSsRY2IfaM= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -34,12 +36,19 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -55,8 +64,11 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -67,13 +79,27 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -88,6 +114,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -122,10 +150,24 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= @@ -154,8 +196,14 @@ github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGG github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/silenceper/wechat/v2 v2.1.14 h1:ORpbzDUENOMO/mXzZ2uajXPbuNTPFWxxO5vQ3Nsh9Sk= +github.com/silenceper/wechat/v2 v2.1.14/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -163,18 +211,28 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= +github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s= github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= @@ -230,32 +288,55 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= @@ -263,6 +344,7 @@ golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= @@ -278,6 +360,14 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -285,12 +375,18 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/logic/common/wxticketlogic.go b/internal/logic/common/wxticketlogic.go index 09dbf4c..567b98e 100644 --- a/internal/logic/common/wxticketlogic.go +++ b/internal/logic/common/wxticketlogic.go @@ -1,7 +1,13 @@ +// TODO(回归): 需真实公众号凭据验证 OAuth 与 JS-SDK 签名 +// +// 本文件已从 chanxuehong/wechat(已停更)迁移至 silenceper/wechat/v2。 package logic import ( "context" + "crypto/sha1" + "encoding/hex" + "strings" "time" "datacenter/common/rpc/common" @@ -9,8 +15,10 @@ import ( "datacenter/internal/types" "datacenter/shared" - "github.com/chanxuehong/wechat/mp/core" - "github.com/chanxuehong/wechat/mp/jssdk" + "github.com/silenceper/wechat/v2" + "github.com/silenceper/wechat/v2/cache" + "github.com/silenceper/wechat/v2/officialaccount" + offConfig "github.com/silenceper/wechat/v2/officialaccount/config" "github.com/zeromicro/go-zero/core/logx" ) @@ -28,15 +36,38 @@ func NewWxTicketLogic(ctx context.Context, svcCtx *svc.ServiceContext) WxTicketL } } -var ( - accessTokenServer core.AccessTokenServer - wechatClient *core.Client - cardTicketServer *jssdk.DefaultTicketServer -) +// wxOfficialAccount 按租户的 appid/secret 动态构建公众号实例。 +// 本系统为多租户模式:每次调用按 beid 查库得到不同的 appid/secret, +// 而 silenceper/wechat 的 Wechat 实例是固定配置,故每次调用按需新建实例, +// access_token/jsapi_ticket 由实例内部的内存 cache 承接;外层再用 svcCtx.Cache +// 缓存 ticket(见 WxTicket),管理面低频调用可接受,后续如调用量上升可增加实例池。 +func wxOfficialAccount(appid, secret string) *officialaccount.OfficialAccount { + wc := wechat.NewWechat() + return wc.GetOfficialAccount(&offConfig.Config{ + AppID: appid, + AppSecret: secret, + Cache: cache.NewMemory(), + }) +} + +// wxConfigSign 计算微信 JS-SDK wx.config 所需签名,官方算法为固定顺序拼接后取 sha1: +// signature = sha1("jsapi_ticket=...&noncestr=...×tamp=...&url=...") +// 注意: +// 1. url 不包含 '#' 及其后面部分(与原 chanxuehong 实现、官方文档规则一致); +// 2. 不能改用 silenceper 的 util.Signature——它会先对参数做字典序排序, +// 只适用于支付等排序签名场景,不符合 jsapi 签名的固定顺序要求。 +func wxConfigSign(jsapiTicket, nonceStr, timestamp, url string) string { + if i := strings.IndexByte(url, '#'); i >= 0 { + url = url[:i] + } + sum := sha1.Sum([]byte("jsapi_ticket=" + jsapiTicket + + "&noncestr=" + nonceStr + + "×tamp=" + timestamp + + "&url=" + url)) + return hex.EncodeToString(sum[:]) +} func (l *WxTicketLogic) WxTicket(req types.SnsReq) (*types.WxShareResp, error) { - // todo: add your logic here and delete this line - //= core.NewDefaultAccessTokenServer(wxAppId, wxAppSecret, nil) appconf, err := l.svcCtx.CommonRpc.GetAppConfig(l.ctx, &common.AppConfigReq{ Beid: req.Beid.Beid, Ptyid: req.Ptyid, @@ -44,38 +75,32 @@ func (l *WxTicketLogic) WxTicket(req types.SnsReq) (*types.WxShareResp, error) { if err != nil { return nil, err } - var ticketoken = "" - //如果有 - l.svcCtx.Cache.Get("ticket_token", &ticketoken) - if ticketoken == "" { - //拿到微信操作的 - accessTokenServer = core.NewDefaultAccessTokenServer(appconf.Appid, appconf.Appsecret, nil) - wechatClient = core.NewClient(accessTokenServer, nil) - - cardTicketServer = jssdk.NewDefaultTicketServer(wechatClient) - ticketoken, err = cardTicketServer.Ticket() + // jsapi_ticket 微信侧有效期 7200 秒,沿用原有逻辑在服务侧缓存 1 小时; + // 缓存 key 携带 appid,避免多租户之间互相覆盖(原实现为固定 key,存在串租户问题) + cacheKey := "ticket_token_" + appconf.Appid + var ticketoken string + l.svcCtx.Cache.Get(cacheKey, &ticketoken) + if ticketoken == "" { + // 缓存未命中:通过 silenceper 拉取 access_token 与 jsapi_ticket + oa := wxOfficialAccount(appconf.Appid, appconf.Appsecret) + accessToken, err := oa.GetAccessToken() + if err != nil { + return nil, err + } + ticketoken, err = oa.GetJs().GetTicket(accessToken) if err != nil { return nil, err } - l.svcCtx.Cache.SetWithExpire("ticket_token", ticketoken, time.Duration(3600)*time.Second) + l.svcCtx.Cache.SetWithExpire(cacheKey, ticketoken, time.Hour) } - // jsapiTicket := "sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg" - // nonceStr := "Wm3WZYTPz0wzccnW" - // timestamp := "1414587457" - // url := "http://mp.weixin.qq.com?params=value#xxxx" - - // wantSignature := "0f9de62fce790f9a083d5c99e95740ceb90c27ed" - - // haveSignature := WXConfigSign(jsapiTicket, nonceStr, timestamp, url) - wxticket := &types.WxShareResp{ Appid: appconf.Appid, Noncestr: shared.Noncestr, Timestamp: time.Now().Unix(), } - wxticket.Signature = jssdk.WXConfigSign(ticketoken, wxticket.Noncestr, shared.Int64ToStr(wxticket.Timestamp), req.BackUrl) + wxticket.Signature = wxConfigSign(ticketoken, wxticket.Noncestr, shared.Int64ToStr(wxticket.Timestamp), req.BackUrl) return wxticket, nil } diff --git a/internal/logic/common/wxticketlogic_test.go b/internal/logic/common/wxticketlogic_test.go new file mode 100644 index 0000000..b84d1d4 --- /dev/null +++ b/internal/logic/common/wxticketlogic_test.go @@ -0,0 +1,60 @@ +package logic + +import "testing" + +// TestWxConfigSign 用微信官方文档《JS-SDK 说明》附录一的签名示例做固定输入断言: +// https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html +// jsapi_ticket=..., noncestr=Wm3WZYTPz0wzccnW, timestamp=1414587457, +// url=http://mp.weixin.qq.com?params=value(不含 # 及其后部分) +// 期望签名 = 0f9de62fce790f9a083d5c99e95740ceb90c27ed +func TestWxConfigSign(t *testing.T) { + const ( + ticket = "sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg" + nonceStr = "Wm3WZYTPz0wzccnW" + timestamp = "1414587457" + want = "0f9de62fce790f9a083d5c99e95740ceb90c27ed" + ) + cases := []struct { + name string + url string + }{ + {"官方文档URL", "http://mp.weixin.qq.com?params=value"}, + {"URL含#片段应被截断", "http://mp.weixin.qq.com?params=value#xxxx"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := wxConfigSign(ticket, nonceStr, timestamp, c.url) + if got != want { + t.Fatalf("wxConfigSign(url=%q) = %q, want %q", c.url, got, want) + } + }) + } +} + +// TestWxConfigSignSensitive 保证签名对各输入敏感且输出为 40 位十六进制 sha1, +// 防止误用"先排序再拼接"的签名实现(如 silenceper 的 util.Signature) +func TestWxConfigSignSensitive(t *testing.T) { + base := wxConfigSign("ticket-1", "nonce", "123", "https://example.com/a") + if len(base) != 40 { + t.Fatalf("签名长度 = %d, want 40(sha1 十六进制)", len(base)) + } + variants := []struct { + name string + sign string + }{ + {"ticket不同", wxConfigSign("ticket-2", "nonce", "123", "https://example.com/a")}, + {"noncestr不同", wxConfigSign("ticket-1", "nonce2", "123", "https://example.com/a")}, + {"timestamp不同", wxConfigSign("ticket-1", "nonce", "456", "https://example.com/a")}, + {"url不同", wxConfigSign("ticket-1", "nonce", "123", "https://example.com/b")}, + } + for _, v := range variants { + if v.sign == base { + t.Fatalf("%s 时签名不应与基准相同", v.name) + } + } + // 与手工计算的 sha1 拼接结果逐字符比对 + manual := wxConfigSign("t", "n", "1", "u") + if manual != wxConfigSign("t", "n", "1", "u#frag") { + t.Fatalf("含 # 片段的 url 与截断后 url 的签名应一致") + } +} diff --git a/user/rpc/internal/logic/snsloginlogic.go b/user/rpc/internal/logic/snsloginlogic.go index 9413b09..416a569 100644 --- a/user/rpc/internal/logic/snsloginlogic.go +++ b/user/rpc/internal/logic/snsloginlogic.go @@ -1,3 +1,6 @@ +// TODO(回归): 需真实公众号凭据验证 OAuth 与 JS-SDK 签名 +// +// 本文件已从 chanxuehong/wechat(已停更)迁移至 silenceper/wechat/v2。 package logic import ( @@ -8,8 +11,9 @@ import ( "datacenter/user/rpc/internal/svc" "datacenter/user/rpc/user" - mpoauth2 "github.com/chanxuehong/wechat/mp/oauth2" - "github.com/chanxuehong/wechat/oauth2" + "github.com/silenceper/wechat/v2" + "github.com/silenceper/wechat/v2/cache" + offConfig "github.com/silenceper/wechat/v2/officialaccount/config" "github.com/zeromicro/go-zero/core/logx" ) @@ -28,48 +32,53 @@ func NewSnsLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SnsLogin } func (l *SnsLoginLogic) SnsLogin(in *user.AppConfigReq) (apr *user.AppUserResp, err error) { - //先取配置文件信息 reply, err := l.svcCtx.CommonRpc.GetAppConfig(l.ctx, &commonclient.AppConfigReq{Beid: in.Beid, Ptyid: in.Ptyid}) if err != nil { return } - //调用微信第三方登陆 - //mpoauth2.GetSessionWithClient() - oauth2Endpoint := mpoauth2.NewEndpoint(reply.Appid, reply.Appsecret) - oauth2Client := oauth2.Client{ - Endpoint: oauth2Endpoint, - } - token, err := oauth2Client.ExchangeToken(in.Code) + //多租户动态 appid/secret 模式:每次调用按 beid 查库得到不同的 appid/secret, + //而 silenceper/wechat 的 Wechat 实例是固定配置,故每次按需新建实例(网页授权 + //access_token 由微信按 code 下发,无需实例级缓存),登录为低频调用可接受, + //后续如调用量上升可增加实例池 + wc := wechat.NewWechat() + oa := wc.GetOfficialAccount(&offConfig.Config{ + AppID: reply.Appid, + AppSecret: reply.Appsecret, + Cache: cache.NewMemory(), + }) + + //调用微信第三方登陆:通过网页授权 code 换取用户 access_token(区别于接口调用凭证) + token, err := oa.GetOauth().GetUserAccessToken(in.Code) if err != nil { return } // 检查 用户是否已存在 - appuser, err := l.svcCtx.AppUserModel.FindOneByOpenid(in.Beid, in.Ptyid, token.OpenId) + appuser, err := l.svcCtx.AppUserModel.FindOneByOpenid(in.Beid, in.Ptyid, token.OpenID) if err != nil && err != model.ErrNotFound { - // logx.Info("appuser==xxxxx=====xxxxxx", appuser, err) return } if err == model.ErrNotFound { - userinfo, errox := mpoauth2.GetUserInfo(token.AccessToken, token.OpenId, "", nil) + //scope=snsapi_userinfo 时可拉取用户资料;lang 传空由 SDK 默认使用 zh_CN + userinfo, errox := oa.GetOauth().GetUserInfo(token.AccessToken, token.OpenID, "") if errox != nil { - // 处理一般错误信息 - return + //原实现此处返回 model.ErrNotFound 会掩盖真实错误,改为透出微信侧错误 + return nil, errox } appuser = &model.AppUser{ Ptyid: in.Ptyid, Beid: in.Beid, - Openid: userinfo.OpenId, + Openid: userinfo.OpenID, Nickname: userinfo.Nickname, - UnionId: userinfo.UnionId, - Avator: userinfo.HeadImageURL, + UnionId: userinfo.Unionid, + Avator: userinfo.HeadImgURL, Province: userinfo.Province, Sex: int64(userinfo.Sex), City: userinfo.City, - Country: userinfo.City, + Country: userinfo.Country, //原实现误填 userinfo.City,此处修正 } res, errox := l.svcCtx.AppUserModel.Insert(*appuser) if errox != nil { @@ -91,6 +100,6 @@ func (l *SnsLoginLogic) SnsLogin(in *user.AppConfigReq) (apr *user.AppUserResp, Province: appuser.Province, Sex: appuser.Sex, City: appuser.City, - Country: appuser.City, + Country: appuser.Country, //原实现误填 appuser.City,与入库侧一并修正 }, nil } From c5f0d54836d4d147606ccc23543fe00d86764649 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 13:51:58 +0800 Subject: [PATCH 08/12] =?UTF-8?q?Phase=203/4=20=E9=AA=A8=E6=9E=B6=EF=BC=9A?= =?UTF-8?q?job(=E8=B0=83=E5=BA=A6=E9=9B=86=E6=88=90)/ide(SQL=20IDE)/ai(AI?= =?UTF-8?q?=20=E9=97=AE=E6=95=B0)=20=E4=B8=89=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3 个 proto 契约 + goctl 骨架 + config(ai 含可配置 LlmConf) - docs/schema/phase3.sql:dc_sync_task/dc_ide_script/dc_ask_log 三表 - 端口 :8092/:8093/:8094 --- .gitignore | 3 + ai/rpc/ai.go | 39 + ai/rpc/ai.proto | 32 + ai/rpc/ai/ai.pb.go | 316 +++++++ ai/rpc/ai/ai_grpc.pb.go | 123 +++ ai/rpc/aiClient/ai.go | 41 + ai/rpc/etc/ai.example.yaml | 13 + ai/rpc/internal/config/config.go | 16 + ai/rpc/internal/logic/askLogic.go | 31 + ai/rpc/internal/server/aiServer.go | 30 + ai/rpc/internal/svc/serviceContext.go | 13 + docs/schema/phase3.sql | 54 ++ ide/rpc/etc/ide.example.yaml | 8 + ide/rpc/ide.go | 39 + ide/rpc/ide.proto | 63 ++ ide/rpc/ide/ide.pb.go | 680 ++++++++++++++ ide/rpc/ide/ide_grpc.pb.go | 283 ++++++ ide/rpc/ideClient/ide.go | 78 ++ ide/rpc/internal/config/config.go | 10 + ide/rpc/internal/logic/deleteScriptLogic.go | 31 + ide/rpc/internal/logic/getScriptLogic.go | 31 + ide/rpc/internal/logic/listScriptsLogic.go | 31 + ide/rpc/internal/logic/runScriptLogic.go | 31 + ide/rpc/internal/logic/saveScriptLogic.go | 31 + ide/rpc/internal/server/ideServer.go | 54 ++ ide/rpc/internal/svc/serviceContext.go | 13 + job/rpc/etc/job.example.yaml | 8 + job/rpc/internal/config/config.go | 10 + job/rpc/internal/logic/createSyncTaskLogic.go | 31 + job/rpc/internal/logic/deleteSyncTaskLogic.go | 31 + .../internal/logic/deployToSchedulerLogic.go | 31 + .../internal/logic/getSchedulerInfoLogic.go | 31 + job/rpc/internal/logic/getTaskStatusLogic.go | 31 + job/rpc/internal/logic/listSyncTasksLogic.go | 31 + job/rpc/internal/logic/triggerTaskLogic.go | 31 + job/rpc/internal/server/jobServer.go | 66 ++ job/rpc/internal/svc/serviceContext.go | 13 + job/rpc/job.go | 39 + job/rpc/job.proto | 76 ++ job/rpc/job/job.pb.go | 853 ++++++++++++++++++ job/rpc/job/job_grpc.pb.go | 363 ++++++++ job/rpc/jobClient/job.go | 96 ++ 42 files changed, 3835 insertions(+) create mode 100644 ai/rpc/ai.go create mode 100644 ai/rpc/ai.proto create mode 100644 ai/rpc/ai/ai.pb.go create mode 100644 ai/rpc/ai/ai_grpc.pb.go create mode 100644 ai/rpc/aiClient/ai.go create mode 100644 ai/rpc/etc/ai.example.yaml create mode 100755 ai/rpc/internal/config/config.go create mode 100644 ai/rpc/internal/logic/askLogic.go create mode 100644 ai/rpc/internal/server/aiServer.go create mode 100644 ai/rpc/internal/svc/serviceContext.go create mode 100644 docs/schema/phase3.sql create mode 100644 ide/rpc/etc/ide.example.yaml create mode 100644 ide/rpc/ide.go create mode 100644 ide/rpc/ide.proto create mode 100644 ide/rpc/ide/ide.pb.go create mode 100644 ide/rpc/ide/ide_grpc.pb.go create mode 100644 ide/rpc/ideClient/ide.go create mode 100755 ide/rpc/internal/config/config.go create mode 100644 ide/rpc/internal/logic/deleteScriptLogic.go create mode 100644 ide/rpc/internal/logic/getScriptLogic.go create mode 100644 ide/rpc/internal/logic/listScriptsLogic.go create mode 100644 ide/rpc/internal/logic/runScriptLogic.go create mode 100644 ide/rpc/internal/logic/saveScriptLogic.go create mode 100644 ide/rpc/internal/server/ideServer.go create mode 100644 ide/rpc/internal/svc/serviceContext.go create mode 100644 job/rpc/etc/job.example.yaml create mode 100755 job/rpc/internal/config/config.go create mode 100644 job/rpc/internal/logic/createSyncTaskLogic.go create mode 100644 job/rpc/internal/logic/deleteSyncTaskLogic.go create mode 100644 job/rpc/internal/logic/deployToSchedulerLogic.go create mode 100644 job/rpc/internal/logic/getSchedulerInfoLogic.go create mode 100644 job/rpc/internal/logic/getTaskStatusLogic.go create mode 100644 job/rpc/internal/logic/listSyncTasksLogic.go create mode 100644 job/rpc/internal/logic/triggerTaskLogic.go create mode 100644 job/rpc/internal/server/jobServer.go create mode 100644 job/rpc/internal/svc/serviceContext.go create mode 100644 job/rpc/job.go create mode 100644 job/rpc/job.proto create mode 100644 job/rpc/job/job.pb.go create mode 100644 job/rpc/job/job_grpc.pb.go create mode 100644 job/rpc/jobClient/job.go diff --git a/.gitignore b/.gitignore index e385ea7..acf63cd 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ catalog/rpc/etc/catalog.yaml quality/rpc/etc/quality.yaml metric/rpc/etc/metric.yaml tag/rpc/etc/tag.yaml +job/rpc/etc/job.yaml +ide/rpc/etc/ide.yaml +ai/rpc/etc/ai.yaml diff --git a/ai/rpc/ai.go b/ai/rpc/ai.go new file mode 100644 index 0000000..224afd0 --- /dev/null +++ b/ai/rpc/ai.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/ai/rpc/ai" + "datacenter/ai/rpc/internal/config" + "datacenter/ai/rpc/internal/server" + "datacenter/ai/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/ai.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + ai.RegisterAiServer(grpcServer, server.NewAiServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/ai/rpc/ai.proto b/ai/rpc/ai.proto new file mode 100644 index 0000000..243d422 --- /dev/null +++ b/ai/rpc/ai.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package ai; + +option go_package = "./ai"; + +//语义候选(元数据实体或指标) +message Candidate { + string type = 1; //entity/metric + int64 id = 2; + string name = 3; + string detail = 4; //entity=fqn / metric=expr + string comment = 5; +} + +message AskReq { + string question = 1; + int64 beid = 2; + int32 max_candidates = 3; //语义检索返回上限,默认 5 +} +message AskReply { + repeated Candidate candidates = 1; + bool llm_enabled = 2; //是否启用了 LLM + string generated_sql = 3; //LLM 生成的 SQL(未启用为空) + string answer = 4; //回复(未启用 LLM 时为检索摘要) + int64 cost_ms = 5; +} + +service Ai { + //AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 + rpc Ask(AskReq) returns (AskReply); +} diff --git a/ai/rpc/ai/ai.pb.go b/ai/rpc/ai/ai.pb.go new file mode 100644 index 0000000..0c525e8 --- /dev/null +++ b/ai/rpc/ai/ai.pb.go @@ -0,0 +1,316 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: ai.proto + +package ai + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// 语义候选(元数据实体或指标) +type Candidate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` //entity/metric + Id int64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Detail string `protobuf:"bytes,4,opt,name=detail,proto3" json:"detail,omitempty"` //entity=fqn / metric=expr + Comment string `protobuf:"bytes,5,opt,name=comment,proto3" json:"comment,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Candidate) Reset() { + *x = Candidate{} + mi := &file_ai_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Candidate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Candidate) ProtoMessage() {} + +func (x *Candidate) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Candidate.ProtoReflect.Descriptor instead. +func (*Candidate) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{0} +} + +func (x *Candidate) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Candidate) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Candidate) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Candidate) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +func (x *Candidate) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +type AskReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Question string `protobuf:"bytes,1,opt,name=question,proto3" json:"question,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + MaxCandidates int32 `protobuf:"varint,3,opt,name=max_candidates,json=maxCandidates,proto3" json:"max_candidates,omitempty"` //语义检索返回上限,默认 5 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AskReq) Reset() { + *x = AskReq{} + mi := &file_ai_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AskReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AskReq) ProtoMessage() {} + +func (x *AskReq) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AskReq.ProtoReflect.Descriptor instead. +func (*AskReq) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{1} +} + +func (x *AskReq) GetQuestion() string { + if x != nil { + return x.Question + } + return "" +} + +func (x *AskReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *AskReq) GetMaxCandidates() int32 { + if x != nil { + return x.MaxCandidates + } + return 0 +} + +type AskReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Candidates []*Candidate `protobuf:"bytes,1,rep,name=candidates,proto3" json:"candidates,omitempty"` + LlmEnabled bool `protobuf:"varint,2,opt,name=llm_enabled,json=llmEnabled,proto3" json:"llm_enabled,omitempty"` //是否启用了 LLM + GeneratedSql string `protobuf:"bytes,3,opt,name=generated_sql,json=generatedSql,proto3" json:"generated_sql,omitempty"` //LLM 生成的 SQL(未启用为空) + Answer string `protobuf:"bytes,4,opt,name=answer,proto3" json:"answer,omitempty"` //回复(未启用 LLM 时为检索摘要) + CostMs int64 `protobuf:"varint,5,opt,name=cost_ms,json=costMs,proto3" json:"cost_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AskReply) Reset() { + *x = AskReply{} + mi := &file_ai_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AskReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AskReply) ProtoMessage() {} + +func (x *AskReply) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AskReply.ProtoReflect.Descriptor instead. +func (*AskReply) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{2} +} + +func (x *AskReply) GetCandidates() []*Candidate { + if x != nil { + return x.Candidates + } + return nil +} + +func (x *AskReply) GetLlmEnabled() bool { + if x != nil { + return x.LlmEnabled + } + return false +} + +func (x *AskReply) GetGeneratedSql() string { + if x != nil { + return x.GeneratedSql + } + return "" +} + +func (x *AskReply) GetAnswer() string { + if x != nil { + return x.Answer + } + return "" +} + +func (x *AskReply) GetCostMs() int64 { + if x != nil { + return x.CostMs + } + return 0 +} + +var File_ai_proto protoreflect.FileDescriptor + +const file_ai_proto_rawDesc = "" + + "\n" + + "\bai.proto\x12\x02ai\"u\n" + + "\tCandidate\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x0e\n" + + "\x02id\x18\x02 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x16\n" + + "\x06detail\x18\x04 \x01(\tR\x06detail\x12\x18\n" + + "\acomment\x18\x05 \x01(\tR\acomment\"_\n" + + "\x06AskReq\x12\x1a\n" + + "\bquestion\x18\x01 \x01(\tR\bquestion\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\x12%\n" + + "\x0emax_candidates\x18\x03 \x01(\x05R\rmaxCandidates\"\xb0\x01\n" + + "\bAskReply\x12-\n" + + "\n" + + "candidates\x18\x01 \x03(\v2\r.ai.CandidateR\n" + + "candidates\x12\x1f\n" + + "\vllm_enabled\x18\x02 \x01(\bR\n" + + "llmEnabled\x12#\n" + + "\rgenerated_sql\x18\x03 \x01(\tR\fgeneratedSql\x12\x16\n" + + "\x06answer\x18\x04 \x01(\tR\x06answer\x12\x17\n" + + "\acost_ms\x18\x05 \x01(\x03R\x06costMs2%\n" + + "\x02Ai\x12\x1f\n" + + "\x03Ask\x12\n" + + ".ai.AskReq\x1a\f.ai.AskReplyB\x06Z\x04./aib\x06proto3" + +var ( + file_ai_proto_rawDescOnce sync.Once + file_ai_proto_rawDescData []byte +) + +func file_ai_proto_rawDescGZIP() []byte { + file_ai_proto_rawDescOnce.Do(func() { + file_ai_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ai_proto_rawDesc), len(file_ai_proto_rawDesc))) + }) + return file_ai_proto_rawDescData +} + +var file_ai_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ai_proto_goTypes = []any{ + (*Candidate)(nil), // 0: ai.Candidate + (*AskReq)(nil), // 1: ai.AskReq + (*AskReply)(nil), // 2: ai.AskReply +} +var file_ai_proto_depIdxs = []int32{ + 0, // 0: ai.AskReply.candidates:type_name -> ai.Candidate + 1, // 1: ai.Ai.Ask:input_type -> ai.AskReq + 2, // 2: ai.Ai.Ask:output_type -> ai.AskReply + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_ai_proto_init() } +func file_ai_proto_init() { + if File_ai_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ai_proto_rawDesc), len(file_ai_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ai_proto_goTypes, + DependencyIndexes: file_ai_proto_depIdxs, + MessageInfos: file_ai_proto_msgTypes, + }.Build() + File_ai_proto = out.File + file_ai_proto_goTypes = nil + file_ai_proto_depIdxs = nil +} diff --git a/ai/rpc/ai/ai_grpc.pb.go b/ai/rpc/ai/ai_grpc.pb.go new file mode 100644 index 0000000..6149108 --- /dev/null +++ b/ai/rpc/ai/ai_grpc.pb.go @@ -0,0 +1,123 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: ai.proto + +package ai + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Ai_Ask_FullMethodName = "/ai.Ai/Ask" +) + +// AiClient is the client API for Ai service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AiClient interface { + //AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 + Ask(ctx context.Context, in *AskReq, opts ...grpc.CallOption) (*AskReply, error) +} + +type aiClient struct { + cc grpc.ClientConnInterface +} + +func NewAiClient(cc grpc.ClientConnInterface) AiClient { + return &aiClient{cc} +} + +func (c *aiClient) Ask(ctx context.Context, in *AskReq, opts ...grpc.CallOption) (*AskReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AskReply) + err := c.cc.Invoke(ctx, Ai_Ask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AiServer is the server API for Ai service. +// All implementations must embed UnimplementedAiServer +// for forward compatibility. +type AiServer interface { + //AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 + Ask(context.Context, *AskReq) (*AskReply, error) + mustEmbedUnimplementedAiServer() +} + +// UnimplementedAiServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAiServer struct{} + +func (UnimplementedAiServer) Ask(context.Context, *AskReq) (*AskReply, error) { + return nil, status.Error(codes.Unimplemented, "method Ask not implemented") +} +func (UnimplementedAiServer) mustEmbedUnimplementedAiServer() {} +func (UnimplementedAiServer) testEmbeddedByValue() {} + +// UnsafeAiServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AiServer will +// result in compilation errors. +type UnsafeAiServer interface { + mustEmbedUnimplementedAiServer() +} + +func RegisterAiServer(s grpc.ServiceRegistrar, srv AiServer) { + // If the following call panics, it indicates UnimplementedAiServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Ai_ServiceDesc, srv) +} + +func _Ai_Ask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AskReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AiServer).Ask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ai_Ask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AiServer).Ask(ctx, req.(*AskReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Ai_ServiceDesc is the grpc.ServiceDesc for Ai service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Ai_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ai.Ai", + HandlerType: (*AiServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ask", + Handler: _Ai_Ask_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ai.proto", +} diff --git a/ai/rpc/aiClient/ai.go b/ai/rpc/aiClient/ai.go new file mode 100644 index 0000000..2201918 --- /dev/null +++ b/ai/rpc/aiClient/ai.go @@ -0,0 +1,41 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: ai.proto + +package aiClient + +import ( + "context" + + "datacenter/ai/rpc/ai" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + AskReply = ai.AskReply + AskReq = ai.AskReq + Candidate = ai.Candidate + + Ai interface { + // AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 + Ask(ctx context.Context, in *AskReq, opts ...grpc.CallOption) (*AskReply, error) + } + + defaultAi struct { + cli zrpc.Client + } +) + +func NewAi(cli zrpc.Client) Ai { + return &defaultAi{ + cli: cli, + } +} + +// AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 +func (m *defaultAi) Ask(ctx context.Context, in *AskReq, opts ...grpc.CallOption) (*AskReply, error) { + client := ai.NewAiClient(m.cli.Conn()) + return client.Ask(ctx, in, opts...) +} diff --git a/ai/rpc/etc/ai.example.yaml b/ai/rpc/etc/ai.example.yaml new file mode 100644 index 0000000..ffa0c8b --- /dev/null +++ b/ai/rpc/etc/ai.example.yaml @@ -0,0 +1,13 @@ +Name: ai.rpc +ListenOn: 0.0.0.0:8094 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: ai.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +Llm: + Enabled: false + BaseUrl: https://api.openai.com/v1 + ApiKey: sk-xxxx + Model: gpt-4o-mini diff --git a/ai/rpc/internal/config/config.go b/ai/rpc/internal/config/config.go new file mode 100755 index 0000000..4d5965e --- /dev/null +++ b/ai/rpc/internal/config/config.go @@ -0,0 +1,16 @@ +package config + +import "github.com/zeromicro/go-zero/zrpc" + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } + Llm struct { + Enabled bool `json:",default=false"` + BaseUrl string `json:",optional"` //OpenAI 兼容接口,如 https://api.openai.com/v1 + ApiKey string `json:",optional"` + Model string `json:",default=gpt-4o-mini"` + } +} diff --git a/ai/rpc/internal/logic/askLogic.go b/ai/rpc/internal/logic/askLogic.go new file mode 100644 index 0000000..3385cfa --- /dev/null +++ b/ai/rpc/internal/logic/askLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/ai/rpc/ai" + "datacenter/ai/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AskLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewAskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AskLogic { + return &AskLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 +func (l *AskLogic) Ask(in *ai.AskReq) (*ai.AskReply, error) { + // todo: add your logic here and delete this line + + return &ai.AskReply{}, nil +} diff --git a/ai/rpc/internal/server/aiServer.go b/ai/rpc/internal/server/aiServer.go new file mode 100644 index 0000000..ae8c6ff --- /dev/null +++ b/ai/rpc/internal/server/aiServer.go @@ -0,0 +1,30 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: ai.proto + +package server + +import ( + "context" + + "datacenter/ai/rpc/ai" + "datacenter/ai/rpc/internal/logic" + "datacenter/ai/rpc/internal/svc" +) + +type AiServer struct { + svcCtx *svc.ServiceContext + ai.UnimplementedAiServer +} + +func NewAiServer(svcCtx *svc.ServiceContext) *AiServer { + return &AiServer{ + svcCtx: svcCtx, + } +} + +// AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 +func (s *AiServer) Ask(ctx context.Context, in *ai.AskReq) (*ai.AskReply, error) { + l := logic.NewAskLogic(ctx, s.svcCtx) + return l.Ask(in) +} diff --git a/ai/rpc/internal/svc/serviceContext.go b/ai/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..8d075cb --- /dev/null +++ b/ai/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/ai/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/docs/schema/phase3.sql b/docs/schema/phase3.sql new file mode 100644 index 0000000..e19a463 --- /dev/null +++ b/docs/schema/phase3.sql @@ -0,0 +1,54 @@ +-- ============================================================ +-- Phase 3/4 数据中台表(调度集成 / SQL IDE / AI 问数审计) +-- 目标库: datacenter 字符集: utf8mb4 引擎: InnoDB +-- ============================================================ + +-- 同步任务表(管控面,执行面交 DolphinScheduler/SeaTunnel) +CREATE TABLE IF NOT EXISTS `dc_sync_task` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL COMMENT '任务名', + `source_ds_id` bigint NOT NULL COMMENT '源数据源id', + `target_ds_id` bigint NOT NULL DEFAULT 0 COMMENT '目标数据源id(0=目标为平台自有库)', + `mode` varchar(16) NOT NULL DEFAULT 'offline' COMMENT 'offline/cdc', + `mapping_json` text COMMENT '字段/表映射(JSON)', + `cron` varchar(64) NOT NULL DEFAULT '' COMMENT '调度周期(推送 DS 时使用)', + `ds_workflow_code` int NOT NULL DEFAULT 0 COMMENT 'DolphinScheduler 工作流编码(部署后回填)', + `status` varchar(16) NOT NULL DEFAULT 'draft' COMMENT 'draft/deployed/running/failed', + `last_run_at` datetime(3) DEFAULT NULL, + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid_status` (`beid`,`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='同步任务表'; + +-- SQL 脚本表(IDE) +CREATE TABLE IF NOT EXISTS `dc_ide_script` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL COMMENT '脚本名', + `sql_text` mediumtext NOT NULL COMMENT 'SQL 内容', + `version` int NOT NULL DEFAULT 1 COMMENT '版本号,每次保存+1', + `datasource_id` bigint NOT NULL DEFAULT 0 COMMENT '执行目标数据源', + `db_name` varchar(128) NOT NULL DEFAULT '' COMMENT '目标库', + `last_run_at` datetime(3) DEFAULT NULL, + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid` (`beid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='SQL 脚本表'; + +-- AI 问数审计表 +CREATE TABLE IF NOT EXISTS `dc_ask_log` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `question` varchar(1024) NOT NULL COMMENT '用户问题', + `candidates_json` text COMMENT '语义检索命中的候选(json)', + `generated_sql` text COMMENT 'LLM 生成的 SQL(未配置 LLM 时为空)', + `answer` text COMMENT '回复内容', + `llm_enabled` tinyint NOT NULL DEFAULT 0 COMMENT '本次是否启用了 LLM', + `cost_ms` int NOT NULL DEFAULT 0, + `beid` bigint NOT NULL DEFAULT 0, + `created_at` datetime(3) DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_beid` (`beid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI 问数审计表'; diff --git a/ide/rpc/etc/ide.example.yaml b/ide/rpc/etc/ide.example.yaml new file mode 100644 index 0000000..74e42fa --- /dev/null +++ b/ide/rpc/etc/ide.example.yaml @@ -0,0 +1,8 @@ +Name: ide.rpc +ListenOn: 0.0.0.0:8093 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: ide.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/ide/rpc/ide.go b/ide/rpc/ide.go new file mode 100644 index 0000000..4d5b6d7 --- /dev/null +++ b/ide/rpc/ide.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/ide/rpc/ide" + "datacenter/ide/rpc/internal/config" + "datacenter/ide/rpc/internal/server" + "datacenter/ide/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/ide.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + ide.RegisterIdeServer(grpcServer, server.NewIdeServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/ide/rpc/ide.proto b/ide/rpc/ide.proto new file mode 100644 index 0000000..48dfffb --- /dev/null +++ b/ide/rpc/ide.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package ide; + +option go_package = "./ide"; + +message Script { + int64 id = 1; + string name = 2; + string sql_text = 3; + int32 version = 4; //每次保存+1 + int64 datasource_id = 5; + string db_name = 6; + int64 last_run_at = 7; + int64 beid = 8; +} +message ScriptReply { Script script = 1; } +message ScriptListReply { + repeated Script list = 1; + int64 total = 2; +} + +message SaveScriptReq { + int64 id = 1; //0=新建,>0=更新(version+1) + string name = 2; + string sql_text = 3; + int64 datasource_id = 4; + string db_name = 5; + int64 beid = 6; +} +message ScriptListReq { + int64 beid = 1; + string keyword = 2; + int64 page = 3; + int64 size = 4; +} +message IdReq { int64 id = 1; int64 beid = 2; } + +message RunScriptReq { + int64 id = 1; + int64 beid = 2; + int32 max_rows = 3; //默认 100,保护性截断 +} +message RunScriptReply { + string columns_json = 1; + string rows_json = 2; + int64 row_count = 3; + int64 cost_ms = 4; + bool truncated = 5; +} + +service Ide { + //保存脚本(新建或更新,更新自动 version+1) + rpc SaveScript(SaveScriptReq) returns (ScriptReply); + //脚本列表 + rpc ListScripts(ScriptListReq) returns (ScriptListReply); + //脚本详情 + rpc GetScript(IdReq) returns (ScriptReply); + //删除脚本 + rpc DeleteScript(IdReq) returns (IdReq); + //执行脚本(开发调试:仅 SELECT,行数保护截断) + rpc RunScript(RunScriptReq) returns (RunScriptReply); +} diff --git a/ide/rpc/ide/ide.pb.go b/ide/rpc/ide/ide.pb.go new file mode 100644 index 0000000..c17913a --- /dev/null +++ b/ide/rpc/ide/ide.pb.go @@ -0,0 +1,680 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: ide.proto + +package ide + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Script struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + SqlText string `protobuf:"bytes,3,opt,name=sql_text,json=sqlText,proto3" json:"sql_text,omitempty"` + Version int32 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"` //每次保存+1 + DatasourceId int64 `protobuf:"varint,5,opt,name=datasource_id,json=datasourceId,proto3" json:"datasource_id,omitempty"` + DbName string `protobuf:"bytes,6,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + LastRunAt int64 `protobuf:"varint,7,opt,name=last_run_at,json=lastRunAt,proto3" json:"last_run_at,omitempty"` + Beid int64 `protobuf:"varint,8,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Script) Reset() { + *x = Script{} + mi := &file_ide_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Script) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Script) ProtoMessage() {} + +func (x *Script) ProtoReflect() protoreflect.Message { + mi := &file_ide_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Script.ProtoReflect.Descriptor instead. +func (*Script) Descriptor() ([]byte, []int) { + return file_ide_proto_rawDescGZIP(), []int{0} +} + +func (x *Script) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Script) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Script) GetSqlText() string { + if x != nil { + return x.SqlText + } + return "" +} + +func (x *Script) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Script) GetDatasourceId() int64 { + if x != nil { + return x.DatasourceId + } + return 0 +} + +func (x *Script) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *Script) GetLastRunAt() int64 { + if x != nil { + return x.LastRunAt + } + return 0 +} + +func (x *Script) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ScriptReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Script *Script `protobuf:"bytes,1,opt,name=script,proto3" json:"script,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScriptReply) Reset() { + *x = ScriptReply{} + mi := &file_ide_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScriptReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScriptReply) ProtoMessage() {} + +func (x *ScriptReply) ProtoReflect() protoreflect.Message { + mi := &file_ide_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScriptReply.ProtoReflect.Descriptor instead. +func (*ScriptReply) Descriptor() ([]byte, []int) { + return file_ide_proto_rawDescGZIP(), []int{1} +} + +func (x *ScriptReply) GetScript() *Script { + if x != nil { + return x.Script + } + return nil +} + +type ScriptListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*Script `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScriptListReply) Reset() { + *x = ScriptListReply{} + mi := &file_ide_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScriptListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScriptListReply) ProtoMessage() {} + +func (x *ScriptListReply) ProtoReflect() protoreflect.Message { + mi := &file_ide_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScriptListReply.ProtoReflect.Descriptor instead. +func (*ScriptListReply) Descriptor() ([]byte, []int) { + return file_ide_proto_rawDescGZIP(), []int{2} +} + +func (x *ScriptListReply) GetList() []*Script { + if x != nil { + return x.List + } + return nil +} + +func (x *ScriptListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type SaveScriptReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` //0=新建,>0=更新(version+1) + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + SqlText string `protobuf:"bytes,3,opt,name=sql_text,json=sqlText,proto3" json:"sql_text,omitempty"` + DatasourceId int64 `protobuf:"varint,4,opt,name=datasource_id,json=datasourceId,proto3" json:"datasource_id,omitempty"` + DbName string `protobuf:"bytes,5,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"` + Beid int64 `protobuf:"varint,6,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SaveScriptReq) Reset() { + *x = SaveScriptReq{} + mi := &file_ide_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SaveScriptReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SaveScriptReq) ProtoMessage() {} + +func (x *SaveScriptReq) ProtoReflect() protoreflect.Message { + mi := &file_ide_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SaveScriptReq.ProtoReflect.Descriptor instead. +func (*SaveScriptReq) Descriptor() ([]byte, []int) { + return file_ide_proto_rawDescGZIP(), []int{3} +} + +func (x *SaveScriptReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *SaveScriptReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SaveScriptReq) GetSqlText() string { + if x != nil { + return x.SqlText + } + return "" +} + +func (x *SaveScriptReq) GetDatasourceId() int64 { + if x != nil { + return x.DatasourceId + } + return 0 +} + +func (x *SaveScriptReq) GetDbName() string { + if x != nil { + return x.DbName + } + return "" +} + +func (x *SaveScriptReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type ScriptListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + Keyword string `protobuf:"bytes,2,opt,name=keyword,proto3" json:"keyword,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScriptListReq) Reset() { + *x = ScriptListReq{} + mi := &file_ide_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScriptListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScriptListReq) ProtoMessage() {} + +func (x *ScriptListReq) ProtoReflect() protoreflect.Message { + mi := &file_ide_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScriptListReq.ProtoReflect.Descriptor instead. +func (*ScriptListReq) Descriptor() ([]byte, []int) { + return file_ide_proto_rawDescGZIP(), []int{4} +} + +func (x *ScriptListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *ScriptListReq) GetKeyword() string { + if x != nil { + return x.Keyword + } + return "" +} + +func (x *ScriptListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ScriptListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_ide_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_ide_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_ide_proto_rawDescGZIP(), []int{5} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type RunScriptReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + MaxRows int32 `protobuf:"varint,3,opt,name=max_rows,json=maxRows,proto3" json:"max_rows,omitempty"` //默认 100,保护性截断 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunScriptReq) Reset() { + *x = RunScriptReq{} + mi := &file_ide_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunScriptReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunScriptReq) ProtoMessage() {} + +func (x *RunScriptReq) ProtoReflect() protoreflect.Message { + mi := &file_ide_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunScriptReq.ProtoReflect.Descriptor instead. +func (*RunScriptReq) Descriptor() ([]byte, []int) { + return file_ide_proto_rawDescGZIP(), []int{6} +} + +func (x *RunScriptReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *RunScriptReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *RunScriptReq) GetMaxRows() int32 { + if x != nil { + return x.MaxRows + } + return 0 +} + +type RunScriptReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + ColumnsJson string `protobuf:"bytes,1,opt,name=columns_json,json=columnsJson,proto3" json:"columns_json,omitempty"` + RowsJson string `protobuf:"bytes,2,opt,name=rows_json,json=rowsJson,proto3" json:"rows_json,omitempty"` + RowCount int64 `protobuf:"varint,3,opt,name=row_count,json=rowCount,proto3" json:"row_count,omitempty"` + CostMs int64 `protobuf:"varint,4,opt,name=cost_ms,json=costMs,proto3" json:"cost_ms,omitempty"` + Truncated bool `protobuf:"varint,5,opt,name=truncated,proto3" json:"truncated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RunScriptReply) Reset() { + *x = RunScriptReply{} + mi := &file_ide_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RunScriptReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunScriptReply) ProtoMessage() {} + +func (x *RunScriptReply) ProtoReflect() protoreflect.Message { + mi := &file_ide_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunScriptReply.ProtoReflect.Descriptor instead. +func (*RunScriptReply) Descriptor() ([]byte, []int) { + return file_ide_proto_rawDescGZIP(), []int{7} +} + +func (x *RunScriptReply) GetColumnsJson() string { + if x != nil { + return x.ColumnsJson + } + return "" +} + +func (x *RunScriptReply) GetRowsJson() string { + if x != nil { + return x.RowsJson + } + return "" +} + +func (x *RunScriptReply) GetRowCount() int64 { + if x != nil { + return x.RowCount + } + return 0 +} + +func (x *RunScriptReply) GetCostMs() int64 { + if x != nil { + return x.CostMs + } + return 0 +} + +func (x *RunScriptReply) GetTruncated() bool { + if x != nil { + return x.Truncated + } + return false +} + +var File_ide_proto protoreflect.FileDescriptor + +const file_ide_proto_rawDesc = "" + + "\n" + + "\tide.proto\x12\x03ide\"\xd3\x01\n" + + "\x06Script\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x19\n" + + "\bsql_text\x18\x03 \x01(\tR\asqlText\x12\x18\n" + + "\aversion\x18\x04 \x01(\x05R\aversion\x12#\n" + + "\rdatasource_id\x18\x05 \x01(\x03R\fdatasourceId\x12\x17\n" + + "\adb_name\x18\x06 \x01(\tR\x06dbName\x12\x1e\n" + + "\vlast_run_at\x18\a \x01(\x03R\tlastRunAt\x12\x12\n" + + "\x04beid\x18\b \x01(\x03R\x04beid\"2\n" + + "\vScriptReply\x12#\n" + + "\x06script\x18\x01 \x01(\v2\v.ide.ScriptR\x06script\"H\n" + + "\x0fScriptListReply\x12\x1f\n" + + "\x04list\x18\x01 \x03(\v2\v.ide.ScriptR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\xa0\x01\n" + + "\rSaveScriptReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x19\n" + + "\bsql_text\x18\x03 \x01(\tR\asqlText\x12#\n" + + "\rdatasource_id\x18\x04 \x01(\x03R\fdatasourceId\x12\x17\n" + + "\adb_name\x18\x05 \x01(\tR\x06dbName\x12\x12\n" + + "\x04beid\x18\x06 \x01(\x03R\x04beid\"e\n" + + "\rScriptListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x18\n" + + "\akeyword\x18\x02 \x01(\tR\akeyword\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"M\n" + + "\fRunScriptReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\x12\x19\n" + + "\bmax_rows\x18\x03 \x01(\x05R\amaxRows\"\xa4\x01\n" + + "\x0eRunScriptReply\x12!\n" + + "\fcolumns_json\x18\x01 \x01(\tR\vcolumnsJson\x12\x1b\n" + + "\trows_json\x18\x02 \x01(\tR\browsJson\x12\x1b\n" + + "\trow_count\x18\x03 \x01(\x03R\browCount\x12\x17\n" + + "\acost_ms\x18\x04 \x01(\x03R\x06costMs\x12\x1c\n" + + "\ttruncated\x18\x05 \x01(\bR\ttruncated2\xfa\x01\n" + + "\x03Ide\x122\n" + + "\n" + + "SaveScript\x12\x12.ide.SaveScriptReq\x1a\x10.ide.ScriptReply\x127\n" + + "\vListScripts\x12\x12.ide.ScriptListReq\x1a\x14.ide.ScriptListReply\x12)\n" + + "\tGetScript\x12\n" + + ".ide.IdReq\x1a\x10.ide.ScriptReply\x12&\n" + + "\fDeleteScript\x12\n" + + ".ide.IdReq\x1a\n" + + ".ide.IdReq\x123\n" + + "\tRunScript\x12\x11.ide.RunScriptReq\x1a\x13.ide.RunScriptReplyB\aZ\x05./ideb\x06proto3" + +var ( + file_ide_proto_rawDescOnce sync.Once + file_ide_proto_rawDescData []byte +) + +func file_ide_proto_rawDescGZIP() []byte { + file_ide_proto_rawDescOnce.Do(func() { + file_ide_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ide_proto_rawDesc), len(file_ide_proto_rawDesc))) + }) + return file_ide_proto_rawDescData +} + +var file_ide_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_ide_proto_goTypes = []any{ + (*Script)(nil), // 0: ide.Script + (*ScriptReply)(nil), // 1: ide.ScriptReply + (*ScriptListReply)(nil), // 2: ide.ScriptListReply + (*SaveScriptReq)(nil), // 3: ide.SaveScriptReq + (*ScriptListReq)(nil), // 4: ide.ScriptListReq + (*IdReq)(nil), // 5: ide.IdReq + (*RunScriptReq)(nil), // 6: ide.RunScriptReq + (*RunScriptReply)(nil), // 7: ide.RunScriptReply +} +var file_ide_proto_depIdxs = []int32{ + 0, // 0: ide.ScriptReply.script:type_name -> ide.Script + 0, // 1: ide.ScriptListReply.list:type_name -> ide.Script + 3, // 2: ide.Ide.SaveScript:input_type -> ide.SaveScriptReq + 4, // 3: ide.Ide.ListScripts:input_type -> ide.ScriptListReq + 5, // 4: ide.Ide.GetScript:input_type -> ide.IdReq + 5, // 5: ide.Ide.DeleteScript:input_type -> ide.IdReq + 6, // 6: ide.Ide.RunScript:input_type -> ide.RunScriptReq + 1, // 7: ide.Ide.SaveScript:output_type -> ide.ScriptReply + 2, // 8: ide.Ide.ListScripts:output_type -> ide.ScriptListReply + 1, // 9: ide.Ide.GetScript:output_type -> ide.ScriptReply + 5, // 10: ide.Ide.DeleteScript:output_type -> ide.IdReq + 7, // 11: ide.Ide.RunScript:output_type -> ide.RunScriptReply + 7, // [7:12] is the sub-list for method output_type + 2, // [2:7] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_ide_proto_init() } +func file_ide_proto_init() { + if File_ide_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ide_proto_rawDesc), len(file_ide_proto_rawDesc)), + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ide_proto_goTypes, + DependencyIndexes: file_ide_proto_depIdxs, + MessageInfos: file_ide_proto_msgTypes, + }.Build() + File_ide_proto = out.File + file_ide_proto_goTypes = nil + file_ide_proto_depIdxs = nil +} diff --git a/ide/rpc/ide/ide_grpc.pb.go b/ide/rpc/ide/ide_grpc.pb.go new file mode 100644 index 0000000..58fb359 --- /dev/null +++ b/ide/rpc/ide/ide_grpc.pb.go @@ -0,0 +1,283 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: ide.proto + +package ide + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Ide_SaveScript_FullMethodName = "/ide.Ide/SaveScript" + Ide_ListScripts_FullMethodName = "/ide.Ide/ListScripts" + Ide_GetScript_FullMethodName = "/ide.Ide/GetScript" + Ide_DeleteScript_FullMethodName = "/ide.Ide/DeleteScript" + Ide_RunScript_FullMethodName = "/ide.Ide/RunScript" +) + +// IdeClient is the client API for Ide service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type IdeClient interface { + //保存脚本(新建或更新,更新自动 version+1) + SaveScript(ctx context.Context, in *SaveScriptReq, opts ...grpc.CallOption) (*ScriptReply, error) + //脚本列表 + ListScripts(ctx context.Context, in *ScriptListReq, opts ...grpc.CallOption) (*ScriptListReply, error) + //脚本详情 + GetScript(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*ScriptReply, error) + //删除脚本 + DeleteScript(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + //执行脚本(开发调试:仅 SELECT,行数保护截断) + RunScript(ctx context.Context, in *RunScriptReq, opts ...grpc.CallOption) (*RunScriptReply, error) +} + +type ideClient struct { + cc grpc.ClientConnInterface +} + +func NewIdeClient(cc grpc.ClientConnInterface) IdeClient { + return &ideClient{cc} +} + +func (c *ideClient) SaveScript(ctx context.Context, in *SaveScriptReq, opts ...grpc.CallOption) (*ScriptReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ScriptReply) + err := c.cc.Invoke(ctx, Ide_SaveScript_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *ideClient) ListScripts(ctx context.Context, in *ScriptListReq, opts ...grpc.CallOption) (*ScriptListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ScriptListReply) + err := c.cc.Invoke(ctx, Ide_ListScripts_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *ideClient) GetScript(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*ScriptReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ScriptReply) + err := c.cc.Invoke(ctx, Ide_GetScript_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *ideClient) DeleteScript(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdReq) + err := c.cc.Invoke(ctx, Ide_DeleteScript_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *ideClient) RunScript(ctx context.Context, in *RunScriptReq, opts ...grpc.CallOption) (*RunScriptReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RunScriptReply) + err := c.cc.Invoke(ctx, Ide_RunScript_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// IdeServer is the server API for Ide service. +// All implementations must embed UnimplementedIdeServer +// for forward compatibility. +type IdeServer interface { + //保存脚本(新建或更新,更新自动 version+1) + SaveScript(context.Context, *SaveScriptReq) (*ScriptReply, error) + //脚本列表 + ListScripts(context.Context, *ScriptListReq) (*ScriptListReply, error) + //脚本详情 + GetScript(context.Context, *IdReq) (*ScriptReply, error) + //删除脚本 + DeleteScript(context.Context, *IdReq) (*IdReq, error) + //执行脚本(开发调试:仅 SELECT,行数保护截断) + RunScript(context.Context, *RunScriptReq) (*RunScriptReply, error) + mustEmbedUnimplementedIdeServer() +} + +// UnimplementedIdeServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedIdeServer struct{} + +func (UnimplementedIdeServer) SaveScript(context.Context, *SaveScriptReq) (*ScriptReply, error) { + return nil, status.Error(codes.Unimplemented, "method SaveScript not implemented") +} +func (UnimplementedIdeServer) ListScripts(context.Context, *ScriptListReq) (*ScriptListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListScripts not implemented") +} +func (UnimplementedIdeServer) GetScript(context.Context, *IdReq) (*ScriptReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetScript not implemented") +} +func (UnimplementedIdeServer) DeleteScript(context.Context, *IdReq) (*IdReq, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteScript not implemented") +} +func (UnimplementedIdeServer) RunScript(context.Context, *RunScriptReq) (*RunScriptReply, error) { + return nil, status.Error(codes.Unimplemented, "method RunScript not implemented") +} +func (UnimplementedIdeServer) mustEmbedUnimplementedIdeServer() {} +func (UnimplementedIdeServer) testEmbeddedByValue() {} + +// UnsafeIdeServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to IdeServer will +// result in compilation errors. +type UnsafeIdeServer interface { + mustEmbedUnimplementedIdeServer() +} + +func RegisterIdeServer(s grpc.ServiceRegistrar, srv IdeServer) { + // If the following call panics, it indicates UnimplementedIdeServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Ide_ServiceDesc, srv) +} + +func _Ide_SaveScript_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SaveScriptReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdeServer).SaveScript(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ide_SaveScript_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdeServer).SaveScript(ctx, req.(*SaveScriptReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Ide_ListScripts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ScriptListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdeServer).ListScripts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ide_ListScripts_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdeServer).ListScripts(ctx, req.(*ScriptListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Ide_GetScript_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdeServer).GetScript(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ide_GetScript_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdeServer).GetScript(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Ide_DeleteScript_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdeServer).DeleteScript(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ide_DeleteScript_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdeServer).DeleteScript(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Ide_RunScript_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RunScriptReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdeServer).RunScript(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ide_RunScript_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdeServer).RunScript(ctx, req.(*RunScriptReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Ide_ServiceDesc is the grpc.ServiceDesc for Ide service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Ide_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ide.Ide", + HandlerType: (*IdeServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SaveScript", + Handler: _Ide_SaveScript_Handler, + }, + { + MethodName: "ListScripts", + Handler: _Ide_ListScripts_Handler, + }, + { + MethodName: "GetScript", + Handler: _Ide_GetScript_Handler, + }, + { + MethodName: "DeleteScript", + Handler: _Ide_DeleteScript_Handler, + }, + { + MethodName: "RunScript", + Handler: _Ide_RunScript_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ide.proto", +} diff --git a/ide/rpc/ideClient/ide.go b/ide/rpc/ideClient/ide.go new file mode 100644 index 0000000..3ed09eb --- /dev/null +++ b/ide/rpc/ideClient/ide.go @@ -0,0 +1,78 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: ide.proto + +package ideClient + +import ( + "context" + + "datacenter/ide/rpc/ide" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + IdReq = ide.IdReq + RunScriptReply = ide.RunScriptReply + RunScriptReq = ide.RunScriptReq + SaveScriptReq = ide.SaveScriptReq + Script = ide.Script + ScriptListReply = ide.ScriptListReply + ScriptListReq = ide.ScriptListReq + ScriptReply = ide.ScriptReply + + Ide interface { + // 保存脚本(新建或更新,更新自动 version+1) + SaveScript(ctx context.Context, in *SaveScriptReq, opts ...grpc.CallOption) (*ScriptReply, error) + // 脚本列表 + ListScripts(ctx context.Context, in *ScriptListReq, opts ...grpc.CallOption) (*ScriptListReply, error) + // 脚本详情 + GetScript(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*ScriptReply, error) + // 删除脚本 + DeleteScript(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + // 执行脚本(开发调试:仅 SELECT,行数保护截断) + RunScript(ctx context.Context, in *RunScriptReq, opts ...grpc.CallOption) (*RunScriptReply, error) + } + + defaultIde struct { + cli zrpc.Client + } +) + +func NewIde(cli zrpc.Client) Ide { + return &defaultIde{ + cli: cli, + } +} + +// 保存脚本(新建或更新,更新自动 version+1) +func (m *defaultIde) SaveScript(ctx context.Context, in *SaveScriptReq, opts ...grpc.CallOption) (*ScriptReply, error) { + client := ide.NewIdeClient(m.cli.Conn()) + return client.SaveScript(ctx, in, opts...) +} + +// 脚本列表 +func (m *defaultIde) ListScripts(ctx context.Context, in *ScriptListReq, opts ...grpc.CallOption) (*ScriptListReply, error) { + client := ide.NewIdeClient(m.cli.Conn()) + return client.ListScripts(ctx, in, opts...) +} + +// 脚本详情 +func (m *defaultIde) GetScript(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*ScriptReply, error) { + client := ide.NewIdeClient(m.cli.Conn()) + return client.GetScript(ctx, in, opts...) +} + +// 删除脚本 +func (m *defaultIde) DeleteScript(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + client := ide.NewIdeClient(m.cli.Conn()) + return client.DeleteScript(ctx, in, opts...) +} + +// 执行脚本(开发调试:仅 SELECT,行数保护截断) +func (m *defaultIde) RunScript(ctx context.Context, in *RunScriptReq, opts ...grpc.CallOption) (*RunScriptReply, error) { + client := ide.NewIdeClient(m.cli.Conn()) + return client.RunScript(ctx, in, opts...) +} diff --git a/ide/rpc/internal/config/config.go b/ide/rpc/internal/config/config.go new file mode 100755 index 0000000..96863e9 --- /dev/null +++ b/ide/rpc/internal/config/config.go @@ -0,0 +1,10 @@ +package config + +import "github.com/zeromicro/go-zero/zrpc" + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } +} diff --git a/ide/rpc/internal/logic/deleteScriptLogic.go b/ide/rpc/internal/logic/deleteScriptLogic.go new file mode 100644 index 0000000..41179ea --- /dev/null +++ b/ide/rpc/internal/logic/deleteScriptLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/ide/rpc/ide" + "datacenter/ide/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteScriptLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeleteScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteScriptLogic { + return &DeleteScriptLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 删除脚本 +func (l *DeleteScriptLogic) DeleteScript(in *ide.IdReq) (*ide.IdReq, error) { + // todo: add your logic here and delete this line + + return &ide.IdReq{}, nil +} diff --git a/ide/rpc/internal/logic/getScriptLogic.go b/ide/rpc/internal/logic/getScriptLogic.go new file mode 100644 index 0000000..14b58f3 --- /dev/null +++ b/ide/rpc/internal/logic/getScriptLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/ide/rpc/ide" + "datacenter/ide/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetScriptLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetScriptLogic { + return &GetScriptLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 脚本详情 +func (l *GetScriptLogic) GetScript(in *ide.IdReq) (*ide.ScriptReply, error) { + // todo: add your logic here and delete this line + + return &ide.ScriptReply{}, nil +} diff --git a/ide/rpc/internal/logic/listScriptsLogic.go b/ide/rpc/internal/logic/listScriptsLogic.go new file mode 100644 index 0000000..f012f3a --- /dev/null +++ b/ide/rpc/internal/logic/listScriptsLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/ide/rpc/ide" + "datacenter/ide/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListScriptsLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListScriptsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListScriptsLogic { + return &ListScriptsLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 脚本列表 +func (l *ListScriptsLogic) ListScripts(in *ide.ScriptListReq) (*ide.ScriptListReply, error) { + // todo: add your logic here and delete this line + + return &ide.ScriptListReply{}, nil +} diff --git a/ide/rpc/internal/logic/runScriptLogic.go b/ide/rpc/internal/logic/runScriptLogic.go new file mode 100644 index 0000000..e7473b5 --- /dev/null +++ b/ide/rpc/internal/logic/runScriptLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/ide/rpc/ide" + "datacenter/ide/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RunScriptLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewRunScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RunScriptLogic { + return &RunScriptLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 执行脚本(开发调试:仅 SELECT,行数保护截断) +func (l *RunScriptLogic) RunScript(in *ide.RunScriptReq) (*ide.RunScriptReply, error) { + // todo: add your logic here and delete this line + + return &ide.RunScriptReply{}, nil +} diff --git a/ide/rpc/internal/logic/saveScriptLogic.go b/ide/rpc/internal/logic/saveScriptLogic.go new file mode 100644 index 0000000..10eb0a4 --- /dev/null +++ b/ide/rpc/internal/logic/saveScriptLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/ide/rpc/ide" + "datacenter/ide/rpc/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SaveScriptLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewSaveScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SaveScriptLogic { + return &SaveScriptLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 保存脚本(新建或更新,更新自动 version+1) +func (l *SaveScriptLogic) SaveScript(in *ide.SaveScriptReq) (*ide.ScriptReply, error) { + // todo: add your logic here and delete this line + + return &ide.ScriptReply{}, nil +} diff --git a/ide/rpc/internal/server/ideServer.go b/ide/rpc/internal/server/ideServer.go new file mode 100644 index 0000000..f27a474 --- /dev/null +++ b/ide/rpc/internal/server/ideServer.go @@ -0,0 +1,54 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: ide.proto + +package server + +import ( + "context" + + "datacenter/ide/rpc/ide" + "datacenter/ide/rpc/internal/logic" + "datacenter/ide/rpc/internal/svc" +) + +type IdeServer struct { + svcCtx *svc.ServiceContext + ide.UnimplementedIdeServer +} + +func NewIdeServer(svcCtx *svc.ServiceContext) *IdeServer { + return &IdeServer{ + svcCtx: svcCtx, + } +} + +// 保存脚本(新建或更新,更新自动 version+1) +func (s *IdeServer) SaveScript(ctx context.Context, in *ide.SaveScriptReq) (*ide.ScriptReply, error) { + l := logic.NewSaveScriptLogic(ctx, s.svcCtx) + return l.SaveScript(in) +} + +// 脚本列表 +func (s *IdeServer) ListScripts(ctx context.Context, in *ide.ScriptListReq) (*ide.ScriptListReply, error) { + l := logic.NewListScriptsLogic(ctx, s.svcCtx) + return l.ListScripts(in) +} + +// 脚本详情 +func (s *IdeServer) GetScript(ctx context.Context, in *ide.IdReq) (*ide.ScriptReply, error) { + l := logic.NewGetScriptLogic(ctx, s.svcCtx) + return l.GetScript(in) +} + +// 删除脚本 +func (s *IdeServer) DeleteScript(ctx context.Context, in *ide.IdReq) (*ide.IdReq, error) { + l := logic.NewDeleteScriptLogic(ctx, s.svcCtx) + return l.DeleteScript(in) +} + +// 执行脚本(开发调试:仅 SELECT,行数保护截断) +func (s *IdeServer) RunScript(ctx context.Context, in *ide.RunScriptReq) (*ide.RunScriptReply, error) { + l := logic.NewRunScriptLogic(ctx, s.svcCtx) + return l.RunScript(in) +} diff --git a/ide/rpc/internal/svc/serviceContext.go b/ide/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..64e746d --- /dev/null +++ b/ide/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/ide/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/job/rpc/etc/job.example.yaml b/job/rpc/etc/job.example.yaml new file mode 100644 index 0000000..0faf0e2 --- /dev/null +++ b/job/rpc/etc/job.example.yaml @@ -0,0 +1,8 @@ +Name: job.rpc +ListenOn: 0.0.0.0:8092 +Etcd: + Hosts: + - 127.0.0.1:2379 + Key: job.rpc +Mysql: + DataSource: root:admin@tcp(127.0.0.1:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/job/rpc/internal/config/config.go b/job/rpc/internal/config/config.go new file mode 100755 index 0000000..96863e9 --- /dev/null +++ b/job/rpc/internal/config/config.go @@ -0,0 +1,10 @@ +package config + +import "github.com/zeromicro/go-zero/zrpc" + +type Config struct { + zrpc.RpcServerConf + Mysql struct { + DataSource string + } +} diff --git a/job/rpc/internal/logic/createSyncTaskLogic.go b/job/rpc/internal/logic/createSyncTaskLogic.go new file mode 100644 index 0000000..b2cdccc --- /dev/null +++ b/job/rpc/internal/logic/createSyncTaskLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateSyncTaskLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewCreateSyncTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateSyncTaskLogic { + return &CreateSyncTaskLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 创建同步任务 +func (l *CreateSyncTaskLogic) CreateSyncTask(in *job.CreateSyncTaskReq) (*job.TaskReply, error) { + // todo: add your logic here and delete this line + + return &job.TaskReply{}, nil +} diff --git a/job/rpc/internal/logic/deleteSyncTaskLogic.go b/job/rpc/internal/logic/deleteSyncTaskLogic.go new file mode 100644 index 0000000..ecd7f82 --- /dev/null +++ b/job/rpc/internal/logic/deleteSyncTaskLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteSyncTaskLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeleteSyncTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteSyncTaskLogic { + return &DeleteSyncTaskLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 删除任务(已部署的先提示需先下线) +func (l *DeleteSyncTaskLogic) DeleteSyncTask(in *job.IdReq) (*job.IdReq, error) { + // todo: add your logic here and delete this line + + return &job.IdReq{}, nil +} diff --git a/job/rpc/internal/logic/deployToSchedulerLogic.go b/job/rpc/internal/logic/deployToSchedulerLogic.go new file mode 100644 index 0000000..20f5f31 --- /dev/null +++ b/job/rpc/internal/logic/deployToSchedulerLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeployToSchedulerLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewDeployToSchedulerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeployToSchedulerLogic { + return &DeployToSchedulerLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因) +func (l *DeployToSchedulerLogic) DeployToScheduler(in *job.IdReq) (*job.DeployReply, error) { + // todo: add your logic here and delete this line + + return &job.DeployReply{}, nil +} diff --git a/job/rpc/internal/logic/getSchedulerInfoLogic.go b/job/rpc/internal/logic/getSchedulerInfoLogic.go new file mode 100644 index 0000000..b4f6529 --- /dev/null +++ b/job/rpc/internal/logic/getSchedulerInfoLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetSchedulerInfoLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetSchedulerInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSchedulerInfoLogic { + return &GetSchedulerInfoLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 调度器连接信息(运维视图) +func (l *GetSchedulerInfoLogic) GetSchedulerInfo(in *job.IdReq) (*job.SchedulerReply, error) { + // todo: add your logic here and delete this line + + return &job.SchedulerReply{}, nil +} diff --git a/job/rpc/internal/logic/getTaskStatusLogic.go b/job/rpc/internal/logic/getTaskStatusLogic.go new file mode 100644 index 0000000..260abc5 --- /dev/null +++ b/job/rpc/internal/logic/getTaskStatusLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetTaskStatusLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTaskStatusLogic { + return &GetTaskStatusLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 任务状态(本地 + 调度器实例状态) +func (l *GetTaskStatusLogic) GetTaskStatus(in *job.IdReq) (*job.StatusReply, error) { + // todo: add your logic here and delete this line + + return &job.StatusReply{}, nil +} diff --git a/job/rpc/internal/logic/listSyncTasksLogic.go b/job/rpc/internal/logic/listSyncTasksLogic.go new file mode 100644 index 0000000..8575744 --- /dev/null +++ b/job/rpc/internal/logic/listSyncTasksLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ListSyncTasksLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewListSyncTasksLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListSyncTasksLogic { + return &ListSyncTasksLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 任务列表 +func (l *ListSyncTasksLogic) ListSyncTasks(in *job.TaskListReq) (*job.TaskListReply, error) { + // todo: add your logic here and delete this line + + return &job.TaskListReply{}, nil +} diff --git a/job/rpc/internal/logic/triggerTaskLogic.go b/job/rpc/internal/logic/triggerTaskLogic.go new file mode 100644 index 0000000..0ebd217 --- /dev/null +++ b/job/rpc/internal/logic/triggerTaskLogic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TriggerTaskLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewTriggerTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TriggerTaskLogic { + return &TriggerTaskLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 手动触发一次 +func (l *TriggerTaskLogic) TriggerTask(in *job.IdReq) (*job.TriggerReply, error) { + // todo: add your logic here and delete this line + + return &job.TriggerReply{}, nil +} diff --git a/job/rpc/internal/server/jobServer.go b/job/rpc/internal/server/jobServer.go new file mode 100644 index 0000000..6713599 --- /dev/null +++ b/job/rpc/internal/server/jobServer.go @@ -0,0 +1,66 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: job.proto + +package server + +import ( + "context" + + "datacenter/job/rpc/internal/logic" + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" +) + +type JobServer struct { + svcCtx *svc.ServiceContext + job.UnimplementedJobServer +} + +func NewJobServer(svcCtx *svc.ServiceContext) *JobServer { + return &JobServer{ + svcCtx: svcCtx, + } +} + +// 创建同步任务 +func (s *JobServer) CreateSyncTask(ctx context.Context, in *job.CreateSyncTaskReq) (*job.TaskReply, error) { + l := logic.NewCreateSyncTaskLogic(ctx, s.svcCtx) + return l.CreateSyncTask(in) +} + +// 任务列表 +func (s *JobServer) ListSyncTasks(ctx context.Context, in *job.TaskListReq) (*job.TaskListReply, error) { + l := logic.NewListSyncTasksLogic(ctx, s.svcCtx) + return l.ListSyncTasks(in) +} + +// 删除任务(已部署的先提示需先下线) +func (s *JobServer) DeleteSyncTask(ctx context.Context, in *job.IdReq) (*job.IdReq, error) { + l := logic.NewDeleteSyncTaskLogic(ctx, s.svcCtx) + return l.DeleteSyncTask(in) +} + +// 部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因) +func (s *JobServer) DeployToScheduler(ctx context.Context, in *job.IdReq) (*job.DeployReply, error) { + l := logic.NewDeployToSchedulerLogic(ctx, s.svcCtx) + return l.DeployToScheduler(in) +} + +// 手动触发一次 +func (s *JobServer) TriggerTask(ctx context.Context, in *job.IdReq) (*job.TriggerReply, error) { + l := logic.NewTriggerTaskLogic(ctx, s.svcCtx) + return l.TriggerTask(in) +} + +// 任务状态(本地 + 调度器实例状态) +func (s *JobServer) GetTaskStatus(ctx context.Context, in *job.IdReq) (*job.StatusReply, error) { + l := logic.NewGetTaskStatusLogic(ctx, s.svcCtx) + return l.GetTaskStatus(in) +} + +// 调度器连接信息(运维视图) +func (s *JobServer) GetSchedulerInfo(ctx context.Context, in *job.IdReq) (*job.SchedulerReply, error) { + l := logic.NewGetSchedulerInfoLogic(ctx, s.svcCtx) + return l.GetSchedulerInfo(in) +} diff --git a/job/rpc/internal/svc/serviceContext.go b/job/rpc/internal/svc/serviceContext.go new file mode 100644 index 0000000..6a03956 --- /dev/null +++ b/job/rpc/internal/svc/serviceContext.go @@ -0,0 +1,13 @@ +package svc + +import "datacenter/job/rpc/internal/config" + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/job/rpc/job.go b/job/rpc/job.go new file mode 100644 index 0000000..34d671a --- /dev/null +++ b/job/rpc/job.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + + "datacenter/job/rpc/internal/config" + "datacenter/job/rpc/internal/server" + "datacenter/job/rpc/internal/svc" + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/job.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + ctx := svc.NewServiceContext(c) + + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + job.RegisterJobServer(grpcServer, server.NewJobServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} diff --git a/job/rpc/job.proto b/job/rpc/job.proto new file mode 100644 index 0000000..dfb3755 --- /dev/null +++ b/job/rpc/job.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +package job; + +option go_package = "./job"; + +//同步任务(管控面:本平台管理定义,执行面推送 DolphinScheduler/SeaTunnel) +message SyncTask { + int64 id = 1; + string name = 2; + int64 source_ds_id = 3; + int64 target_ds_id = 4; + string mode = 5; //offline/cdc + string mapping_json = 6; + string cron = 7; + int32 ds_workflow_code = 8; //DS 工作流编码(部署后回填) + string status = 9; //draft/deployed/running/failed + int64 last_run_at = 10; + int64 beid = 11; +} +message TaskReply { SyncTask task = 1; } +message TaskListReply { + repeated SyncTask list = 1; + int64 total = 2; +} + +message CreateSyncTaskReq { + string name = 1; + int64 source_ds_id = 2; + int64 target_ds_id = 3; + string mode = 4; + string mapping_json = 5; + string cron = 6; + int64 beid = 7; +} +message TaskListReq { int64 beid = 1; string status = 2; int64 page = 3; int64 size = 4; } +message IdReq { int64 id = 1; int64 beid = 2; } + +message DeployReply { + bool ok = 1; + int32 ds_workflow_code = 2; + string msg = 3; //失败原因(DS 未配置/不可达等) +} +message TriggerReply { + bool ok = 1; + int32 ds_process_instance_id = 2; + string msg = 3; +} +message StatusReply { + string status = 1; //本地状态 + DS 实例状态聚合 + int32 ds_instance_id = 2; + string msg = 3; +} +message SchedulerReply { + bool configured = 1; //是否已配置 DS 地址 + bool connected = 2; //当前是否可达(登录成功) + string base_url = 3; + string msg = 4; +} + +service Job { + //创建同步任务 + rpc CreateSyncTask(CreateSyncTaskReq) returns (TaskReply); + //任务列表 + rpc ListSyncTasks(TaskListReq) returns (TaskListReply); + //删除任务(已部署的先提示需先下线) + rpc DeleteSyncTask(IdReq) returns (IdReq); + //部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因) + rpc DeployToScheduler(IdReq) returns (DeployReply); + //手动触发一次 + rpc TriggerTask(IdReq) returns (TriggerReply); + //任务状态(本地 + 调度器实例状态) + rpc GetTaskStatus(IdReq) returns (StatusReply); + //调度器连接信息(运维视图) + rpc GetSchedulerInfo(IdReq) returns (SchedulerReply); +} diff --git a/job/rpc/job/job.pb.go b/job/rpc/job/job.pb.go new file mode 100644 index 0000000..cf27469 --- /dev/null +++ b/job/rpc/job/job.pb.go @@ -0,0 +1,853 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.12 +// protoc v7.36.2 +// source: job.proto + +package job + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// 同步任务(管控面:本平台管理定义,执行面推送 DolphinScheduler/SeaTunnel) +type SyncTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + SourceDsId int64 `protobuf:"varint,3,opt,name=source_ds_id,json=sourceDsId,proto3" json:"source_ds_id,omitempty"` + TargetDsId int64 `protobuf:"varint,4,opt,name=target_ds_id,json=targetDsId,proto3" json:"target_ds_id,omitempty"` + Mode string `protobuf:"bytes,5,opt,name=mode,proto3" json:"mode,omitempty"` //offline/cdc + MappingJson string `protobuf:"bytes,6,opt,name=mapping_json,json=mappingJson,proto3" json:"mapping_json,omitempty"` + Cron string `protobuf:"bytes,7,opt,name=cron,proto3" json:"cron,omitempty"` + DsWorkflowCode int32 `protobuf:"varint,8,opt,name=ds_workflow_code,json=dsWorkflowCode,proto3" json:"ds_workflow_code,omitempty"` //DS 工作流编码(部署后回填) + Status string `protobuf:"bytes,9,opt,name=status,proto3" json:"status,omitempty"` //draft/deployed/running/failed + LastRunAt int64 `protobuf:"varint,10,opt,name=last_run_at,json=lastRunAt,proto3" json:"last_run_at,omitempty"` + Beid int64 `protobuf:"varint,11,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncTask) Reset() { + *x = SyncTask{} + mi := &file_job_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncTask) ProtoMessage() {} + +func (x *SyncTask) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncTask.ProtoReflect.Descriptor instead. +func (*SyncTask) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{0} +} + +func (x *SyncTask) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *SyncTask) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SyncTask) GetSourceDsId() int64 { + if x != nil { + return x.SourceDsId + } + return 0 +} + +func (x *SyncTask) GetTargetDsId() int64 { + if x != nil { + return x.TargetDsId + } + return 0 +} + +func (x *SyncTask) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *SyncTask) GetMappingJson() string { + if x != nil { + return x.MappingJson + } + return "" +} + +func (x *SyncTask) GetCron() string { + if x != nil { + return x.Cron + } + return "" +} + +func (x *SyncTask) GetDsWorkflowCode() int32 { + if x != nil { + return x.DsWorkflowCode + } + return 0 +} + +func (x *SyncTask) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *SyncTask) GetLastRunAt() int64 { + if x != nil { + return x.LastRunAt + } + return 0 +} + +func (x *SyncTask) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type TaskReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Task *SyncTask `protobuf:"bytes,1,opt,name=task,proto3" json:"task,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskReply) Reset() { + *x = TaskReply{} + mi := &file_job_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskReply) ProtoMessage() {} + +func (x *TaskReply) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskReply.ProtoReflect.Descriptor instead. +func (*TaskReply) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{1} +} + +func (x *TaskReply) GetTask() *SyncTask { + if x != nil { + return x.Task + } + return nil +} + +type TaskListReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + List []*SyncTask `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskListReply) Reset() { + *x = TaskListReply{} + mi := &file_job_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskListReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskListReply) ProtoMessage() {} + +func (x *TaskListReply) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskListReply.ProtoReflect.Descriptor instead. +func (*TaskListReply) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{2} +} + +func (x *TaskListReply) GetList() []*SyncTask { + if x != nil { + return x.List + } + return nil +} + +func (x *TaskListReply) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +type CreateSyncTaskReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + SourceDsId int64 `protobuf:"varint,2,opt,name=source_ds_id,json=sourceDsId,proto3" json:"source_ds_id,omitempty"` + TargetDsId int64 `protobuf:"varint,3,opt,name=target_ds_id,json=targetDsId,proto3" json:"target_ds_id,omitempty"` + Mode string `protobuf:"bytes,4,opt,name=mode,proto3" json:"mode,omitempty"` + MappingJson string `protobuf:"bytes,5,opt,name=mapping_json,json=mappingJson,proto3" json:"mapping_json,omitempty"` + Cron string `protobuf:"bytes,6,opt,name=cron,proto3" json:"cron,omitempty"` + Beid int64 `protobuf:"varint,7,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSyncTaskReq) Reset() { + *x = CreateSyncTaskReq{} + mi := &file_job_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSyncTaskReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSyncTaskReq) ProtoMessage() {} + +func (x *CreateSyncTaskReq) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSyncTaskReq.ProtoReflect.Descriptor instead. +func (*CreateSyncTaskReq) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateSyncTaskReq) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateSyncTaskReq) GetSourceDsId() int64 { + if x != nil { + return x.SourceDsId + } + return 0 +} + +func (x *CreateSyncTaskReq) GetTargetDsId() int64 { + if x != nil { + return x.TargetDsId + } + return 0 +} + +func (x *CreateSyncTaskReq) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *CreateSyncTaskReq) GetMappingJson() string { + if x != nil { + return x.MappingJson + } + return "" +} + +func (x *CreateSyncTaskReq) GetCron() string { + if x != nil { + return x.Cron + } + return "" +} + +func (x *CreateSyncTaskReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type TaskListReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Beid int64 `protobuf:"varint,1,opt,name=beid,proto3" json:"beid,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + Page int64 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskListReq) Reset() { + *x = TaskListReq{} + mi := &file_job_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskListReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskListReq) ProtoMessage() {} + +func (x *TaskListReq) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskListReq.ProtoReflect.Descriptor instead. +func (*TaskListReq) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{4} +} + +func (x *TaskListReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *TaskListReq) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *TaskListReq) GetPage() int64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *TaskListReq) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type IdReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdReq) Reset() { + *x = IdReq{} + mi := &file_job_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdReq) ProtoMessage() {} + +func (x *IdReq) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdReq.ProtoReflect.Descriptor instead. +func (*IdReq) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{5} +} + +func (x *IdReq) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +type DeployReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + DsWorkflowCode int32 `protobuf:"varint,2,opt,name=ds_workflow_code,json=dsWorkflowCode,proto3" json:"ds_workflow_code,omitempty"` + Msg string `protobuf:"bytes,3,opt,name=msg,proto3" json:"msg,omitempty"` //失败原因(DS 未配置/不可达等) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeployReply) Reset() { + *x = DeployReply{} + mi := &file_job_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeployReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeployReply) ProtoMessage() {} + +func (x *DeployReply) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeployReply.ProtoReflect.Descriptor instead. +func (*DeployReply) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{6} +} + +func (x *DeployReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *DeployReply) GetDsWorkflowCode() int32 { + if x != nil { + return x.DsWorkflowCode + } + return 0 +} + +func (x *DeployReply) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +type TriggerReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + DsProcessInstanceId int32 `protobuf:"varint,2,opt,name=ds_process_instance_id,json=dsProcessInstanceId,proto3" json:"ds_process_instance_id,omitempty"` + Msg string `protobuf:"bytes,3,opt,name=msg,proto3" json:"msg,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TriggerReply) Reset() { + *x = TriggerReply{} + mi := &file_job_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TriggerReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TriggerReply) ProtoMessage() {} + +func (x *TriggerReply) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TriggerReply.ProtoReflect.Descriptor instead. +func (*TriggerReply) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{7} +} + +func (x *TriggerReply) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *TriggerReply) GetDsProcessInstanceId() int32 { + if x != nil { + return x.DsProcessInstanceId + } + return 0 +} + +func (x *TriggerReply) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +type StatusReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` //本地状态 + DS 实例状态聚合 + DsInstanceId int32 `protobuf:"varint,2,opt,name=ds_instance_id,json=dsInstanceId,proto3" json:"ds_instance_id,omitempty"` + Msg string `protobuf:"bytes,3,opt,name=msg,proto3" json:"msg,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatusReply) Reset() { + *x = StatusReply{} + mi := &file_job_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatusReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusReply) ProtoMessage() {} + +func (x *StatusReply) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StatusReply.ProtoReflect.Descriptor instead. +func (*StatusReply) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{8} +} + +func (x *StatusReply) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *StatusReply) GetDsInstanceId() int32 { + if x != nil { + return x.DsInstanceId + } + return 0 +} + +func (x *StatusReply) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +type SchedulerReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Configured bool `protobuf:"varint,1,opt,name=configured,proto3" json:"configured,omitempty"` //是否已配置 DS 地址 + Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` //当前是否可达(登录成功) + BaseUrl string `protobuf:"bytes,3,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + Msg string `protobuf:"bytes,4,opt,name=msg,proto3" json:"msg,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SchedulerReply) Reset() { + *x = SchedulerReply{} + mi := &file_job_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SchedulerReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SchedulerReply) ProtoMessage() {} + +func (x *SchedulerReply) ProtoReflect() protoreflect.Message { + mi := &file_job_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SchedulerReply.ProtoReflect.Descriptor instead. +func (*SchedulerReply) Descriptor() ([]byte, []int) { + return file_job_proto_rawDescGZIP(), []int{9} +} + +func (x *SchedulerReply) GetConfigured() bool { + if x != nil { + return x.Configured + } + return false +} + +func (x *SchedulerReply) GetConnected() bool { + if x != nil { + return x.Connected + } + return false +} + +func (x *SchedulerReply) GetBaseUrl() string { + if x != nil { + return x.BaseUrl + } + return "" +} + +func (x *SchedulerReply) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +var File_job_proto protoreflect.FileDescriptor + +const file_job_proto_rawDesc = "" + + "\n" + + "\tjob.proto\x12\x03job\"\xb3\x02\n" + + "\bSyncTask\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12 \n" + + "\fsource_ds_id\x18\x03 \x01(\x03R\n" + + "sourceDsId\x12 \n" + + "\ftarget_ds_id\x18\x04 \x01(\x03R\n" + + "targetDsId\x12\x12\n" + + "\x04mode\x18\x05 \x01(\tR\x04mode\x12!\n" + + "\fmapping_json\x18\x06 \x01(\tR\vmappingJson\x12\x12\n" + + "\x04cron\x18\a \x01(\tR\x04cron\x12(\n" + + "\x10ds_workflow_code\x18\b \x01(\x05R\x0edsWorkflowCode\x12\x16\n" + + "\x06status\x18\t \x01(\tR\x06status\x12\x1e\n" + + "\vlast_run_at\x18\n" + + " \x01(\x03R\tlastRunAt\x12\x12\n" + + "\x04beid\x18\v \x01(\x03R\x04beid\".\n" + + "\tTaskReply\x12!\n" + + "\x04task\x18\x01 \x01(\v2\r.job.SyncTaskR\x04task\"H\n" + + "\rTaskListReply\x12!\n" + + "\x04list\x18\x01 \x03(\v2\r.job.SyncTaskR\x04list\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\xca\x01\n" + + "\x11CreateSyncTaskReq\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\fsource_ds_id\x18\x02 \x01(\x03R\n" + + "sourceDsId\x12 \n" + + "\ftarget_ds_id\x18\x03 \x01(\x03R\n" + + "targetDsId\x12\x12\n" + + "\x04mode\x18\x04 \x01(\tR\x04mode\x12!\n" + + "\fmapping_json\x18\x05 \x01(\tR\vmappingJson\x12\x12\n" + + "\x04cron\x18\x06 \x01(\tR\x04cron\x12\x12\n" + + "\x04beid\x18\a \x01(\x03R\x04beid\"a\n" + + "\vTaskListReq\x12\x12\n" + + "\x04beid\x18\x01 \x01(\x03R\x04beid\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x12\n" + + "\x04page\x18\x03 \x01(\x03R\x04page\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\"+\n" + + "\x05IdReq\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\"Y\n" + + "\vDeployReply\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12(\n" + + "\x10ds_workflow_code\x18\x02 \x01(\x05R\x0edsWorkflowCode\x12\x10\n" + + "\x03msg\x18\x03 \x01(\tR\x03msg\"e\n" + + "\fTriggerReply\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x123\n" + + "\x16ds_process_instance_id\x18\x02 \x01(\x05R\x13dsProcessInstanceId\x12\x10\n" + + "\x03msg\x18\x03 \x01(\tR\x03msg\"]\n" + + "\vStatusReply\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\x12$\n" + + "\x0eds_instance_id\x18\x02 \x01(\x05R\fdsInstanceId\x12\x10\n" + + "\x03msg\x18\x03 \x01(\tR\x03msg\"{\n" + + "\x0eSchedulerReply\x12\x1e\n" + + "\n" + + "configured\x18\x01 \x01(\bR\n" + + "configured\x12\x1c\n" + + "\tconnected\x18\x02 \x01(\bR\tconnected\x12\x19\n" + + "\bbase_url\x18\x03 \x01(\tR\abaseUrl\x12\x10\n" + + "\x03msg\x18\x04 \x01(\tR\x03msg2\xe5\x02\n" + + "\x03Job\x128\n" + + "\x0eCreateSyncTask\x12\x16.job.CreateSyncTaskReq\x1a\x0e.job.TaskReply\x125\n" + + "\rListSyncTasks\x12\x10.job.TaskListReq\x1a\x12.job.TaskListReply\x12(\n" + + "\x0eDeleteSyncTask\x12\n" + + ".job.IdReq\x1a\n" + + ".job.IdReq\x121\n" + + "\x11DeployToScheduler\x12\n" + + ".job.IdReq\x1a\x10.job.DeployReply\x12,\n" + + "\vTriggerTask\x12\n" + + ".job.IdReq\x1a\x11.job.TriggerReply\x12-\n" + + "\rGetTaskStatus\x12\n" + + ".job.IdReq\x1a\x10.job.StatusReply\x123\n" + + "\x10GetSchedulerInfo\x12\n" + + ".job.IdReq\x1a\x13.job.SchedulerReplyB\aZ\x05./jobb\x06proto3" + +var ( + file_job_proto_rawDescOnce sync.Once + file_job_proto_rawDescData []byte +) + +func file_job_proto_rawDescGZIP() []byte { + file_job_proto_rawDescOnce.Do(func() { + file_job_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_job_proto_rawDesc), len(file_job_proto_rawDesc))) + }) + return file_job_proto_rawDescData +} + +var file_job_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_job_proto_goTypes = []any{ + (*SyncTask)(nil), // 0: job.SyncTask + (*TaskReply)(nil), // 1: job.TaskReply + (*TaskListReply)(nil), // 2: job.TaskListReply + (*CreateSyncTaskReq)(nil), // 3: job.CreateSyncTaskReq + (*TaskListReq)(nil), // 4: job.TaskListReq + (*IdReq)(nil), // 5: job.IdReq + (*DeployReply)(nil), // 6: job.DeployReply + (*TriggerReply)(nil), // 7: job.TriggerReply + (*StatusReply)(nil), // 8: job.StatusReply + (*SchedulerReply)(nil), // 9: job.SchedulerReply +} +var file_job_proto_depIdxs = []int32{ + 0, // 0: job.TaskReply.task:type_name -> job.SyncTask + 0, // 1: job.TaskListReply.list:type_name -> job.SyncTask + 3, // 2: job.Job.CreateSyncTask:input_type -> job.CreateSyncTaskReq + 4, // 3: job.Job.ListSyncTasks:input_type -> job.TaskListReq + 5, // 4: job.Job.DeleteSyncTask:input_type -> job.IdReq + 5, // 5: job.Job.DeployToScheduler:input_type -> job.IdReq + 5, // 6: job.Job.TriggerTask:input_type -> job.IdReq + 5, // 7: job.Job.GetTaskStatus:input_type -> job.IdReq + 5, // 8: job.Job.GetSchedulerInfo:input_type -> job.IdReq + 1, // 9: job.Job.CreateSyncTask:output_type -> job.TaskReply + 2, // 10: job.Job.ListSyncTasks:output_type -> job.TaskListReply + 5, // 11: job.Job.DeleteSyncTask:output_type -> job.IdReq + 6, // 12: job.Job.DeployToScheduler:output_type -> job.DeployReply + 7, // 13: job.Job.TriggerTask:output_type -> job.TriggerReply + 8, // 14: job.Job.GetTaskStatus:output_type -> job.StatusReply + 9, // 15: job.Job.GetSchedulerInfo:output_type -> job.SchedulerReply + 9, // [9:16] is the sub-list for method output_type + 2, // [2:9] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_job_proto_init() } +func file_job_proto_init() { + if File_job_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_job_proto_rawDesc), len(file_job_proto_rawDesc)), + NumEnums: 0, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_job_proto_goTypes, + DependencyIndexes: file_job_proto_depIdxs, + MessageInfos: file_job_proto_msgTypes, + }.Build() + File_job_proto = out.File + file_job_proto_goTypes = nil + file_job_proto_depIdxs = nil +} diff --git a/job/rpc/job/job_grpc.pb.go b/job/rpc/job/job_grpc.pb.go new file mode 100644 index 0000000..95ef1ba --- /dev/null +++ b/job/rpc/job/job_grpc.pb.go @@ -0,0 +1,363 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v7.36.2 +// source: job.proto + +package job + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Job_CreateSyncTask_FullMethodName = "/job.Job/CreateSyncTask" + Job_ListSyncTasks_FullMethodName = "/job.Job/ListSyncTasks" + Job_DeleteSyncTask_FullMethodName = "/job.Job/DeleteSyncTask" + Job_DeployToScheduler_FullMethodName = "/job.Job/DeployToScheduler" + Job_TriggerTask_FullMethodName = "/job.Job/TriggerTask" + Job_GetTaskStatus_FullMethodName = "/job.Job/GetTaskStatus" + Job_GetSchedulerInfo_FullMethodName = "/job.Job/GetSchedulerInfo" +) + +// JobClient is the client API for Job service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type JobClient interface { + //创建同步任务 + CreateSyncTask(ctx context.Context, in *CreateSyncTaskReq, opts ...grpc.CallOption) (*TaskReply, error) + //任务列表 + ListSyncTasks(ctx context.Context, in *TaskListReq, opts ...grpc.CallOption) (*TaskListReply, error) + //删除任务(已部署的先提示需先下线) + DeleteSyncTask(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + //部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因) + DeployToScheduler(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*DeployReply, error) + //手动触发一次 + TriggerTask(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*TriggerReply, error) + //任务状态(本地 + 调度器实例状态) + GetTaskStatus(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*StatusReply, error) + //调度器连接信息(运维视图) + GetSchedulerInfo(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*SchedulerReply, error) +} + +type jobClient struct { + cc grpc.ClientConnInterface +} + +func NewJobClient(cc grpc.ClientConnInterface) JobClient { + return &jobClient{cc} +} + +func (c *jobClient) CreateSyncTask(ctx context.Context, in *CreateSyncTaskReq, opts ...grpc.CallOption) (*TaskReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TaskReply) + err := c.cc.Invoke(ctx, Job_CreateSyncTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *jobClient) ListSyncTasks(ctx context.Context, in *TaskListReq, opts ...grpc.CallOption) (*TaskListReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TaskListReply) + err := c.cc.Invoke(ctx, Job_ListSyncTasks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *jobClient) DeleteSyncTask(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdReq) + err := c.cc.Invoke(ctx, Job_DeleteSyncTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *jobClient) DeployToScheduler(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*DeployReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeployReply) + err := c.cc.Invoke(ctx, Job_DeployToScheduler_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *jobClient) TriggerTask(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*TriggerReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TriggerReply) + err := c.cc.Invoke(ctx, Job_TriggerTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *jobClient) GetTaskStatus(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*StatusReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StatusReply) + err := c.cc.Invoke(ctx, Job_GetTaskStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *jobClient) GetSchedulerInfo(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*SchedulerReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SchedulerReply) + err := c.cc.Invoke(ctx, Job_GetSchedulerInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// JobServer is the server API for Job service. +// All implementations must embed UnimplementedJobServer +// for forward compatibility. +type JobServer interface { + //创建同步任务 + CreateSyncTask(context.Context, *CreateSyncTaskReq) (*TaskReply, error) + //任务列表 + ListSyncTasks(context.Context, *TaskListReq) (*TaskListReply, error) + //删除任务(已部署的先提示需先下线) + DeleteSyncTask(context.Context, *IdReq) (*IdReq, error) + //部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因) + DeployToScheduler(context.Context, *IdReq) (*DeployReply, error) + //手动触发一次 + TriggerTask(context.Context, *IdReq) (*TriggerReply, error) + //任务状态(本地 + 调度器实例状态) + GetTaskStatus(context.Context, *IdReq) (*StatusReply, error) + //调度器连接信息(运维视图) + GetSchedulerInfo(context.Context, *IdReq) (*SchedulerReply, error) + mustEmbedUnimplementedJobServer() +} + +// UnimplementedJobServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedJobServer struct{} + +func (UnimplementedJobServer) CreateSyncTask(context.Context, *CreateSyncTaskReq) (*TaskReply, error) { + return nil, status.Error(codes.Unimplemented, "method CreateSyncTask not implemented") +} +func (UnimplementedJobServer) ListSyncTasks(context.Context, *TaskListReq) (*TaskListReply, error) { + return nil, status.Error(codes.Unimplemented, "method ListSyncTasks not implemented") +} +func (UnimplementedJobServer) DeleteSyncTask(context.Context, *IdReq) (*IdReq, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteSyncTask not implemented") +} +func (UnimplementedJobServer) DeployToScheduler(context.Context, *IdReq) (*DeployReply, error) { + return nil, status.Error(codes.Unimplemented, "method DeployToScheduler not implemented") +} +func (UnimplementedJobServer) TriggerTask(context.Context, *IdReq) (*TriggerReply, error) { + return nil, status.Error(codes.Unimplemented, "method TriggerTask not implemented") +} +func (UnimplementedJobServer) GetTaskStatus(context.Context, *IdReq) (*StatusReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetTaskStatus not implemented") +} +func (UnimplementedJobServer) GetSchedulerInfo(context.Context, *IdReq) (*SchedulerReply, error) { + return nil, status.Error(codes.Unimplemented, "method GetSchedulerInfo not implemented") +} +func (UnimplementedJobServer) mustEmbedUnimplementedJobServer() {} +func (UnimplementedJobServer) testEmbeddedByValue() {} + +// UnsafeJobServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to JobServer will +// result in compilation errors. +type UnsafeJobServer interface { + mustEmbedUnimplementedJobServer() +} + +func RegisterJobServer(s grpc.ServiceRegistrar, srv JobServer) { + // If the following call panics, it indicates UnimplementedJobServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Job_ServiceDesc, srv) +} + +func _Job_CreateSyncTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateSyncTaskReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(JobServer).CreateSyncTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Job_CreateSyncTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(JobServer).CreateSyncTask(ctx, req.(*CreateSyncTaskReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Job_ListSyncTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TaskListReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(JobServer).ListSyncTasks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Job_ListSyncTasks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(JobServer).ListSyncTasks(ctx, req.(*TaskListReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Job_DeleteSyncTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(JobServer).DeleteSyncTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Job_DeleteSyncTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(JobServer).DeleteSyncTask(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Job_DeployToScheduler_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(JobServer).DeployToScheduler(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Job_DeployToScheduler_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(JobServer).DeployToScheduler(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Job_TriggerTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(JobServer).TriggerTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Job_TriggerTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(JobServer).TriggerTask(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Job_GetTaskStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(JobServer).GetTaskStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Job_GetTaskStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(JobServer).GetTaskStatus(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Job_GetSchedulerInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IdReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(JobServer).GetSchedulerInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Job_GetSchedulerInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(JobServer).GetSchedulerInfo(ctx, req.(*IdReq)) + } + return interceptor(ctx, in, info, handler) +} + +// Job_ServiceDesc is the grpc.ServiceDesc for Job service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Job_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "job.Job", + HandlerType: (*JobServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateSyncTask", + Handler: _Job_CreateSyncTask_Handler, + }, + { + MethodName: "ListSyncTasks", + Handler: _Job_ListSyncTasks_Handler, + }, + { + MethodName: "DeleteSyncTask", + Handler: _Job_DeleteSyncTask_Handler, + }, + { + MethodName: "DeployToScheduler", + Handler: _Job_DeployToScheduler_Handler, + }, + { + MethodName: "TriggerTask", + Handler: _Job_TriggerTask_Handler, + }, + { + MethodName: "GetTaskStatus", + Handler: _Job_GetTaskStatus_Handler, + }, + { + MethodName: "GetSchedulerInfo", + Handler: _Job_GetSchedulerInfo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "job.proto", +} diff --git a/job/rpc/jobClient/job.go b/job/rpc/jobClient/job.go new file mode 100644 index 0000000..a3ff227 --- /dev/null +++ b/job/rpc/jobClient/job.go @@ -0,0 +1,96 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.2 +// Source: job.proto + +package jobClient + +import ( + "context" + + "datacenter/job/rpc/job" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + CreateSyncTaskReq = job.CreateSyncTaskReq + DeployReply = job.DeployReply + IdReq = job.IdReq + SchedulerReply = job.SchedulerReply + StatusReply = job.StatusReply + SyncTask = job.SyncTask + TaskListReply = job.TaskListReply + TaskListReq = job.TaskListReq + TaskReply = job.TaskReply + TriggerReply = job.TriggerReply + + Job interface { + // 创建同步任务 + CreateSyncTask(ctx context.Context, in *CreateSyncTaskReq, opts ...grpc.CallOption) (*TaskReply, error) + // 任务列表 + ListSyncTasks(ctx context.Context, in *TaskListReq, opts ...grpc.CallOption) (*TaskListReply, error) + // 删除任务(已部署的先提示需先下线) + DeleteSyncTask(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) + // 部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因) + DeployToScheduler(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*DeployReply, error) + // 手动触发一次 + TriggerTask(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*TriggerReply, error) + // 任务状态(本地 + 调度器实例状态) + GetTaskStatus(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*StatusReply, error) + // 调度器连接信息(运维视图) + GetSchedulerInfo(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*SchedulerReply, error) + } + + defaultJob struct { + cli zrpc.Client + } +) + +func NewJob(cli zrpc.Client) Job { + return &defaultJob{ + cli: cli, + } +} + +// 创建同步任务 +func (m *defaultJob) CreateSyncTask(ctx context.Context, in *CreateSyncTaskReq, opts ...grpc.CallOption) (*TaskReply, error) { + client := job.NewJobClient(m.cli.Conn()) + return client.CreateSyncTask(ctx, in, opts...) +} + +// 任务列表 +func (m *defaultJob) ListSyncTasks(ctx context.Context, in *TaskListReq, opts ...grpc.CallOption) (*TaskListReply, error) { + client := job.NewJobClient(m.cli.Conn()) + return client.ListSyncTasks(ctx, in, opts...) +} + +// 删除任务(已部署的先提示需先下线) +func (m *defaultJob) DeleteSyncTask(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*IdReq, error) { + client := job.NewJobClient(m.cli.Conn()) + return client.DeleteSyncTask(ctx, in, opts...) +} + +// 部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因) +func (m *defaultJob) DeployToScheduler(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*DeployReply, error) { + client := job.NewJobClient(m.cli.Conn()) + return client.DeployToScheduler(ctx, in, opts...) +} + +// 手动触发一次 +func (m *defaultJob) TriggerTask(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*TriggerReply, error) { + client := job.NewJobClient(m.cli.Conn()) + return client.TriggerTask(ctx, in, opts...) +} + +// 任务状态(本地 + 调度器实例状态) +func (m *defaultJob) GetTaskStatus(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*StatusReply, error) { + client := job.NewJobClient(m.cli.Conn()) + return client.GetTaskStatus(ctx, in, opts...) +} + +// 调度器连接信息(运维视图) +func (m *defaultJob) GetSchedulerInfo(ctx context.Context, in *IdReq, opts ...grpc.CallOption) (*SchedulerReply, error) { + client := job.NewJobClient(m.cli.Conn()) + return client.GetSchedulerInfo(ctx, in, opts...) +} From 76b85d4eca3d1381fd2df5c00b218e06a2cba2f6 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 14:10:59 +0800 Subject: [PATCH 09/12] =?UTF-8?q?Phase=203/4=EF=BC=9A=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E9=9B=86=E6=88=90/SQL=20IDE/AI=20=E9=97=AE=E6=95=B0=E4=B8=89?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=20+=20=E5=85=83=E6=95=B0=E6=8D=AE=E6=A3=80?= =?UTF-8?q?=E7=B4=A2=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - job-rpc(:8092):同步任务管控面(DolphinScheduler OpenAPI 客户端以 httptest 22 用例锁定协议),DS 未配置/不可达部署返回明确原因(实测),触发/状态聚合/调度器运维视图 - ide-rpc(:8093):SQL 脚本版本管理(保存自动 version+1,实测 1→2),RunScript 安全闸(仅 SELECT 单语句,非 SELECT 实测拒绝,行数保护截断),经 DSN 真实执行(实测返回 base_app 数据 3ms) - ai-rpc(:8094):AI 问数语义层——关键词提取(停用词)→检索元数据+指标→打分排序→可选 LLM(OpenAI 兼容/可配置)→LLM 失败降级纯检索→全程 dc_ask_log 审计(实测:'投票相关的表'命中 votecount/viewcount 等实体) - 元数据检索增强:keyword 增加 comment 匹配(中文场景按表/列注释命中,实测修复'投票'检索盲点) - docs/schema/phase3.sql:dc_sync_task/dc_ide_script/dc_ask_log - 网关:3 个 .api + 13 路由 + 13 编排 logic Closes #22 Closes #23 Part of #13(#21 job-rpc 代码就绪,真实 DolphinScheduler 联调待实例) --- ai.api | 34 ++ ai/model/asklogmodel.go | 62 +++ ai/rpc/internal/llm/llmclient.go | 120 +++++ ai/rpc/internal/llm/llmclient_test.go | 117 +++++ ai/rpc/internal/logic/askLogic.go | 120 ++++- ai/rpc/internal/logic/keywords.go | 167 +++++++ ai/rpc/internal/logic/keywords_test.go | 85 ++++ ai/rpc/internal/logic/prompt.go | 71 +++ ai/rpc/internal/logic/prompt_test.go | 109 +++++ ai/rpc/internal/logic/semantic.go | 176 ++++++++ ai/rpc/internal/logic/semantic_test.go | 125 ++++++ ai/rpc/internal/svc/serviceContext.go | 51 ++- datacenter.api | 3 + etc/datacenter-api.example.yaml | 15 + ide.api | 82 ++++ ide/model/scriptmodel.go | 169 +++++++ ide/model/vars.go | 49 ++ ide/rpc/internal/logic/convert.go | 20 + ide/rpc/internal/logic/deleteScriptLogic.go | 24 +- ide/rpc/internal/logic/error.go | 19 + ide/rpc/internal/logic/getScriptLogic.go | 18 +- ide/rpc/internal/logic/listScriptsLogic.go | 17 +- ide/rpc/internal/logic/rowscan.go | 63 +++ ide/rpc/internal/logic/runScriptLogic.go | 98 +++- ide/rpc/internal/logic/saveScriptLogic.go | 38 +- ide/rpc/internal/logic/selectsql.go | 76 ++++ ide/rpc/internal/logic/selectsql_test.go | 65 +++ ide/rpc/internal/svc/serviceContext.go | 51 ++- internal/config/config.go | 5 + internal/handler/ai/askhandler.go | 32 ++ internal/handler/ide/deletescripthandler.go | 32 ++ internal/handler/ide/runscripthandler.go | 32 ++ internal/handler/ide/savescripthandler.go | 32 ++ internal/handler/ide/scriptinfohandler.go | 32 ++ internal/handler/ide/scriptlisthandler.go | 32 ++ internal/handler/job/createsynctaskhandler.go | 32 ++ internal/handler/job/deletesynctaskhandler.go | 32 ++ internal/handler/job/deploytaskhandler.go | 32 ++ internal/handler/job/schedulerinfohandler.go | 32 ++ internal/handler/job/synctasklisthandler.go | 32 ++ internal/handler/job/taskstatushandler.go | 32 ++ internal/handler/job/triggertaskhandler.go | 32 ++ internal/handler/routes.go | 96 ++++ internal/logic/ai/asklogic.go | 63 +++ internal/logic/ide/convert.go | 23 + internal/logic/ide/deletescriptlogic.go | 38 ++ internal/logic/ide/runscriptlogic.go | 53 +++ internal/logic/ide/savescriptlogic.go | 46 ++ internal/logic/ide/scriptinfologic.go | 42 ++ internal/logic/ide/scriptlistlogic.go | 58 +++ internal/logic/job/convert.go | 26 ++ internal/logic/job/createsynctasklogic.go | 51 +++ internal/logic/job/deletesynctasklogic.go | 38 ++ internal/logic/job/deploytasklogic.go | 46 ++ internal/logic/job/schedulerinfologic.go | 47 ++ internal/logic/job/synctasklistlogic.go | 58 +++ internal/logic/job/taskstatuslogic.go | 46 ++ internal/logic/job/triggertasklogic.go | 46 ++ internal/svc/servicecontext.go | 12 + internal/types/types.go | 139 ++++++ job.api | 105 +++++ job/model/synctaskmodel.go | 170 +++++++ job/model/vars.go | 69 +++ job/rpc/internal/logic/convert.go | 23 + job/rpc/internal/logic/createSyncTaskLogic.go | 32 +- job/rpc/internal/logic/deleteSyncTaskLogic.go | 17 +- .../internal/logic/deployToSchedulerLogic.go | 34 +- job/rpc/internal/logic/dsclient.go | 383 ++++++++++++++++ job/rpc/internal/logic/dsclient_test.go | 425 ++++++++++++++++++ job/rpc/internal/logic/error.go | 27 ++ .../internal/logic/getSchedulerInfoLogic.go | 28 +- job/rpc/internal/logic/getTaskStatusLogic.go | 36 +- job/rpc/internal/logic/listSyncTasksLogic.go | 23 +- job/rpc/internal/logic/status.go | 137 ++++++ job/rpc/internal/logic/status_test.go | 112 +++++ job/rpc/internal/logic/triggerTaskLogic.go | 31 +- job/rpc/internal/svc/serviceContext.go | 16 +- metadata/model/entitymodel.go | 7 +- restart.sh | 6 + 79 files changed, 5013 insertions(+), 61 deletions(-) create mode 100644 ai.api create mode 100644 ai/model/asklogmodel.go create mode 100644 ai/rpc/internal/llm/llmclient.go create mode 100644 ai/rpc/internal/llm/llmclient_test.go create mode 100644 ai/rpc/internal/logic/keywords.go create mode 100644 ai/rpc/internal/logic/keywords_test.go create mode 100644 ai/rpc/internal/logic/prompt.go create mode 100644 ai/rpc/internal/logic/prompt_test.go create mode 100644 ai/rpc/internal/logic/semantic.go create mode 100644 ai/rpc/internal/logic/semantic_test.go create mode 100644 ide.api create mode 100644 ide/model/scriptmodel.go create mode 100644 ide/model/vars.go create mode 100644 ide/rpc/internal/logic/convert.go create mode 100644 ide/rpc/internal/logic/error.go create mode 100644 ide/rpc/internal/logic/rowscan.go create mode 100644 ide/rpc/internal/logic/selectsql.go create mode 100644 ide/rpc/internal/logic/selectsql_test.go create mode 100644 internal/handler/ai/askhandler.go create mode 100644 internal/handler/ide/deletescripthandler.go create mode 100644 internal/handler/ide/runscripthandler.go create mode 100644 internal/handler/ide/savescripthandler.go create mode 100644 internal/handler/ide/scriptinfohandler.go create mode 100644 internal/handler/ide/scriptlisthandler.go create mode 100644 internal/handler/job/createsynctaskhandler.go create mode 100644 internal/handler/job/deletesynctaskhandler.go create mode 100644 internal/handler/job/deploytaskhandler.go create mode 100644 internal/handler/job/schedulerinfohandler.go create mode 100644 internal/handler/job/synctasklisthandler.go create mode 100644 internal/handler/job/taskstatushandler.go create mode 100644 internal/handler/job/triggertaskhandler.go create mode 100644 internal/logic/ai/asklogic.go create mode 100644 internal/logic/ide/convert.go create mode 100644 internal/logic/ide/deletescriptlogic.go create mode 100644 internal/logic/ide/runscriptlogic.go create mode 100644 internal/logic/ide/savescriptlogic.go create mode 100644 internal/logic/ide/scriptinfologic.go create mode 100644 internal/logic/ide/scriptlistlogic.go create mode 100644 internal/logic/job/convert.go create mode 100644 internal/logic/job/createsynctasklogic.go create mode 100644 internal/logic/job/deletesynctasklogic.go create mode 100644 internal/logic/job/deploytasklogic.go create mode 100644 internal/logic/job/schedulerinfologic.go create mode 100644 internal/logic/job/synctasklistlogic.go create mode 100644 internal/logic/job/taskstatuslogic.go create mode 100644 internal/logic/job/triggertasklogic.go create mode 100644 job.api create mode 100644 job/model/synctaskmodel.go create mode 100644 job/model/vars.go create mode 100644 job/rpc/internal/logic/convert.go create mode 100644 job/rpc/internal/logic/dsclient.go create mode 100644 job/rpc/internal/logic/dsclient_test.go create mode 100644 job/rpc/internal/logic/error.go create mode 100644 job/rpc/internal/logic/status.go create mode 100644 job/rpc/internal/logic/status_test.go diff --git a/ai.api b/ai.api new file mode 100644 index 0000000..aa73dee --- /dev/null +++ b/ai.api @@ -0,0 +1,34 @@ +syntax = "v1" + +//AI 问数(Phase 4:语义检索 + 可选 LLM,全程审计) +type Candidate { + Type string `json:"type,optional"` //entity/metric + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + Detail string `json:"detail,optional"` + Comment string `json:"comment,optional"` +} + +type AskReq { + Question string `json:"question"` + Beid int64 `json:"beid,optional"` + MaxCandidates int32 `json:"max_candidates,optional"` +} +type AskResp { + Candidates []Candidate `json:"candidates"` + LlmEnabled bool `json:"llm_enabled"` + GeneratedSql string `json:"generated_sql,optional"` + Answer string `json:"answer"` + CostMs int64 `json:"cost_ms"` +} + +@server( + group: ai +) +service datacenter-api { + @doc( + summary: "AI 问数" + ) + @handler ask + post /ai/ask (AskReq) returns (AskResp) +} diff --git a/ai/model/asklogmodel.go b/ai/model/asklogmodel.go new file mode 100644 index 0000000..3920c82 --- /dev/null +++ b/ai/model/asklogmodel.go @@ -0,0 +1,62 @@ +package model + +import ( + "context" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// AskLog dc_ask_log 行(AI 问数审计表) +// 注意坑:字段顺序与 askLogRows 列清单一一对应(9 列对 9 字段), +// 若新增查询必须按 askLogRows 的列清单扫描,少列查询扫宽 struct 会报错 +type AskLog struct { + Id int64 `db:"id"` + Question string `db:"question"` + CandidatesJson string `db:"candidates_json"` //语义检索命中的候选(json) + GeneratedSql string `db:"generated_sql"` //LLM 生成的 SQL(未启用为空) + Answer string `db:"answer"` + LlmEnabled int64 `db:"llm_enabled"` //1=本次 LLM 生成成功 0=未启用或已降级 + CostMs int64 `db:"cost_ms"` + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` +} + +// askLogRows 查询列(与 AskLog 字段顺序一致;text 列可空,扫描需 ifnull 兜底避免空值扫描报错) +const askLogRows = "`id`,`question`,ifnull(`candidates_json`,'') as `candidates_json`,ifnull(`generated_sql`,'') as `generated_sql`,ifnull(`answer`,'') as `answer`,`llm_enabled`,`cost_ms`,`beid`,`created_at`" + +const askLogTable = "`dc_ask_log`" + +type ( + // AskLogModel AI 问数审计表 model + AskLogModel interface { + // Insert 写入一条问数审计记录,返回自增 id + Insert(ctx context.Context, data *AskLog) (int64, error) + } + + defaultAskLogModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewAskLogModel 创建问数审计 model +func NewAskLogModel(conn sqlx.SqlConn) AskLogModel { + return &defaultAskLogModel{ + conn: conn, + table: askLogTable, + } +} + +// Insert 写审计记录:显式列清单插入,created_at 由库默认 CURRENT_TIMESTAMP(3) 填充 +func (m *defaultAskLogModel) Insert(ctx context.Context, data *AskLog) (int64, error) { + query := "insert into " + m.table + + " (`question`,`candidates_json`,`generated_sql`,`answer`,`llm_enabled`,`cost_ms`,`beid`) values (?,?,?,?,?,?,?)" + ret, err := m.conn.ExecCtx(ctx, query, + data.Question, data.CandidatesJson, data.GeneratedSql, data.Answer, + data.LlmEnabled, data.CostMs, data.Beid) + if err != nil { + return 0, err + } + return ret.LastInsertId() +} diff --git a/ai/rpc/internal/llm/llmclient.go b/ai/rpc/internal/llm/llmclient.go new file mode 100644 index 0000000..8fa5fe7 --- /dev/null +++ b/ai/rpc/internal/llm/llmclient.go @@ -0,0 +1,120 @@ +// Package llm OpenAI 兼容 chat/completions 客户端封装(标准库 http 实现,可注入 http.Client 便于 httptest 单测) +package llm + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// RequestTimeout 单次 LLM 请求超时(契约要求 30s) +const RequestTimeout = 30 * time.Second + +// maxRespBodySize 响应体读取上限(1MB),防止异常超大响应拖垮内存 +const maxRespBodySize = 1 << 20 + +// Client OpenAI 兼容 LLM 客户端 +type Client struct { + baseUrl string //形如 https://api.openai.com/v1,自动拼 /chat/completions + apiKey string //Bearer 鉴权 + model string //模型名,如 gpt-4o-mini + httpClient *http.Client //可注入,便于 httptest 测试 +} + +// NewClient 创建 LLM 客户端;httpClient 为 nil 时使用带 30s 超时的默认客户端 +func NewClient(baseUrl, apiKey, model string, httpClient *http.Client) *Client { + if httpClient == nil { + httpClient = &http.Client{Timeout: RequestTimeout} + } + return &Client{ + baseUrl: strings.TrimRight(baseUrl, "/"), + apiKey: apiKey, + model: model, + httpClient: httpClient, + } +} + +// chatRequest OpenAI 兼容请求体(仅取所需字段) +type chatRequest struct { + Model string `json:"model"` + Messages []chatMessage `json:"messages"` + Temperature float64 `json:"temperature"` //固定 0,问数场景要求确定性输出 +} + +// chatMessage 对话消息 +type chatMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// chatResponse OpenAI 兼容响应体(仅解析所需字段) +type chatResponse struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` +} + +// ChatCompletion 发送一次对话补全,返回 choices[0].message.content +// 单 prompt 入参:调用方把系统提示+候选 JSON+用户问题拼成完整 prompt 后传入 +func (c *Client) ChatCompletion(ctx context.Context, prompt string) (string, error) { + //30s 超时兜底:即使注入的 http.Client 未设置 Timeout 也不会无限等待 + ctx, cancel := context.WithTimeout(ctx, RequestTimeout) + defer cancel() + + body, err := json.Marshal(chatRequest{ + Model: c.model, + Messages: []chatMessage{{Role: "user", Content: prompt}}, + Temperature: 0, + }) + if err != nil { + return "", fmt.Errorf("LLM 请求体序列化失败: %w", err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseUrl+"/chat/completions", bytes.NewReader(body)) + if err != nil { + return "", fmt.Errorf("LLM 请求构造失败: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if c.apiKey != "" { + req.Header.Set("Authorization", "Bearer "+c.apiKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("LLM 请求失败: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(io.LimitReader(resp.Body, maxRespBodySize)) + if err != nil { + return "", fmt.Errorf("LLM 响应读取失败: %w", err) + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("LLM 接口返回非 200: status=%d body=%s", resp.StatusCode, truncateForLog(string(data))) + } + + var cr chatResponse + if err := json.Unmarshal(data, &cr); err != nil { + return "", fmt.Errorf("LLM 响应解析失败: %w", err) + } + if len(cr.Choices) == 0 { + return "", errors.New("LLM 响应无 choices") + } + return cr.Choices[0].Message.Content, nil +} + +// truncateForLog 错误信息里的响应体截断(避免日志超长) +func truncateForLog(s string) string { + runes := []rune(s) + if len(runes) <= 256 { + return s + } + return string(runes[:256]) + "..." +} diff --git a/ai/rpc/internal/llm/llmclient_test.go b/ai/rpc/internal/llm/llmclient_test.go new file mode 100644 index 0000000..d55923a --- /dev/null +++ b/ai/rpc/internal/llm/llmclient_test.go @@ -0,0 +1,117 @@ +package llm + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// 测试正常请求/响应:URL、方法、Bearer 鉴权头、请求体、choices[0].message.content 解析 +func TestChatCompletionOK(t *testing.T) { + var gotAuth, gotPath, gotMethod, gotContentType string + var gotBody string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + gotMethod = r.Method + gotAuth = r.Header.Get("Authorization") + gotContentType = r.Header.Get("Content-Type") + data, _ := io.ReadAll(r.Body) + gotBody = string(data) + //注意:JSON 里的 ```sql 围栏含反引号,不能放进 Go 原始字符串字面量 + w.Write([]byte("{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"```sql\\nselect 1\\n```\"}}]}")) + })) + defer srv.Close() + + c := NewClient(srv.URL, "sk-test", "gpt-4o-mini", srv.Client()) + content, err := c.ChatCompletion(context.Background(), "上月gmv是多少") + if err != nil { + t.Fatalf("ChatCompletion 报错: %v", err) + } + if content != "```sql\nselect 1\n```" { + t.Errorf("content = %q, 期望 %q", content, "```sql\nselect 1\n```") + } + if gotPath != "/chat/completions" { + t.Errorf("请求路径 = %q, 期望 /chat/completions", gotPath) + } + if gotMethod != http.MethodPost { + t.Errorf("请求方法 = %q, 期望 POST", gotMethod) + } + if gotAuth != "Bearer sk-test" { + t.Errorf("Authorization = %q, 期望 %q", gotAuth, "Bearer sk-test") + } + if !strings.HasPrefix(gotContentType, "application/json") { + t.Errorf("Content-Type = %q, 期望 application/json", gotContentType) + } + //请求体:model / temperature=0 / prompt + for _, want := range []string{`"model":"gpt-4o-mini"`, `"temperature":0`, `"role":"user"`, `"上月gmv是多少"`} { + if !strings.Contains(gotBody, want) { + t.Errorf("请求体缺少 %s: %s", want, gotBody) + } + } +} + +// 测试非 200 响应:返回错误且带状态码 +func TestChatCompletionNon200(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("upstream error")) + })) + defer srv.Close() + + c := NewClient(srv.URL, "sk-test", "gpt-4o-mini", srv.Client()) + _, err := c.ChatCompletion(context.Background(), "问题") + if err == nil { + t.Fatal("期望非 200 返回错误,实际为 nil") + } + if !strings.Contains(err.Error(), "非 200") || !strings.Contains(err.Error(), "500") { + t.Errorf("错误信息应包含状态码: %v", err) + } +} + +// 测试超时:注入短超时 http.Client,服务端 sleep 超过客户端超时 +func TestChatCompletionTimeout(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(300 * time.Millisecond) + w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`)) + })) + defer srv.Close() + + slowClient := &http.Client{Timeout: 50 * time.Millisecond} + c := NewClient(srv.URL, "sk-test", "gpt-4o-mini", slowClient) + _, err := c.ChatCompletion(context.Background(), "问题") + if err == nil { + t.Fatal("期望超时返回错误,实际为 nil") + } + if !strings.Contains(err.Error(), "LLM 请求失败") { + t.Errorf("错误信息应含降级包装前缀: %v", err) + } +} + +// 测试非法响应体:choices 为空返回错误 +func TestChatCompletionNoChoices(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"choices":[]}`)) + })) + defer srv.Close() + + c := NewClient(srv.URL, "sk-test", "gpt-4o-mini", srv.Client()) + _, err := c.ChatCompletion(context.Background(), "问题") + if err == nil || !strings.Contains(err.Error(), "choices") { + t.Errorf("期望空 choices 报错,实际: %v", err) + } +} + +// 测试 baseUrl 尾斜杠清理 +func TestNewClientTrimBaseUrl(t *testing.T) { + c := NewClient("https://api.example.com/v1/", "k", "m", http.DefaultClient) + if c.baseUrl != "https://api.example.com/v1" { + t.Errorf("baseUrl = %q, 期望尾斜杠被清理", c.baseUrl) + } + if c.httpClient != http.DefaultClient { + t.Errorf("httpClient 应保留注入实例") + } +} diff --git a/ai/rpc/internal/logic/askLogic.go b/ai/rpc/internal/logic/askLogic.go index 3385cfa..86cee33 100644 --- a/ai/rpc/internal/logic/askLogic.go +++ b/ai/rpc/internal/logic/askLogic.go @@ -2,13 +2,26 @@ package logic import ( "context" + "fmt" + "strings" + "time" + "unicode/utf8" + "datacenter/ai/model" "datacenter/ai/rpc/ai" + "datacenter/ai/rpc/internal/llm" "datacenter/ai/rpc/internal/svc" "github.com/zeromicro/go-zero/core/logx" ) +const ( + defaultMaxCandidates = 5 //候选数缺省(与 proto 注释一致) + maxCandidatesLimit = 20 //候选数上限(防御异常大值) + maxQuestionRunes = 1024 //dc_ask_log.question 为 varchar(1024) + maxTextBytes = 60000 //dc_ask_log text 列按字节截断兜底(上限 65535) +) + type AskLogic struct { ctx context.Context svcCtx *svc.ServiceContext @@ -25,7 +38,110 @@ func NewAskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AskLogic { // AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 func (l *AskLogic) Ask(in *ai.AskReq) (*ai.AskReply, error) { - // todo: add your logic here and delete this line + start := time.Now() + + //1. question 非空校验 + question := strings.TrimSpace(in.Question) + if question == "" { + return nil, fmt.Errorf("question 不能为空") + } + + maxCand := int(in.MaxCandidates) + if maxCand <= 0 { + maxCand = defaultMaxCandidates + } + if maxCand > maxCandidatesLimit { + maxCand = maxCandidatesLimit + } + + //2. 关键词提取(MVP 分词降级法:去停用词 → 整句 + 分词) + keywords := ExtractKeywords(question) - return &ai.AskReply{}, nil + //3. 语义检索 + 打分排序:元数据实体 + 指标合并候选,截取 top max_candidates + candidates := RankCandidates(l.searchCandidates(l.ctx, in.Beid, keywords, maxCand), keywords, maxCand) + + reply := &ai.AskReply{ + Candidates: candidates, + } + if reply.Candidates == nil { + reply.Candidates = []*ai.Candidate{} + } + + //4. 生成回答:LLM 可选路径(Config.Llm.Enabled),失败降级为纯检索,不返回 error + if l.svcCtx.Config.Llm.Enabled { + // TODO(联调): 需配置真实 LLM ApiKey 验证生成质量 + client := llm.NewClient(l.svcCtx.Config.Llm.BaseUrl, l.svcCtx.Config.Llm.ApiKey, + l.svcCtx.Config.Llm.Model, l.svcCtx.HttpClient) + content, err := client.ChatCompletion(l.ctx, buildAskPrompt(question, candidates)) + if err != nil { + //降级:answer 写明降级原因 + 错误摘要,llm_enabled=false + l.Errorf("LLM 调用失败已降级为纯检索: %v", err) + reply.Answer = fmt.Sprintf("%s\n\n[LLM 调用失败已降级为纯检索] 错误摘要: %v", buildSearchSummary(candidates), err) + } else { + reply.LlmEnabled = true + reply.GeneratedSql = extractSQLBlock(content) + reply.Answer = content + } + } else { + reply.Answer = buildSearchSummary(candidates) + } + reply.CostMs = time.Since(start).Milliseconds() + + //5. 审计:无论成败写 dc_ask_log;审计写失败仅记日志,不阻断返回 + askLog := &model.AskLog{ + Question: truncateRunes(question, maxQuestionRunes), + CandidatesJson: truncateBytes(marshalCandidates(candidates), maxTextBytes), + GeneratedSql: truncateBytes(reply.GeneratedSql, maxTextBytes), + Answer: truncateBytes(reply.Answer, maxTextBytes), + LlmEnabled: boolToInt64(reply.LlmEnabled), + CostMs: reply.CostMs, + Beid: in.Beid, + } + if _, err := l.svcCtx.AskLogModel.Insert(l.ctx, askLog); err != nil { + l.Errorf("写 dc_ask_log 审计失败(不阻断返回): %v", err) + } + + return reply, nil +} + +// buildSearchSummary 无 LLM(或降级)时的检索摘要回答:命中候选清单 + 引导提示 +func buildSearchSummary(candidates []*ai.Candidate) string { + if len(candidates) == 0 { + return "语义检索未命中任何候选(元数据/指标无匹配),请更换关键词,或先在元数据/指标服务完善登记。" + } + parts := make([]string, 0, len(candidates)) + for _, c := range candidates { + parts = append(parts, fmt.Sprintf("[%s] %s(%s)", c.Type, c.Name, c.Detail)) + } + return fmt.Sprintf("语义检索命中 %d 个候选:%s。如需自动生成 SQL,请在配置中启用 LLM。", + len(candidates), strings.Join(parts, "、")) +} + +// truncateRunes 按字符数截断(question 为 varchar(1024) 字符) +func truncateRunes(s string, n int) string { + runes := []rune(s) + if len(runes) <= n { + return s + } + return string(runes[:n]) +} + +// truncateBytes 按字节数截断(text 列上限 65535 字节,按 rune 边界回退避免截半个字符) +func truncateBytes(s string, n int) string { + if len(s) <= n { + return s + } + cut := n + for cut > 0 && !utf8.RuneStart(s[cut]) { + cut-- + } + return s[:cut] +} + +// boolToInt64 bool 转 tinyint +func boolToInt64(b bool) int64 { + if b { + return 1 + } + return 0 } diff --git a/ai/rpc/internal/logic/keywords.go b/ai/rpc/internal/logic/keywords.go new file mode 100644 index 0000000..b799318 --- /dev/null +++ b/ai/rpc/internal/logic/keywords.go @@ -0,0 +1,167 @@ +package logic + +import ( + "sort" + "strings" + "unicode" +) + +// stopWords 问数停用词表(MVP 硬编码):问句中的疑问词、量词、泛义动词等无检索价值词, +// 匹配时按字节长度降序(最长优先),避免 "有多少" 被 "有" 提前截断 +var stopWords = []string{ + // 疑问/量词词组 + "有哪些", "有多少", "是什么", "怎么样", "有没有", "多少", "什么", "哪些", "几个", "怎么", "如何", + // 泛义动作/统计词 + "查询", "统计", "汇总", "列出", "显示", "查看", "看看", "一下", "分别", "情况", "数量", + // 单字虚词/代词/介词 + "的", "了", "吗", "呢", "吧", "是", "有", "在", "和", "与", "或", "及", + "按", "对", "从", "都", "各", "中", "请", "帮", "我", "想", "要", "个", +} + +// sortedStopWords 停用词按字节长度降序副本(包初始化时排好,避免每次请求重排) +var sortedStopWords = func() []string { + s := append([]string(nil), stopWords...) + sort.Slice(s, func(i, j int) bool { return len(s[i]) > len(s[j]) }) + return s +}() + +const ( + maxKeywords = 8 //关键词数上限(控制下游语义检索 RPC 调用次数) + bigramMinLen = 4 //中文块达到该长度才降级切 bigram + minChunkLen = 2 //中文块最短保留长度(单字块噪音大,直接过滤) +) + +// tokenKind 词元类型 +type tokenKind int + +const ( + tokCJK tokenKind = iota //连续中文块 + tokWord //连续英文/数字词 +) + +// token 简易分词词元 +type token struct { + kind tokenKind + text string +} + +// ExtractKeywords 问句关键词提取(MVP 分词降级法,不依赖分词库): +// 1. 去停用词:按硬编码停用词表(最长优先)剔除疑问词/泛义动词等; +// 2. 整句:清洗后剩余词元拼接为紧凑短语,作为首个关键词(精确优先); +// 3. 分词:英文按连续字母数字切词,中文按连续字块切分,长度 >= 4 的中文块 +// 再降级切为二元 bigram 提升模糊匹配命中率(单字中文块直接过滤); +// 4. 去重保序、截断至上限;清洗后为空时兜底返回原问句。 +func ExtractKeywords(question string) []string { + trimmed := strings.TrimSpace(question) + if trimmed == "" { + return nil + } + + //1. 去停用词(统一小写,英文停用词场景兼容) + cleaned := removeStopWords(strings.ToLower(trimmed)) + tokens := splitTokens(cleaned) + if len(tokens) == 0 { + //整句皆为停用词:兜底返回原问句 + return []string{trimmed} + } + + //2. 整句短语:词元直接拼接(中英混排不补空格,贴近表名字段命名习惯) + var phrase strings.Builder + for _, tk := range tokens { + phrase.WriteString(tk.text) + } + + //3. 分词:英文词原样保留,中文块按长度降级 + parts := make([]string, 0, len(tokens)+2) + parts = append(parts, phrase.String()) + for _, tk := range tokens { + if tk.kind == tokWord { + parts = append(parts, tk.text) + continue + } + parts = append(parts, expandChunk(tk.text)...) + } + + //4. 去重保序 + 截断 + return dedupeKeepOrder(parts, maxKeywords) +} + +// removeStopWords 按停用词表(最长优先)剔除停用词,命中处替换为空格分隔 +func removeStopWords(text string) string { + for _, sw := range sortedStopWords { + if strings.Contains(text, sw) { + text = strings.ReplaceAll(text, sw, " ") + } + } + return strings.Join(strings.Fields(text), " ") +} + +// splitTokens 简易切词:连续中文为一个块、连续字母数字为一个词,其余字符视作分隔符 +func splitTokens(text string) []token { + var toks []token + var cur []rune + kind := tokCJK + flush := func() { + if len(cur) > 0 { + toks = append(toks, token{kind: kind, text: string(cur)}) + cur = cur[:0] + } + } + for _, r := range text { + switch { + case unicode.Is(unicode.Han, r): //中文块(先于 IsLetter 判断,Han 亦是 Letter) + if kind != tokCJK { + flush() + kind = tokCJK + } + cur = append(cur, r) + case unicode.IsLetter(r) || unicode.IsDigit(r): //英文/数字词 + if kind != tokWord { + flush() + kind = tokWord + } + cur = append(cur, r) + default: //标点/空白等分隔符 + flush() + } + } + flush() + return toks +} + +// expandChunk 中文块降级切分:>= 4 字切 bigram(提升模糊命中率),2~3 字整块保留,单字过滤 +func expandChunk(s string) []string { + runes := []rune(s) + switch { + case len(runes) >= bigramMinLen: + out := make([]string, 0, len(runes)-1) + for i := 0; i+1 < len(runes); i++ { + out = append(out, string(runes[i:i+2])) + } + return out + case len(runes) >= minChunkLen: + return []string{s} + default: + return nil + } +} + +// dedupeKeepOrder 去重保序并截断至上限 +func dedupeKeepOrder(in []string, limit int) []string { + seen := make(map[string]struct{}, len(in)) + out := make([]string, 0, len(in)) + for _, s := range in { + if s == "" { + continue + } + if _, ok := seen[s]; ok { + continue + } + seen[s] = struct{}{} + out = append(out, s) + if len(out) >= limit { + break + } + } + return out +} diff --git a/ai/rpc/internal/logic/keywords_test.go b/ai/rpc/internal/logic/keywords_test.go new file mode 100644 index 0000000..87a7c65 --- /dev/null +++ b/ai/rpc/internal/logic/keywords_test.go @@ -0,0 +1,85 @@ +package logic + +import ( + "reflect" + "testing" +) + +// 测试停用词剔除 + 整句/分词提取 +func TestExtractKeywords(t *testing.T) { + cases := []struct { + name string + question string + want []string + }{ + { + name: "空问句", + question: " ", + want: nil, + }, + { + name: "纯中文停用词剔除", + question: "查询订单表有多少数据", + //整句在前,短中文块整块保留(<4 不切 bigram) + want: []string{"订单表数据", "订单表", "数据"}, + }, + { + name: "单字虚词剔除", + question: "用户的订单数量", + want: []string{"用户订单", "用户", "订单"}, + }, + { + name: "中英混排", + question: "查询orders表的gmv", + //整句拼接不补空格;单字中文块"表"被过滤 + want: []string{"orders表gmv", "orders", "gmv"}, + }, + { + name: "英文大写统一小写", + question: "查看GMV指标", + want: []string{"gmv指标", "gmv", "指标"}, + }, + { + name: "长中文块切bigram", + question: "订单明细", + want: []string{"订单明细", "订单", "单明", "明细"}, + }, + { + name: "去重保序", + question: "订单订单", + want: []string{"订单订单", "订单", "单订"}, + }, + { + name: "全停用词兜底原问句", + question: "是的", + want: []string{"是的"}, + }, + { + name: "标点分隔", + question: "统计一下:用户表,行数?", + want: []string{"用户表行数", "用户表", "行数"}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := ExtractKeywords(c.question) + if c.want == nil { + if got != nil { + t.Errorf("ExtractKeywords(%q) = %v, 期望 nil", c.question, got) + } + return + } + if !reflect.DeepEqual(got, c.want) { + t.Errorf("ExtractKeywords(%q) = %v, 期望 %v", c.question, got, c.want) + } + }) + } +} + +// 测试多字停用词最长优先剔除:"有多少" 先于 "有"/"少" 处理 +func TestRemoveStopWordsLongestFirst(t *testing.T) { + if got := removeStopWords("有多少用户"); got != "用户" { + t.Errorf("removeStopWords = %q, 期望 %q", got, "用户") + } +} diff --git a/ai/rpc/internal/logic/prompt.go b/ai/rpc/internal/logic/prompt.go new file mode 100644 index 0000000..94e5a33 --- /dev/null +++ b/ai/rpc/internal/logic/prompt.go @@ -0,0 +1,71 @@ +package logic + +import ( + "encoding/json" + "regexp" + "strings" + + "datacenter/ai/rpc/ai" +) + +// askSystemPrompt 系统提示:约束 LLM 基于语义层生成只读 SQL +const askSystemPrompt = "你是数据助手,基于以下元数据/指标语义层生成只读 SQL:" + + "只能生成 select 查询,禁止 insert/update/delete/drop 等任何写操作;" + + "优先使用候选中的表(fqn 为全限定名)与指标口径;无法确定时明确说明,不要臆造表名或字段。" + +// sqlFenceRe ```sql ... ``` 代码块(忽略大小写,跨行) +var sqlFenceRe = regexp.MustCompile(`(?is)` + "```sql\\s*(.*?)```") + +// anyFenceRe 任意 ``` ... ``` 代码块兜底 +var anyFenceRe = regexp.MustCompile(`(?is)` + "```\\s*(.*?)```") + +// buildAskPrompt 构造问数 prompt:系统提示 + 候选 JSON + 用户问题(送 OpenAI 兼容 chat/completions) +func buildAskPrompt(question string, candidates []*ai.Candidate) string { + var b strings.Builder + b.WriteString(askSystemPrompt) + b.WriteString("\n\n候选元数据(JSON):\n") + b.WriteString(marshalCandidates(candidates)) + b.WriteString("\n\n用户问题:") + b.WriteString(question) + b.WriteString("\n请基于候选元数据给出 SQL,并放入 ```sql 代码块中。") + return b.String() +} + +// marshalCandidates 候选序列化为 JSON(prompt 与审计共用;序列化失败兜底空数组) +func marshalCandidates(candidates []*ai.Candidate) string { + list := make([]candidateJson, 0, len(candidates)) + for _, c := range candidates { + if c == nil { + continue + } + list = append(list, candidateJson{ + Type: c.Type, + Id: c.Id, + Name: c.Name, + Detail: c.Detail, + Comment: c.Comment, + }) + } + data, err := json.Marshal(list) + if err != nil { + return "[]" + } + return string(data) +} + +// extractSQLBlock 从 LLM 回复中提取 SQL: +// 优先 ```sql ... ``` 代码块,其次任意 ``` ... ``` 代码块,都没有则裸文本整段兜底 +func extractSQLBlock(content string) string { + if m := sqlFenceRe.FindStringSubmatch(content); len(m) > 1 { + return strings.TrimSpace(m[1]) + } + if m := anyFenceRe.FindStringSubmatch(content); len(m) > 1 { + return strings.TrimSpace(m[1]) + } + //裸文本兜底:剥离未闭合围栏标记后原样返回 + t := strings.TrimSpace(content) + t = strings.TrimPrefix(t, "```sql") + t = strings.TrimPrefix(t, "```") + t = strings.TrimSuffix(t, "```") + return strings.TrimSpace(t) +} diff --git a/ai/rpc/internal/logic/prompt_test.go b/ai/rpc/internal/logic/prompt_test.go new file mode 100644 index 0000000..c320639 --- /dev/null +++ b/ai/rpc/internal/logic/prompt_test.go @@ -0,0 +1,109 @@ +package logic + +import ( + "strings" + "testing" + + "datacenter/ai/rpc/ai" +) + +// 测试 prompt 构造:系统提示 + 候选 JSON + 用户问题 +func TestBuildAskPrompt(t *testing.T) { + candidates := []*ai.Candidate{ + {Type: "entity", Id: 1, Name: "订单表", Detail: "mysql://mall.orders", Comment: "订单主表"}, + {Type: "metric", Id: 2, Name: "GMV", Detail: "sum(amount)", Comment: "成交金额口径"}, + } + prompt := buildAskPrompt("上月gmv是多少", candidates) + + //系统提示(含只读 SQL 约束) + if !strings.Contains(prompt, "你是数据助手") || !strings.Contains(prompt, "只读") { + t.Errorf("prompt 缺少系统提示: %s", prompt) + } + //候选 JSON(含字段名与值) + for _, want := range []string{`"type":"entity"`, `"name":"订单表"`, `"detail":"mysql://mall.orders"`, `"name":"GMV"`, `"detail":"sum(amount)"`} { + if !strings.Contains(prompt, want) { + t.Errorf("prompt 缺少候选 JSON 片段 %s: %s", want, prompt) + } + } + //用户问题 + if !strings.Contains(prompt, "用户问题:上月gmv是多少") { + t.Errorf("prompt 缺少用户问题: %s", prompt) + } + //SQL 代码块约定(供 extractSQLBlock 提取) + if !strings.Contains(prompt, "```sql") { + t.Errorf("prompt 缺少 sql 代码块约定: %s", prompt) + } +} + +// 测试空候选 prompt:JSON 为空数组 +func TestBuildAskPromptEmptyCandidates(t *testing.T) { + prompt := buildAskPrompt("有多少用户", nil) + if !strings.Contains(prompt, "[]") { + t.Errorf("空候选 prompt 应含空数组 JSON: %s", prompt) + } +} + +// 测试 SQL 代码块提取:```sql 块 > 任意 ``` 块 > 裸文本兜底 +func TestExtractSQLBlock(t *testing.T) { + cases := []struct { + name string + content string + want string + }{ + { + name: "标准 sql 代码块", + content: "查询如下:\n```sql\nselect count(*) from mall.orders\n```\n以上。", + want: "select count(*) from mall.orders", + }, + { + name: "SQL 大写围栏与多行", + content: "```SQL\nselect 1\nfrom t\n```", + want: "select 1\nfrom t", + }, + { + name: "无语言标注代码块兜底", + content: "```\nselect 2\n```", + want: "select 2", + }, + { + name: "裸文本兜底", + content: "select 3 from t where id = 1", + want: "select 3 from t where id = 1", + }, + { + name: "未闭合 sql 围栏剥离", + content: "```sql\nselect 4", + want: "select 4", + }, + { + name: "空内容", + content: " ", + want: "", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := extractSQLBlock(c.content); got != c.want { + t.Errorf("extractSQLBlock(%q) = %q, 期望 %q", c.content, got, c.want) + } + }) + } +} + +// 测试检索摘要文本 +func TestBuildSearchSummary(t *testing.T) { + if s := buildSearchSummary(nil); !strings.Contains(s, "未命中任何候选") { + t.Errorf("空候选摘要 = %q, 期望包含未命中提示", s) + } + cands := []*ai.Candidate{ + {Type: "entity", Id: 1, Name: "订单表", Detail: "mysql://mall.orders"}, + {Type: "metric", Id: 2, Name: "GMV", Detail: "sum(amount)"}, + } + s := buildSearchSummary(cands) + for _, want := range []string{"命中 2 个候选", "[entity] 订单表(mysql://mall.orders)", "[metric] GMV(sum(amount))"} { + if !strings.Contains(s, want) { + t.Errorf("摘要缺少 %q: %s", want, s) + } + } +} diff --git a/ai/rpc/internal/logic/semantic.go b/ai/rpc/internal/logic/semantic.go new file mode 100644 index 0000000..0bb0b8b --- /dev/null +++ b/ai/rpc/internal/logic/semantic.go @@ -0,0 +1,176 @@ +package logic + +import ( + "context" + "sort" + "strconv" + "strings" + + "datacenter/ai/rpc/ai" + "datacenter/metadata/rpc/metadataClient" + "datacenter/metric/rpc/metricsvc" +) + +// 打分权重:name 命中 > comment 命中 > detail(fqn/expr) 命中 +const ( + scoreNameHit = 3 //名称命中关键词 + scoreCommentHit = 2 //注释/口径命中关键词 + scoreDetailHit = 1 //fqn/表达式命中关键词 +) + +// candidateJson 候选的稳定 JSON 形态(prompt 与审计 candidates_json 共用,避免 pb 结构序列化噪音) +type candidateJson struct { + Type string `json:"type"` + Id int64 `json:"id"` + Name string `json:"name"` + Detail string `json:"detail"` + Comment string `json:"comment"` +} + +// searchCandidates 语义检索:用提取的关键词分别调元数据实体与指标列表(keyword 模糊,size=max_candidates), +// 合并去重为统一候选;单个关键词或单个下游失败只记日志降级,不阻断问数主流程 +func (l *AskLogic) searchCandidates(ctx context.Context, beid int64, keywords []string, size int) []*ai.Candidate { + if beid <= 0 || len(keywords) == 0 { + return nil + } + //多租户必填校验提前:下游两个列表接口均要求 beid>0 + var entities []*metadataClient.MetaEntity + var ms []*metricsvc.Metric + for _, kw := range keywords { + //元数据实体:name/fqn 模糊匹配 + entResp, err := l.svcCtx.MetadataRpc.ListEntities(ctx, &metadataClient.EntityListReq{ + Beid: beid, + Keyword: kw, + Page: 1, + Size: int64(size), + }) + if err != nil { + l.Errorf("语义检索元数据实体失败(降级跳过): keyword=%s err=%v", kw, err) + } else { + entities = append(entities, entResp.List...) + } + + //指标:name_cn/name_en 模糊匹配 + metricResp, err := l.svcCtx.MetricRpc.ListMetrics(ctx, &metricsvc.MetricListReq{ + Beid: beid, + Keyword: kw, + Page: 1, + Size: int64(size), + }) + if err != nil { + l.Errorf("语义检索指标失败(降级跳过): keyword=%s err=%v", kw, err) + continue + } + ms = append(ms, metricResp.List...) + } + return buildCandidates(entities, ms) +} + +// buildCandidates 实体与指标检索结果转换为统一候选(纯函数): +// entity→detail=fqn、comment 取注释;metric→detail=agg_type(biz_field)(派生指标用 expr)、comment 取口径说明; +// 并按 type+id 去重保序(多关键词命中同一实体/指标时只保留首个) +func buildCandidates(entities []*metadataClient.MetaEntity, ms []*metricsvc.Metric) []*ai.Candidate { + out := make([]*ai.Candidate, 0, len(entities)+len(ms)) + for _, e := range entities { + out = append(out, &ai.Candidate{ + Type: "entity", + Id: e.Id, + Name: e.Name, + Detail: e.Fqn, + Comment: e.Comment, + }) + } + for _, m := range ms { + //口径表达式:原子指标用 agg_type(biz_field),两者缺其一则回退派生表达式 expr + detail := m.Expr + if m.AggType != "" && m.BizField != "" { + detail = m.AggType + "(" + m.BizField + ")" + } + out = append(out, &ai.Candidate{ + Type: "metric", + Id: m.Id, + Name: metricName(m), + Detail: detail, + Comment: m.Comment, + }) + } + return dedupeCandidates(out) +} + +// metricName 指标展示名:中文名优先,为空回退英文名 +func metricName(m *metricsvc.Metric) string { + if m.NameCn != "" { + return m.NameCn + } + return m.NameEn +} + +// dedupeCandidates 按 type+id 去重(多关键词命中同一实体/指标时只保留首个),保序 +func dedupeCandidates(cands []*ai.Candidate) []*ai.Candidate { + seen := make(map[string]struct{}, len(cands)) + out := make([]*ai.Candidate, 0, len(cands)) + for _, c := range cands { + key := c.Type + ":" + strconv.FormatInt(c.Id, 10) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + out = append(out, c) + } + return out +} + +// RankCandidates 候选打分排序(纯函数): +// 对每个关键词,name 含关键词计 3 分 > comment 含计 2 分 > detail(fqn/expr) 含计 1 分,命中数累加计分; +// 按总分降序(同分按命中关键词数降序,再按原始顺序稳定排序),截取 top n +func RankCandidates(cands []*ai.Candidate, keywords []string, n int) []*ai.Candidate { + if len(cands) == 0 || n <= 0 { + return nil + } + type scored struct { + cand *ai.Candidate + score int + hits int + } + list := make([]scored, 0, len(cands)) + for _, c := range cands { + s, h := scoreCandidate(c, keywords) + list = append(list, scored{cand: c, score: s, hits: h}) + } + sort.SliceStable(list, func(i, j int) bool { + if list[i].score != list[j].score { + return list[i].score > list[j].score + } + return list[i].hits > list[j].hits + }) + if n > len(list) { + n = len(list) + } + out := make([]*ai.Candidate, 0, n) + for _, s := range list[:n] { + out = append(out, s.cand) + } + return out +} + +// scoreCandidate 单候选打分(纯函数):返回(总分, 命中的关键词数), +// 比较统一转小写,兼容英文关键词与驼峰/大写字段名 +func scoreCandidate(c *ai.Candidate, keywords []string) (score, hits int) { + name := strings.ToLower(c.Name) + comment := strings.ToLower(c.Comment) + detail := strings.ToLower(c.Detail) + for _, kw := range keywords { + switch { + case kw != "" && strings.Contains(name, kw): + score += scoreNameHit + hits++ + case kw != "" && strings.Contains(comment, kw): + score += scoreCommentHit + hits++ + case kw != "" && strings.Contains(detail, kw): + score += scoreDetailHit + hits++ + } + } + return score, hits +} diff --git a/ai/rpc/internal/logic/semantic_test.go b/ai/rpc/internal/logic/semantic_test.go new file mode 100644 index 0000000..a27ccf1 --- /dev/null +++ b/ai/rpc/internal/logic/semantic_test.go @@ -0,0 +1,125 @@ +package logic + +import ( + "reflect" + "testing" + + "datacenter/ai/rpc/ai" + "datacenter/metadata/rpc/metadataClient" + "datacenter/metric/rpc/metricsvc" +) + +// 测试候选打分:name(3) > comment(2) > detail(1),命中数累加计分 +func TestScoreCandidate(t *testing.T) { + keywords := []string{"订单", "gmv"} + + cases := []struct { + name string + candidate *ai.Candidate + wantScore int + wantHits int + }{ + { + name: "name 命中一个关键词", + candidate: &ai.Candidate{Name: "订单表"}, + wantScore: 3, + wantHits: 1, + }, + { + name: "comment 命中优先级低于 name", + candidate: &ai.Candidate{Name: "用户表", Comment: "含订单信息"}, + wantScore: 2, + wantHits: 1, + }, + { + name: "detail(fqn) 命中优先级最低", + candidate: &ai.Candidate{Name: "支付表", Comment: "支付流水", Detail: "mysql://mall.gmv_fact"}, + wantScore: 1, + wantHits: 1, + }, + { + name: "多关键词命中累加(name+name)", + candidate: &ai.Candidate{Name: "gmv订单视图"}, + wantScore: 6, + wantHits: 2, + }, + { + name: "大小写不敏感", + candidate: &ai.Candidate{Name: "GMV指标"}, + wantScore: 3, + wantHits: 1, + }, + { + name: "全不命中", + candidate: &ai.Candidate{Name: "用户表", Comment: "用户主表", Detail: "mysql://mall.users"}, + wantScore: 0, + wantHits: 0, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + score, hits := scoreCandidate(c.candidate, keywords) + if score != c.wantScore || hits != c.wantHits { + t.Errorf("scoreCandidate = (%d,%d), 期望 (%d,%d)", score, hits, c.wantScore, c.wantHits) + } + }) + } +} + +// 测试排序与截取:分数降序,同分按命中数降序,同分同命中保持稳定 +func TestRankCandidates(t *testing.T) { + a := &ai.Candidate{Type: "entity", Id: 1, Name: "订单表", Detail: "mysql://mall.orders"} //name 命中"订单":3 分 1 命中 + b := &ai.Candidate{Type: "metric", Id: 2, Name: "订单gmv", Comment: "成交金额"} //name 命中两词:6 分 2 命中 + c := &ai.Candidate{Type: "metric", Id: 3, Name: "统计视图", Comment: "订单相关", Detail: "gmv expr"} //comment(2)+detail(1):3 分 2 命中 + d := &ai.Candidate{Type: "entity", Id: 4, Name: "无关联"} //0 分 + in := []*ai.Candidate{a, b, c, d} + got := RankCandidates(in, []string{"订单", "gmv"}, 10) + + //b(6) > c(3分2命中) > a(3分1命中) > d(0) + want := []*ai.Candidate{b, c, a, d} + if !reflect.DeepEqual(got, want) { + t.Errorf("RankCandidates 顺序 = %v, 期望 %v", candIds(got), candIds(want)) + } + + //截取 top n + got2 := RankCandidates(in, []string{"订单", "gmv"}, 2) + if len(got2) != 2 || got2[0] != b || got2[1] != c { + t.Errorf("RankCandidates top2 = %v, 期望 [b c]", candIds(got2)) + } + + //空入参 + if got3 := RankCandidates(nil, []string{"x"}, 5); got3 != nil { + t.Errorf("RankCandidates(nil) = %v, 期望 nil", got3) + } +} + +func candIds(cands []*ai.Candidate) []int64 { + ids := make([]int64, 0, len(cands)) + for _, c := range cands { + ids = append(ids, c.Id) + } + return ids +} + +// 测试实体/指标候选转换:entity→detail=fqn,metric→agg(biz_field)/expr,去重保序 +func TestBuildAndDedupeCandidates(t *testing.T) { + entities := []*metadataClient.MetaEntity{ + {Id: 1, Name: "订单表", Fqn: "mysql://mall.orders", Comment: "订单主表"}, + {Id: 1, Name: "订单表", Fqn: "mysql://mall.orders", Comment: "订单主表"}, //与上一条重复(多关键词命中) + } + ms := []*metricsvc.Metric{ + {Id: 2, NameCn: "GMV", NameEn: "gmv", AggType: "sum", BizField: "amount"}, + {Id: 3, NameCn: "", NameEn: "aov", Expr: "gmv/user_cnt"}, //无中文名回退英文名 + } + + got := buildCandidates(entities, ms) + want := []*ai.Candidate{ + {Type: "entity", Id: 1, Name: "订单表", Detail: "mysql://mall.orders", Comment: "订单主表"}, + {Type: "metric", Id: 2, Name: "GMV", Detail: "sum(amount)"}, + {Type: "metric", Id: 3, Name: "aov", Detail: "gmv/user_cnt"}, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("候选合并结果 = %v, 期望 %v", got, want) + } +} diff --git a/ai/rpc/internal/svc/serviceContext.go b/ai/rpc/internal/svc/serviceContext.go index 8d075cb..c532262 100644 --- a/ai/rpc/internal/svc/serviceContext.go +++ b/ai/rpc/internal/svc/serviceContext.go @@ -1,13 +1,58 @@ package svc -import "datacenter/ai/rpc/internal/config" +import ( + "net/http" + + "datacenter/ai/model" + "datacenter/ai/rpc/internal/config" + "datacenter/ai/rpc/internal/llm" + "datacenter/metadata/rpc/metadataClient" + "datacenter/metric/rpc/metricsvc" + + "github.com/zeromicro/go-zero/core/discov" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/zrpc" +) + +// 跨服务 rpc 的 etcd key(契约固定,与各服务注册的 ListenOn key 一致) +const ( + metadataEtcdKey = "metadata.rpc" //元数据 rpc + metricEtcdKey = "metric.rpc" //指标 rpc +) type ServiceContext struct { - Config config.Config + Config config.Config + Conn sqlx.SqlConn //中台主库连接(dc_ask_log 审计表) + AskLogModel model.AskLogModel //问数审计 model + MetadataRpc metadataClient.Metadata //元数据 rpc(语义检索实体) + MetricRpc metricsvc.MetricSvc //指标 rpc(语义检索指标) + HttpClient *http.Client //LLM HTTP 客户端(30s 超时,llm 包内另有 ctx 超时兜底) } func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) + //config.go 不可改,参考 tag 服务内联装配写法; + //NonBlock: true 保证 metadata/metric 未就绪时不阻塞本服务启动 + mdRpc := metadataClient.NewMetadata(zrpc.MustNewClient(zrpc.RpcClientConf{ + Etcd: discov.EtcdConf{ + Hosts: c.Etcd.Hosts, + Key: metadataEtcdKey, + }, + NonBlock: true, + })) + metricRpc := metricsvc.NewMetricSvc(zrpc.MustNewClient(zrpc.RpcClientConf{ + Etcd: discov.EtcdConf{ + Hosts: c.Etcd.Hosts, + Key: metricEtcdKey, + }, + NonBlock: true, + })) return &ServiceContext{ - Config: c, + Config: c, + Conn: conn, + AskLogModel: model.NewAskLogModel(conn), + MetadataRpc: mdRpc, + MetricRpc: metricRpc, + HttpClient: &http.Client{Timeout: llm.RequestTimeout}, } } diff --git a/datacenter.api b/datacenter.api index f1b2477..94e4a50 100644 --- a/datacenter.api +++ b/datacenter.api @@ -15,6 +15,9 @@ import "catalog.api" import "quality.api" import "metric.api" import "tag.api" +import "job.api" +import "ide.api" +import "ai.api" import "votes.api" import "search.api" import "questions.api" diff --git a/etc/datacenter-api.example.yaml b/etc/datacenter-api.example.yaml index 318c18c..67457df 100644 --- a/etc/datacenter-api.example.yaml +++ b/etc/datacenter-api.example.yaml @@ -69,3 +69,18 @@ TagRpc: Hosts: - 127.0.0.1:2379 Key: tag.rpc +JobRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: job.rpc +IdeRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: ide.rpc +AiRpc: + Etcd: + Hosts: + - 127.0.0.1:2379 + Key: ai.rpc diff --git a/ide.api b/ide.api new file mode 100644 index 0000000..a687247 --- /dev/null +++ b/ide.api @@ -0,0 +1,82 @@ +syntax = "v1" + +//SQL IDE(Phase 3) +type Script { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + SqlText string `json:"sql_text,optional"` + Version int32 `json:"version,optional"` + DatasourceId int64 `json:"datasource_id,optional"` + DbName string `json:"db_name,optional"` + LastRunAt int64 `json:"last_run_at,optional"` + Beid int64 `json:"beid,optional"` +} + +type SaveScriptReq { + Id int64 `json:"id,optional"` + Name string `json:"name"` + SqlText string `json:"sql_text"` + DatasourceId int64 `json:"datasource_id"` + DbName string `json:"db_name,optional"` + Beid int64 `json:"beid,optional"` +} + +type ScriptIdReq { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type ScriptListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type ScriptListResp { + List []Script `json:"list"` + Total int64 `json:"total"` +} + +type RunScriptReq { + Id int64 `json:"id"` + Beid int64 `json:"beid,optional"` + MaxRows int32 `json:"max_rows,optional"` +} +type RunScriptResp { + ColumnsJson string `json:"columns_json"` + RowsJson string `json:"rows_json"` + RowCount int64 `json:"row_count"` + CostMs int64 `json:"cost_ms"` + Truncated bool `json:"truncated"` +} + +@server( + group: ide +) +service datacenter-api { + @doc( + summary: "保存脚本" + ) + @handler saveScript + post /ide/script/save (SaveScriptReq) returns (Script) + @doc( + summary: "脚本列表" + ) + @handler scriptList + get /ide/script/list (ScriptListReq) returns (ScriptListResp) + @doc( + summary: "脚本详情" + ) + @handler scriptInfo + get /ide/script/info (ScriptIdReq) returns (Script) + @doc( + summary: "删除脚本" + ) + @handler deleteScript + post /ide/script/delete (ScriptIdReq) + @doc( + summary: "执行脚本(仅 SELECT,行数保护)" + ) + @handler runScript + post /ide/script/run (RunScriptReq) returns (RunScriptResp) +} diff --git a/ide/model/scriptmodel.go b/ide/model/scriptmodel.go new file mode 100644 index 0000000..6113020 --- /dev/null +++ b/ide/model/scriptmodel.go @@ -0,0 +1,169 @@ +package model + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + scriptTable = "`dc_ide_script`" + // scriptInsertColumns 插入字段(不含自增 id 与时间列;version 新建固定 1,更新时在库上自增) + scriptInsertColumns = "`name`,`sql_text`,`datasource_id`,`db_name`,`beid`" +) + +// scriptRows 查询列(last_run_at 允许 NULL,转毫秒时间戳并以 0 兜底避免扫描报错; +// 列数与 Script 字段数一一对应(10 列对 10 字段),规避 go-zero 少列查询扫宽 struct +// 的 "not matching destination to scan" 报错) +const scriptRows = "`id`,`name`,`sql_text`,`version`,`datasource_id`,`db_name`," + + "ifnull(cast(unix_timestamp(`last_run_at`) * 1000 as signed), 0) as `last_run_at`," + + "`beid`,`created_at`,`updated_at`" + +// Script dc_ide_script 行(SQL 脚本) +type ( + Script struct { + Id int64 `db:"id"` + Name string `db:"name"` + SqlText string `db:"sql_text"` // SQL 内容(mediumtext) + Version int32 `db:"version"` // 版本号,每次保存 +1 + DatasourceId int64 `db:"datasource_id"` + DbName string `db:"db_name"` // 目标库(空则使用数据源默认库) + LastRunAt int64 `db:"last_run_at"` // 最近执行时间(毫秒时间戳,未运行为 0) + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + // ScriptModel SQL 脚本表操作接口 + ScriptModel interface { + // Save 保存脚本:id=0 新建(version=1);id>0 更新内容且 version=version+1(回读整行返回新版本号) + Save(ctx context.Context, data *Script) (*Script, error) + // FindOne 按主键查询(beid>0 时追加多租户过滤) + FindOne(ctx context.Context, id, beid int64) (*Script, error) + // FindList 分页列表(beid 必带,keyword 模糊匹配 name),返回列表与总数 + FindList(ctx context.Context, beid int64, keyword string, page, size int64) ([]*Script, int64, error) + // Delete 删除脚本(按 id+beid 定位) + Delete(ctx context.Context, id, beid int64) error + // UpdateLastRunAt 脚本执行完成后回填最近执行时间 + UpdateLastRunAt(ctx context.Context, id, beid int64, runAt time.Time) error + } + + defaultScriptModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewScriptModel 创建 SQL 脚本 model +func NewScriptModel(conn sqlx.SqlConn) ScriptModel { + return &defaultScriptModel{ + conn: conn, + table: scriptTable, + } +} + +// Save 保存脚本(新建与更新主键不同,insert ... on duplicate 不适用,拆成两条链路): +// id=0 走 insert,version 固定 1;id>0 走 update,version=version+1 在库上自增 +// (规避读-改-写的并发丢版本),随后回读整行(含新版本号)返回 +func (m *defaultScriptModel) Save(ctx context.Context, data *Script) (*Script, error) { + if data.Id <= 0 { + query := fmt.Sprintf("insert into %s (%s,`version`) values (?,?,?,?,?,1)", m.table, scriptInsertColumns) + ret, err := m.conn.ExecCtx(ctx, query, + data.Name, data.SqlText, data.DatasourceId, data.DbName, data.Beid) + if err != nil { + return nil, err + } + id, err := ret.LastInsertId() + if err != nil { + return nil, err + } + data.Id = id + data.Version = 1 + return data, nil + } + + // 更新:version 在库上 +1,内容列整体覆盖 + query := fmt.Sprintf("update %s set `name` = ?, `sql_text` = ?, `datasource_id` = ?, `db_name` = ?, "+ + "`version` = `version` + 1 where `id` = ? and `beid` = ?", m.table) + ret, err := m.conn.ExecCtx(ctx, query, + data.Name, data.SqlText, data.DatasourceId, data.DbName, data.Id, data.Beid) + if err != nil { + return nil, err + } + // 影响行数为 0:记录不存在或不归属当前租户(并发被删的兜底) + if affected, aerr := ret.RowsAffected(); aerr == nil && affected == 0 { + return nil, ErrNotFound + } + // 回读更新后的整行(含新版本号) + return m.FindOne(ctx, data.Id, data.Beid) +} + +// FindOne 按主键查询脚本 +func (m *defaultScriptModel) FindOne(ctx context.Context, id, beid int64) (*Script, error) { + query := fmt.Sprintf("select %s from %s where `id` = ?", scriptRows, m.table) + args := []any{id} + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var resp Script + err := m.conn.QueryRowCtx(ctx, &resp, query+" limit 1", args...) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindList 分页查询脚本列表(beid 多租户必带),keyword 非空时模糊匹配 name +func (m *defaultScriptModel) FindList(ctx context.Context, beid int64, keyword string, page, size int64) ([]*Script, int64, error) { + if beid <= 0 { + return nil, 0, ErrBeidRequired + } + page, size = normalizePage(page, size) + + where := "`beid` = ?" + args := []any{beid} + if kw := strings.TrimSpace(keyword); kw != "" { + where += " and `name` like ?" + args = append(args, "%"+escapeLike(kw)+"%") + } + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页数据 + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ?, ?", + scriptRows, m.table, where) + args = append(args, (page-1)*size, size) + var list []*Script + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} + +// Delete 删除脚本(按 id+beid 定位,防止越权删除其他租户脚本) +func (m *defaultScriptModel) Delete(ctx context.Context, id, beid int64) error { + query := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, id, beid) + return err +} + +// UpdateLastRunAt 脚本执行完成后回填最近执行时间 +func (m *defaultScriptModel) UpdateLastRunAt(ctx context.Context, id, beid int64, runAt time.Time) error { + query := fmt.Sprintf("update %s set `last_run_at` = ? where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, runAt, id, beid) + return err +} diff --git a/ide/model/vars.go b/ide/model/vars.go new file mode 100644 index 0000000..65cc9f1 --- /dev/null +++ b/ide/model/vars.go @@ -0,0 +1,49 @@ +package model + +import ( + "errors" + "strings" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// 通用错误 +var ( + // ErrNotFound 记录不存在 + ErrNotFound = sqlx.ErrNotFound + // ErrBeidRequired 多租户 beid 缺失 + ErrBeidRequired = errors.New("beid 不能为空") +) + +// 分页缺省值与上限 +const ( + defaultPageSize = 10 + maxPageSize = 200 +) + +// placeholders 生成 n 个 ? 占位符(逗号分隔) +func placeholders(n int) string { + return strings.TrimSuffix(strings.Repeat("?,", n), ",") +} + +// normalizePage 分页参数防御:page 缺省 1,size 缺省 10、封顶 200 +func normalizePage(page, size int64) (int64, int64) { + if page < 1 { + page = 1 + } + if size <= 0 { + size = defaultPageSize + } + if size > maxPageSize { + size = maxPageSize + } + return page, size +} + +// escapeLike 转义 like 通配符(%/_/\),防止用户输入干扰模糊匹配 +func escapeLike(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `%`, `\%`) + s = strings.ReplaceAll(s, `_`, `\_`) + return s +} diff --git a/ide/rpc/internal/logic/convert.go b/ide/rpc/internal/logic/convert.go new file mode 100644 index 0000000..fedc0df --- /dev/null +++ b/ide/rpc/internal/logic/convert.go @@ -0,0 +1,20 @@ +package logic + +import ( + "datacenter/ide/model" + "datacenter/ide/rpc/ide" +) + +// scriptToPb 脚本 model -> pb(last_run_at 为毫秒时间戳,未运行为 0) +func scriptToPb(s *model.Script) *ide.Script { + return &ide.Script{ + Id: s.Id, + Name: s.Name, + SqlText: s.SqlText, + Version: s.Version, + DatasourceId: s.DatasourceId, + DbName: s.DbName, + LastRunAt: s.LastRunAt, + Beid: s.Beid, + } +} diff --git a/ide/rpc/internal/logic/deleteScriptLogic.go b/ide/rpc/internal/logic/deleteScriptLogic.go index 41179ea..c994406 100644 --- a/ide/rpc/internal/logic/deleteScriptLogic.go +++ b/ide/rpc/internal/logic/deleteScriptLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "errors" + "datacenter/ide/model" "datacenter/ide/rpc/ide" "datacenter/ide/rpc/internal/svc" @@ -23,9 +25,23 @@ func NewDeleteScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Dele } } -// 删除脚本 +// 删除脚本:先确认存在且归属当前租户(避免误报删除成功),再按 id+beid 定位删除 func (l *DeleteScriptLogic) DeleteScript(in *ide.IdReq) (*ide.IdReq, error) { - // todo: add your logic here and delete this line - - return &ide.IdReq{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + if in.Id <= 0 { + return nil, ErrScriptNotFound + } + if _, err := l.svcCtx.ScriptModel.FindOne(l.ctx, in.Id, in.Beid); err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrScriptNotFound + } + return nil, err + } + if err := l.svcCtx.ScriptModel.Delete(l.ctx, in.Id, in.Beid); err != nil { + return nil, err + } + l.Infof("[DeleteScript] id=%d beid=%d", in.Id, in.Beid) + return &ide.IdReq{Id: in.Id, Beid: in.Beid}, nil } diff --git a/ide/rpc/internal/logic/error.go b/ide/rpc/internal/logic/error.go new file mode 100644 index 0000000..c556de4 --- /dev/null +++ b/ide/rpc/internal/logic/error.go @@ -0,0 +1,19 @@ +package logic + +import "errors" + +// 业务错误(对齐 metric/tag 服务写法:errors.New 中文语义错误) +var ( + // ErrBeidRequired 租户 beid 缺失 + ErrBeidRequired = errors.New("beid 不能为空") + // ErrNameRequired 脚本名必填 + ErrNameRequired = errors.New("脚本名 name 不能为空") + // ErrScriptNotFound 脚本不存在(或不属于当前租户) + ErrScriptNotFound = errors.New("脚本不存在") + // ErrInvalidSql 脚本 SQL 未通过安全闸(仅允许单条 SELECT) + ErrInvalidSql = errors.New("脚本 SQL 校验未通过") + // ErrDatasource 数据源 rpc 调用失败或未返回 DSN + ErrDatasource = errors.New("获取目标数据源失败") + // ErrScriptExec 脚本执行失败 + ErrScriptExec = errors.New("脚本执行失败") +) diff --git a/ide/rpc/internal/logic/getScriptLogic.go b/ide/rpc/internal/logic/getScriptLogic.go index 14b58f3..95c8653 100644 --- a/ide/rpc/internal/logic/getScriptLogic.go +++ b/ide/rpc/internal/logic/getScriptLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "errors" + "datacenter/ide/model" "datacenter/ide/rpc/ide" "datacenter/ide/rpc/internal/svc" @@ -23,9 +25,17 @@ func NewGetScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetScri } } -// 脚本详情 +// 脚本详情(FindOne 已带多租户过滤,越权/不存在统一按脚本不存在处理) func (l *GetScriptLogic) GetScript(in *ide.IdReq) (*ide.ScriptReply, error) { - // todo: add your logic here and delete this line - - return &ide.ScriptReply{}, nil + if in.Id <= 0 { + return nil, ErrScriptNotFound + } + s, err := l.svcCtx.ScriptModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrScriptNotFound + } + return nil, err + } + return &ide.ScriptReply{Script: scriptToPb(s)}, nil } diff --git a/ide/rpc/internal/logic/listScriptsLogic.go b/ide/rpc/internal/logic/listScriptsLogic.go index f012f3a..c76861e 100644 --- a/ide/rpc/internal/logic/listScriptsLogic.go +++ b/ide/rpc/internal/logic/listScriptsLogic.go @@ -23,9 +23,18 @@ func NewListScriptsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListS } } -// 脚本列表 +// 脚本列表:beid 多租户必带,keyword 模糊匹配 name,分页返回列表与总数 func (l *ListScriptsLogic) ListScripts(in *ide.ScriptListReq) (*ide.ScriptListReply, error) { - // todo: add your logic here and delete this line - - return &ide.ScriptListReply{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + list, total, err := l.svcCtx.ScriptModel.FindList(l.ctx, in.Beid, in.Keyword, in.Page, in.Size) + if err != nil { + return nil, err + } + reply := &ide.ScriptListReply{Total: total} + for _, s := range list { + reply.List = append(reply.List, scriptToPb(s)) + } + return reply, nil } diff --git a/ide/rpc/internal/logic/rowscan.go b/ide/rpc/internal/logic/rowscan.go new file mode 100644 index 0000000..b462467 --- /dev/null +++ b/ide/rpc/internal/logic/rowscan.go @@ -0,0 +1,63 @@ +package logic + +import ( + "context" + "database/sql" +) + +// QueryRowsAsMaps 在目标库执行 SELECT,把结果集转为通用行集(手工扫描模式,参考 datasvc executor.go): +// - 按结果集列名定位取值,不依赖扫描 struct,规避 go-zero sqlx 少列查询扫宽 struct +// 的 "not matching destination to scan" 报错; +// - 驱动返回的 []byte(varchar/text/decimal 等)统一转 string,便于 JSON 输出; +// - maxRows 保护性截断:达到行数上限且仍有下一行时停止拉取,truncated=true 标识; +// 提前 break 后由 defer rs.Close() 归还连接,剩余行由驱动丢弃。 +func QueryRowsAsMaps(ctx context.Context, db *sql.DB, query string, maxRows int) ([]string, []map[string]interface{}, bool, error) { + rs, err := db.QueryContext(ctx, query) + if err != nil { + return nil, nil, false, err + } + defer rs.Close() + cols, err := rs.Columns() + if err != nil { + return nil, nil, false, err + } + if cols == nil { + cols = []string{} + } + truncated := false + rows := make([]map[string]interface{}, 0, 16) + for rs.Next() { + if len(rows) >= maxRows { + //已达上限且仍有下一行 → 截断 + truncated = true + break + } + container := make([]interface{}, len(cols)) + ptrs := make([]interface{}, len(cols)) + for i := range container { + ptrs[i] = &container[i] + } + if err := rs.Scan(ptrs...); err != nil { + return nil, nil, false, err + } + row := make(map[string]interface{}, len(cols)) + for i, c := range cols { + row[c] = normalizeDbValue(container[i]) + } + rows = append(rows, row) + } + if err := rs.Err(); err != nil { + return nil, nil, false, err + } + return cols, rows, truncated, nil +} + +// normalizeDbValue 驱动返回值归一化(仅 []byte → string,其余保持原样) +func normalizeDbValue(v interface{}) interface{} { + switch t := v.(type) { + case []byte: + return string(t) + default: + return v + } +} diff --git a/ide/rpc/internal/logic/runScriptLogic.go b/ide/rpc/internal/logic/runScriptLogic.go index e7473b5..f5b30d9 100644 --- a/ide/rpc/internal/logic/runScriptLogic.go +++ b/ide/rpc/internal/logic/runScriptLogic.go @@ -2,13 +2,23 @@ package logic import ( "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/ide/model" "datacenter/ide/rpc/ide" "datacenter/ide/rpc/internal/svc" "github.com/zeromicro/go-zero/core/logx" ) +// runDefaultMaxRows 行数保护缺省上限(入参 max_rows<=0 时生效) +const runDefaultMaxRows = 100 + type RunScriptLogic struct { ctx context.Context svcCtx *svc.ServiceContext @@ -23,9 +33,91 @@ func NewRunScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RunScri } } -// 执行脚本(开发调试:仅 SELECT,行数保护截断) +// 执行脚本(核心链路,任一步失败立即拒绝): +// 取脚本(存在性+beid 归属)→ 安全闸(剥注释与前导括号后必须以 select 开头,禁止分号多语句)→ +// datasource rpc 换目标库 DSN(with_secret=true)→ 目标库连接池按 DSN 缓存 → +// 手工扫描结果集(max_rows<=0 默认 100,超出截断 truncated=true)→ +// 回填 last_run_at → 返回 columns_json/rows_json/row_count/cost_ms/truncated func (l *RunScriptLogic) RunScript(in *ide.RunScriptReq) (*ide.RunScriptReply, error) { - // todo: add your logic here and delete this line + start := time.Now() + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + if in.Id <= 0 { + return nil, ErrScriptNotFound + } + + //1. 取脚本(FindOne 已带多租户过滤,越权/不存在统一按脚本不存在处理) + script, err := l.svcCtx.ScriptModel.FindOne(l.ctx, in.Id, in.Beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrScriptNotFound + } + return nil, err + } + + //2. 安全闸:仅放行单条 SELECT(写语句与多语句注入片段一律拒绝) + if err := ValidateSelectSql(script.SqlText); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidSql, err) + } + + //3. datasource rpc 换目标库 DSN(with_secret=true 返回解密密码与拼好的连接串) + if script.DatasourceId <= 0 { + return nil, fmt.Errorf("%w: 脚本未配置数据源", ErrDatasource) + } + dsReply, err := l.svcCtx.DataSourceRpc.GetDataSource(l.ctx, &dataSourceClient.GetDataSourceReq{ + Id: script.DatasourceId, + Beid: in.Beid, + WithSecret: true, + }) + if err != nil { + return nil, fmt.Errorf("%w: 数据源 %d: %v", ErrDatasource, script.DatasourceId, err) + } + if strings.TrimSpace(dsReply.GetDsn()) == "" { + return nil, fmt.Errorf("%w: 数据源 %d 未返回 DSN", ErrDatasource, script.DatasourceId) + } - return &ide.RunScriptReply{}, nil + //4. 目标库连接(ServiceContext 按 DSN 缓存连接池);RawDB 不可 Close,底层连接池需复用 + conn, err := l.svcCtx.TargetConn(dsReply.GetDsn()) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrDatasource, err) + } + db, err := conn.RawDB() + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrDatasource, err) + } + + //5. 行数保护:max_rows<=0 默认 100,超出截断 + maxRows := int(in.MaxRows) + if maxRows <= 0 { + maxRows = runDefaultMaxRows + } + columns, rows, truncated, err := QueryRowsAsMaps(l.ctx, db, script.SqlText, maxRows) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrScriptExec, err) + } + + //6. 回填最近执行时间(失败仅记日志,不影响结果返回) + if uerr := l.svcCtx.ScriptModel.UpdateLastRunAt(l.ctx, script.Id, in.Beid, time.Now()); uerr != nil { + l.Errorf("[RunScript] 回填 last_run_at 失败 script=%d err=%v", script.Id, uerr) + } + + //7. 结果序列化:columns_json=列名数组,rows_json=通用行集数组(空结果输出 []) + columnsJson, err := json.Marshal(columns) + if err != nil { + return nil, fmt.Errorf("%w: 列信息序列化失败: %v", ErrScriptExec, err) + } + rowsJson, err := json.Marshal(rows) + if err != nil { + return nil, fmt.Errorf("%w: 结果集序列化失败: %v", ErrScriptExec, err) + } + l.Infof("[RunScript] script=%d datasource=%d rows=%d truncated=%v cost=%dms", + script.Id, script.DatasourceId, len(rows), truncated, time.Since(start).Milliseconds()) + return &ide.RunScriptReply{ + ColumnsJson: string(columnsJson), + RowsJson: string(rowsJson), + RowCount: int64(len(rows)), + CostMs: time.Since(start).Milliseconds(), + Truncated: truncated, + }, nil } diff --git a/ide/rpc/internal/logic/saveScriptLogic.go b/ide/rpc/internal/logic/saveScriptLogic.go index 10eb0a4..615fa46 100644 --- a/ide/rpc/internal/logic/saveScriptLogic.go +++ b/ide/rpc/internal/logic/saveScriptLogic.go @@ -2,7 +2,10 @@ package logic import ( "context" + "errors" + "strings" + "datacenter/ide/model" "datacenter/ide/rpc/ide" "datacenter/ide/rpc/internal/svc" @@ -23,9 +26,36 @@ func NewSaveScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SaveSc } } -// 保存脚本(新建或更新,更新自动 version+1) +// 保存脚本(新建或更新,更新自动 version+1): +// name 必填;id>0 时先校验脚本存在且归属当前租户(FindOne 已带多租户过滤), +// 再交由 model.Save 走更新链路(version 在库上 +1 并回读新版本号) func (l *SaveScriptLogic) SaveScript(in *ide.SaveScriptReq) (*ide.ScriptReply, error) { - // todo: add your logic here and delete this line - - return &ide.ScriptReply{}, nil + if in.Beid <= 0 { + return nil, ErrBeidRequired + } + if strings.TrimSpace(in.Name) == "" { + return nil, ErrNameRequired + } + if in.Id > 0 { + //更新:存在性 + beid 归属校验,不存在/越权统一按脚本不存在处理 + if _, err := l.svcCtx.ScriptModel.FindOne(l.ctx, in.Id, in.Beid); err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrScriptNotFound + } + return nil, err + } + } + saved, err := l.svcCtx.ScriptModel.Save(l.ctx, &model.Script{ + Id: in.Id, + Name: strings.TrimSpace(in.Name), + SqlText: in.SqlText, + DatasourceId: in.DatasourceId, + DbName: in.DbName, + Beid: in.Beid, + }) + if err != nil { + return nil, err + } + l.Infof("[SaveScript] id=%d version=%d beid=%d", saved.Id, saved.Version, in.Beid) + return &ide.ScriptReply{Script: scriptToPb(saved)}, nil } diff --git a/ide/rpc/internal/logic/selectsql.go b/ide/rpc/internal/logic/selectsql.go new file mode 100644 index 0000000..aabfc95 --- /dev/null +++ b/ide/rpc/internal/logic/selectsql.go @@ -0,0 +1,76 @@ +package logic + +import ( + "fmt" + "strings" +) + +// ValidateSelectSql 脚本执行前的安全闸(纯函数,便于单测),思路参考 tag 服务 ValidateRuleSql: +// 1. 剥掉前导注释(-- 行注释、# 行注释、块注释)与前导括号后,必须以 select 开头(只读限制, +// 杜绝 update/delete/drop 等写语句,也拒绝 with ... select 的 CTE 包装); +// 2. 禁止分号(单语句限制,天然拒绝 `select 1; drop table ...` 等多语句注入片段, +// 结尾分号同样拒绝,与 tag 服务口径一致); +// 3. 已知限制:字符串字面量中的分号同样会被拒绝(宁严勿漏)。 +func ValidateSelectSql(sqlText string) error { + s, ok := stripLeadingNoise(sqlText) + if !ok { + return fmt.Errorf("SQL 存在未闭合的块注释") + } + if s == "" { + return fmt.Errorf("SQL 不能为空") + } + if len(s) < 6 || !strings.EqualFold(s[:6], "select") { + return fmt.Errorf("仅允许 SELECT 查询") + } + //词边界校验:select 后必须是空白或结尾,杜绝 selectx 之类的标识符冒充 + if tail := s[6:]; tail != "" && !isSpaceByte(tail[0]) { + return fmt.Errorf("仅允许 SELECT 查询") + } + if strings.Contains(s, ";") { + return fmt.Errorf("仅允许单条语句,禁止分号(拒绝 ; drop 等多语句注入片段)") + } + return nil +} + +// stripLeadingNoise 循环剥掉语句头部的空白、注释与前导括号,返回剩余语句文本; +// ok=false 表示出现未闭合的块注释。循环剥离支持注释与括号任意交错(如 "/* a */ ( /* b */ select ...") +func stripLeadingNoise(sqlText string) (string, bool) { + s := sqlText + for { + //跳过前导空白 + for len(s) > 0 && isSpaceByte(s[0]) { + s = s[1:] + } + switch { + case strings.HasPrefix(s, "--"), strings.HasPrefix(s, "#"): + //行注释:跳到行尾(无行尾则整段只剩注释) + idx := strings.IndexByte(s, '\n') + if idx < 0 { + return "", true + } + s = s[idx+1:] + case strings.HasPrefix(s, "/*"): + //块注释:未闭合视为非法 + idx := strings.Index(s, "*/") + if idx < 0 { + return "", false + } + s = s[idx+2:] + case strings.HasPrefix(s, "("): + //前导括号剥离,支持 (select ...) 包装 + s = s[1:] + default: + return s, true + } + } +} + +// isSpaceByte 判断字节是否为空白字符 +func isSpaceByte(b byte) bool { + switch b { + case ' ', '\t', '\n', '\r', '\v', '\f': + return true + default: + return false + } +} diff --git a/ide/rpc/internal/logic/selectsql_test.go b/ide/rpc/internal/logic/selectsql_test.go new file mode 100644 index 0000000..6d2c29d --- /dev/null +++ b/ide/rpc/internal/logic/selectsql_test.go @@ -0,0 +1,65 @@ +package logic + +import ( + "strings" + "testing" +) + +// TestValidateSelectSql 执行脚本安全闸:仅放行单条 SELECT; +// 非 select 语句、多语句分号、注入片段必须拒绝;前导注释与括号包装放行 +func TestValidateSelectSql(t *testing.T) { + cases := []struct { + name string + sqlText string + wantErr bool + }{ + {"合法 select", "select id, name from ods.t_user where id = 1", false}, + {"大写 SELECT", "SELECT 1", false}, + {"换行多段", "select id\nfrom t_user\nwhere status = 1", false}, + {"括号包装", "(select 1)", false}, + {"前导行注释+括号", "-- 查询用户\n(select id from t_user limit 10)", false}, + {"前导块注释", "/* 只读查询 */ select 1", false}, + {"前导井号注释", "# 查询\nselect 1", false}, + {"注释与括号交错", "/* a */ ( /* b */ select 1 )", false}, + {"空语句", " ", true}, + {"纯注释", "-- 只有注释", true}, + {"非 select: update", "update t_user set name = 'x'", true}, + {"非 select: delete", "delete from t_user", true}, + {"非 select: insert", "insert into t(id) values (1)", true}, + {"非 select: drop", "drop table t_user", true}, + {"非 select: show", "show tables", true}, + {"cte 包装拒绝", "with c as (select 1) select * from c", true}, + {"标识符冒充 select", "selectx 1", true}, + {"多语句", "select 1; select 2", true}, + {"注入片段 ; drop", "select 1; drop table t", true}, + {"结尾分号也拒绝", "select 1;", true}, + {"块注释未闭合", "/* 未闭合 select 1", true}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := ValidateSelectSql(c.sqlText) + if (err != nil) != c.wantErr { + t.Fatalf("ValidateSelectSql(%q) err = %v, wantErr = %v", c.sqlText, err, c.wantErr) + } + }) + } +} + +// TestValidateSelectSqlErrorText 多语句注入片段拒绝时的错误信息应说明分号限制 +func TestValidateSelectSqlErrorText(t *testing.T) { + err := ValidateSelectSql("select 1; drop table t") + if err == nil { + t.Fatal("select 1; drop 必须被拒绝") + } + if !strings.Contains(err.Error(), "分号") { + t.Fatalf("错误信息应提示分号限制, got: %v", err) + } + //非 select 拒绝时的错误信息应说明仅允许 SELECT + err = ValidateSelectSql("delete from t_user") + if err == nil { + t.Fatal("delete 必须被拒绝") + } + if !strings.Contains(err.Error(), "SELECT") { + t.Fatalf("错误信息应提示仅允许 SELECT, got: %v", err) + } +} diff --git a/ide/rpc/internal/svc/serviceContext.go b/ide/rpc/internal/svc/serviceContext.go index 64e746d..3cfbda1 100644 --- a/ide/rpc/internal/svc/serviceContext.go +++ b/ide/rpc/internal/svc/serviceContext.go @@ -1,13 +1,58 @@ package svc -import "datacenter/ide/rpc/internal/config" +import ( + "sync" + + "datacenter/datasource/rpc/dataSourceClient" + "datacenter/ide/model" + "datacenter/ide/rpc/internal/config" + + "github.com/zeromicro/go-zero/core/discov" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/zrpc" +) + +// dataSourceEtcdKey datasource rpc 的 etcd 服务 key(Phase 1 契约固定) +const dataSourceEtcdKey = "datasource.rpc" type ServiceContext struct { - Config config.Config + Config config.Config + Conn sqlx.SqlConn //中台主库连接(dc_ide_script 表) + ScriptModel model.ScriptModel + DataSourceRpc dataSourceClient.DataSource //数据源 rpc(RunScript 换目标库 DSN) + + connMu sync.Mutex + targetCons map[string]sqlx.SqlConn //目标库连接池缓存:key=dsn,复用底层连接池 } func NewServiceContext(c config.Config) *ServiceContext { + conn := sqlx.NewMysql(c.Mysql.DataSource) + //datasource rpc 客户端:config.go 不可改,参考 tag 服务内联装配写法, + //etcd key 固定 datasource.rpc;NonBlock 保证 datasource 未就绪时不阻塞本服务启动 + dsRpc := dataSourceClient.NewDataSource(zrpc.MustNewClient(zrpc.RpcClientConf{ + Etcd: discov.EtcdConf{ + Hosts: c.Etcd.Hosts, + Key: dataSourceEtcdKey, + }, + NonBlock: true, + })) return &ServiceContext{ - Config: c, + Config: c, + Conn: conn, + ScriptModel: model.NewScriptModel(conn), + DataSourceRpc: dsRpc, + targetCons: make(map[string]sqlx.SqlConn), + } +} + +// TargetConn 获取目标库连接(按 DSN 缓存 sqlx.SqlConn,复用底层连接池) +func (sc *ServiceContext) TargetConn(dsn string) (sqlx.SqlConn, error) { + sc.connMu.Lock() + defer sc.connMu.Unlock() + if conn, ok := sc.targetCons[dsn]; ok { + return conn, nil } + conn := sqlx.NewMysql(dsn) + sc.targetCons[dsn] = conn + return conn, nil } diff --git a/internal/config/config.go b/internal/config/config.go index 0a62883..2665e80 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,5 +28,10 @@ type Config struct { MetricRpc zrpc.RpcClientConf TagRpc zrpc.RpcClientConf + //Phase 3/4 数据中台服务 + JobRpc zrpc.RpcClientConf + IdeRpc zrpc.RpcClientConf + AiRpc zrpc.RpcClientConf + CacheRedis cache.ClusterConf } diff --git a/internal/handler/ai/askhandler.go b/internal/handler/ai/askhandler.go new file mode 100644 index 0000000..708acea --- /dev/null +++ b/internal/handler/ai/askhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ai + +import ( + "net/http" + + "datacenter/internal/logic/ai" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// AI 问数 +func AskHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AskReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAskLogic(r.Context(), svcCtx) + resp, err := l.Ask(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/ide/deletescripthandler.go b/internal/handler/ide/deletescripthandler.go new file mode 100644 index 0000000..3c35d12 --- /dev/null +++ b/internal/handler/ide/deletescripthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "net/http" + + "datacenter/internal/logic/ide" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除脚本 +func DeleteScriptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ScriptIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ide.NewDeleteScriptLogic(r.Context(), svcCtx) + err := l.DeleteScript(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/ide/runscripthandler.go b/internal/handler/ide/runscripthandler.go new file mode 100644 index 0000000..7d636e0 --- /dev/null +++ b/internal/handler/ide/runscripthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "net/http" + + "datacenter/internal/logic/ide" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 执行脚本(仅 SELECT,行数保护) +func RunScriptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RunScriptReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ide.NewRunScriptLogic(r.Context(), svcCtx) + resp, err := l.RunScript(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/ide/savescripthandler.go b/internal/handler/ide/savescripthandler.go new file mode 100644 index 0000000..e1d946c --- /dev/null +++ b/internal/handler/ide/savescripthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "net/http" + + "datacenter/internal/logic/ide" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 保存脚本 +func SaveScriptHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SaveScriptReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ide.NewSaveScriptLogic(r.Context(), svcCtx) + resp, err := l.SaveScript(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/ide/scriptinfohandler.go b/internal/handler/ide/scriptinfohandler.go new file mode 100644 index 0000000..a5334e8 --- /dev/null +++ b/internal/handler/ide/scriptinfohandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "net/http" + + "datacenter/internal/logic/ide" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 脚本详情 +func ScriptInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ScriptIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ide.NewScriptInfoLogic(r.Context(), svcCtx) + resp, err := l.ScriptInfo(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/ide/scriptlisthandler.go b/internal/handler/ide/scriptlisthandler.go new file mode 100644 index 0000000..87f1ea9 --- /dev/null +++ b/internal/handler/ide/scriptlisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "net/http" + + "datacenter/internal/logic/ide" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 脚本列表 +func ScriptListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ScriptListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ide.NewScriptListLogic(r.Context(), svcCtx) + resp, err := l.ScriptList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/job/createsynctaskhandler.go b/internal/handler/job/createsynctaskhandler.go new file mode 100644 index 0000000..4d371b6 --- /dev/null +++ b/internal/handler/job/createsynctaskhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "net/http" + + "datacenter/internal/logic/job" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建同步任务 +func CreateSyncTaskHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateSyncTaskReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := job.NewCreateSyncTaskLogic(r.Context(), svcCtx) + resp, err := l.CreateSyncTask(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/job/deletesynctaskhandler.go b/internal/handler/job/deletesynctaskhandler.go new file mode 100644 index 0000000..a6fc487 --- /dev/null +++ b/internal/handler/job/deletesynctaskhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "net/http" + + "datacenter/internal/logic/job" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除同步任务 +func DeleteSyncTaskHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SyncTaskIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := job.NewDeleteSyncTaskLogic(r.Context(), svcCtx) + err := l.DeleteSyncTask(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/job/deploytaskhandler.go b/internal/handler/job/deploytaskhandler.go new file mode 100644 index 0000000..7d80c56 --- /dev/null +++ b/internal/handler/job/deploytaskhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "net/http" + + "datacenter/internal/logic/job" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 部署到调度器 +func DeployTaskHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SyncTaskIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := job.NewDeployTaskLogic(r.Context(), svcCtx) + resp, err := l.DeployTask(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/job/schedulerinfohandler.go b/internal/handler/job/schedulerinfohandler.go new file mode 100644 index 0000000..a10d831 --- /dev/null +++ b/internal/handler/job/schedulerinfohandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "net/http" + + "datacenter/internal/logic/job" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 调度器连接信息 +func SchedulerInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SyncTaskIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := job.NewSchedulerInfoLogic(r.Context(), svcCtx) + resp, err := l.SchedulerInfo(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/job/synctasklisthandler.go b/internal/handler/job/synctasklisthandler.go new file mode 100644 index 0000000..f410d49 --- /dev/null +++ b/internal/handler/job/synctasklisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "net/http" + + "datacenter/internal/logic/job" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 同步任务列表 +func SyncTaskListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SyncTaskListReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := job.NewSyncTaskListLogic(r.Context(), svcCtx) + resp, err := l.SyncTaskList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/job/taskstatushandler.go b/internal/handler/job/taskstatushandler.go new file mode 100644 index 0000000..8e770fb --- /dev/null +++ b/internal/handler/job/taskstatushandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "net/http" + + "datacenter/internal/logic/job" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 任务状态 +func TaskStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SyncTaskIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := job.NewTaskStatusLogic(r.Context(), svcCtx) + resp, err := l.TaskStatus(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/job/triggertaskhandler.go b/internal/handler/job/triggertaskhandler.go new file mode 100644 index 0000000..c1a5f72 --- /dev/null +++ b/internal/handler/job/triggertaskhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "net/http" + + "datacenter/internal/logic/job" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 手动触发任务 +func TriggerTaskHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SyncTaskIdReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := job.NewTriggerTaskLogic(r.Context(), svcCtx) + resp, err := l.TriggerTask(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 2068b32..23064a4 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -6,10 +6,13 @@ package handler import ( "net/http" + ai "datacenter/internal/handler/ai" catalog "datacenter/internal/handler/catalog" common "datacenter/internal/handler/common" datasource "datacenter/internal/handler/datasource" datasvc "datacenter/internal/handler/datasvc" + ide "datacenter/internal/handler/ide" + job "datacenter/internal/handler/job" metadata "datacenter/internal/handler/metadata" metric "datacenter/internal/handler/metric" quality "datacenter/internal/handler/quality" @@ -24,6 +27,17 @@ import ( ) func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + []rest.Route{ + { + // AI 问数 + Method: http.MethodPost, + Path: "/ai/ask", + Handler: ai.AskHandler(serverCtx), + }, + }, + ) + server.AddRoutes( []rest.Route{ { @@ -193,6 +207,88 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { }, ) + server.AddRoutes( + []rest.Route{ + { + // 删除脚本 + Method: http.MethodPost, + Path: "/ide/script/delete", + Handler: ide.DeleteScriptHandler(serverCtx), + }, + { + // 脚本详情 + Method: http.MethodGet, + Path: "/ide/script/info", + Handler: ide.ScriptInfoHandler(serverCtx), + }, + { + // 脚本列表 + Method: http.MethodGet, + Path: "/ide/script/list", + Handler: ide.ScriptListHandler(serverCtx), + }, + { + // 执行脚本(仅 SELECT,行数保护) + Method: http.MethodPost, + Path: "/ide/script/run", + Handler: ide.RunScriptHandler(serverCtx), + }, + { + // 保存脚本 + Method: http.MethodPost, + Path: "/ide/script/save", + Handler: ide.SaveScriptHandler(serverCtx), + }, + }, + ) + + server.AddRoutes( + []rest.Route{ + { + // 调度器连接信息 + Method: http.MethodGet, + Path: "/job/scheduler/info", + Handler: job.SchedulerInfoHandler(serverCtx), + }, + { + // 创建同步任务 + Method: http.MethodPost, + Path: "/job/task/create", + Handler: job.CreateSyncTaskHandler(serverCtx), + }, + { + // 删除同步任务 + Method: http.MethodPost, + Path: "/job/task/delete", + Handler: job.DeleteSyncTaskHandler(serverCtx), + }, + { + // 部署到调度器 + Method: http.MethodPost, + Path: "/job/task/deploy", + Handler: job.DeployTaskHandler(serverCtx), + }, + { + // 同步任务列表 + Method: http.MethodGet, + Path: "/job/task/list", + Handler: job.SyncTaskListHandler(serverCtx), + }, + { + // 任务状态 + Method: http.MethodGet, + Path: "/job/task/status", + Handler: job.TaskStatusHandler(serverCtx), + }, + { + // 手动触发任务 + Method: http.MethodPost, + Path: "/job/task/trigger", + Handler: job.TriggerTaskHandler(serverCtx), + }, + }, + ) + server.AddRoutes( []rest.Route{ { diff --git a/internal/logic/ai/asklogic.go b/internal/logic/ai/asklogic.go new file mode 100644 index 0000000..a415f6a --- /dev/null +++ b/internal/logic/ai/asklogic.go @@ -0,0 +1,63 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ai + +import ( + "context" + + "datacenter/ai/rpc/aiClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AskLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// AI 问数 +func NewAskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AskLogic { + return &AskLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// AI 问数: 候选上限默认值处理(maxCandidates<=0 取 5)后转发, 转发后把候选列表逐字段映射为网关类型返回 +func (l *AskLogic) Ask(req *types.AskReq) (resp *types.AskResp, err error) { + maxCandidates := req.MaxCandidates + if maxCandidates <= 0 { + maxCandidates = 5 + } + reply, err := l.svcCtx.AiRpc.Ask(l.ctx, &aiClient.AskReq{ + Question: req.Question, + Beid: req.Beid, + MaxCandidates: maxCandidates, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + candidates := make([]types.Candidate, 0, len(reply.GetCandidates())) + for _, item := range reply.GetCandidates() { + candidates = append(candidates, types.Candidate{ + Type: item.GetType(), + Id: item.GetId(), + Name: item.GetName(), + Detail: item.GetDetail(), + Comment: item.GetComment(), + }) + } + return &types.AskResp{ + Candidates: candidates, + LlmEnabled: reply.GetLlmEnabled(), + GeneratedSql: reply.GetGeneratedSql(), + Answer: reply.GetAnswer(), + CostMs: reply.GetCostMs(), + }, nil +} diff --git a/internal/logic/ide/convert.go b/internal/logic/ide/convert.go new file mode 100644 index 0000000..92b9908 --- /dev/null +++ b/internal/logic/ide/convert.go @@ -0,0 +1,23 @@ +package ide + +import ( + "datacenter/ide/rpc/ideClient" + "datacenter/internal/types" +) + +// scriptToTypes 将 rpc 的 Script 逐字段转换为网关类型。 +func scriptToTypes(script *ideClient.Script) *types.Script { + if script == nil { + return &types.Script{} + } + return &types.Script{ + Id: script.GetId(), + Name: script.GetName(), + SqlText: script.GetSqlText(), + Version: script.GetVersion(), + DatasourceId: script.GetDatasourceId(), + DbName: script.GetDbName(), + LastRunAt: script.GetLastRunAt(), + Beid: script.GetBeid(), + } +} diff --git a/internal/logic/ide/deletescriptlogic.go b/internal/logic/ide/deletescriptlogic.go new file mode 100644 index 0000000..6a04e32 --- /dev/null +++ b/internal/logic/ide/deletescriptlogic.go @@ -0,0 +1,38 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "context" + + "datacenter/ide/rpc/ideClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteScriptLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除脚本 +func NewDeleteScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteScriptLogic { + return &DeleteScriptLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 删除脚本: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传 +func (l *DeleteScriptLogic) DeleteScript(req *types.ScriptIdReq) error { + _, err := l.svcCtx.IdeRpc.DeleteScript(l.ctx, &ideClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + return err +} diff --git a/internal/logic/ide/runscriptlogic.go b/internal/logic/ide/runscriptlogic.go new file mode 100644 index 0000000..30064c3 --- /dev/null +++ b/internal/logic/ide/runscriptlogic.go @@ -0,0 +1,53 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "context" + + "datacenter/ide/rpc/ideClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RunScriptLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 执行脚本(仅 SELECT,行数保护) +func NewRunScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RunScriptLogic { + return &RunScriptLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 执行脚本: 行数上限默认值处理(maxRows<=0 取 100)后转发, 转发后把执行结果转为网关类型返回 +func (l *RunScriptLogic) RunScript(req *types.RunScriptReq) (resp *types.RunScriptResp, err error) { + maxRows := req.MaxRows + if maxRows <= 0 { + maxRows = 100 + } + reply, err := l.svcCtx.IdeRpc.RunScript(l.ctx, &ideClient.RunScriptReq{ + Id: req.Id, + Beid: req.Beid, + MaxRows: maxRows, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.RunScriptResp{ + ColumnsJson: reply.GetColumnsJson(), + RowsJson: reply.GetRowsJson(), + RowCount: reply.GetRowCount(), + CostMs: reply.GetCostMs(), + Truncated: reply.GetTruncated(), + }, nil +} diff --git a/internal/logic/ide/savescriptlogic.go b/internal/logic/ide/savescriptlogic.go new file mode 100644 index 0000000..e6f9ffa --- /dev/null +++ b/internal/logic/ide/savescriptlogic.go @@ -0,0 +1,46 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "context" + + "datacenter/ide/rpc/ideClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SaveScriptLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 保存脚本 +func NewSaveScriptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SaveScriptLogic { + return &SaveScriptLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 保存脚本: 网关请求逐字段映射为 rpc 请求(id=0 新建, >0 更新自动 version+1), 转发后把 Script 转为网关类型返回 +func (l *SaveScriptLogic) SaveScript(req *types.SaveScriptReq) (resp *types.Script, err error) { + reply, err := l.svcCtx.IdeRpc.SaveScript(l.ctx, &ideClient.SaveScriptReq{ + Id: req.Id, + Name: req.Name, + SqlText: req.SqlText, + DatasourceId: req.DatasourceId, + DbName: req.DbName, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return scriptToTypes(reply.Script), nil +} diff --git a/internal/logic/ide/scriptinfologic.go b/internal/logic/ide/scriptinfologic.go new file mode 100644 index 0000000..96982db --- /dev/null +++ b/internal/logic/ide/scriptinfologic.go @@ -0,0 +1,42 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "context" + + "datacenter/ide/rpc/ideClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ScriptInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 脚本详情 +func NewScriptInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ScriptInfoLogic { + return &ScriptInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 脚本详情: 网关请求逐字段映射为 rpc 请求, 转发后把 Script 转为网关类型返回 +func (l *ScriptInfoLogic) ScriptInfo(req *types.ScriptIdReq) (resp *types.Script, err error) { + reply, err := l.svcCtx.IdeRpc.GetScript(l.ctx, &ideClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return scriptToTypes(reply.Script), nil +} diff --git a/internal/logic/ide/scriptlistlogic.go b/internal/logic/ide/scriptlistlogic.go new file mode 100644 index 0000000..7b1f801 --- /dev/null +++ b/internal/logic/ide/scriptlistlogic.go @@ -0,0 +1,58 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ide + +import ( + "context" + + "datacenter/ide/rpc/ideClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ScriptListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 脚本列表 +func NewScriptListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ScriptListLogic { + return &ScriptListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 脚本列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *ScriptListLogic) ScriptList(req *types.ScriptListReq) (resp *types.ScriptListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.IdeRpc.ListScripts(l.ctx, &ideClient.ScriptListReq{ + Beid: req.Beid, + Keyword: req.Keyword, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.Script, 0, len(reply.List)) + for _, item := range reply.List { + list = append(list, *scriptToTypes(item)) + } + return &types.ScriptListResp{ + List: list, + Total: reply.Total, + }, nil +} diff --git a/internal/logic/job/convert.go b/internal/logic/job/convert.go new file mode 100644 index 0000000..ae56997 --- /dev/null +++ b/internal/logic/job/convert.go @@ -0,0 +1,26 @@ +package job + +import ( + "datacenter/internal/types" + "datacenter/job/rpc/jobClient" +) + +// syncTaskToTypes 将 rpc 的 SyncTask 逐字段转换为网关类型。 +func syncTaskToTypes(task *jobClient.SyncTask) *types.SyncTask { + if task == nil { + return &types.SyncTask{} + } + return &types.SyncTask{ + Id: task.GetId(), + Name: task.GetName(), + SourceDsId: task.GetSourceDsId(), + TargetDsId: task.GetTargetDsId(), + Mode: task.GetMode(), + MappingJson: task.GetMappingJson(), + Cron: task.GetCron(), + DsWorkflowCode: task.GetDsWorkflowCode(), + Status: task.GetStatus(), + LastRunAt: task.GetLastRunAt(), + Beid: task.GetBeid(), + } +} diff --git a/internal/logic/job/createsynctasklogic.go b/internal/logic/job/createsynctasklogic.go new file mode 100644 index 0000000..c5e9f8c --- /dev/null +++ b/internal/logic/job/createsynctasklogic.go @@ -0,0 +1,51 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/job/rpc/jobClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateSyncTaskLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建同步任务 +func NewCreateSyncTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateSyncTaskLogic { + return &CreateSyncTaskLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 创建同步任务: 同步模式默认 offline, 网关请求逐字段映射为 rpc 请求, 转发后把 SyncTask 转为网关类型返回 +func (l *CreateSyncTaskLogic) CreateSyncTask(req *types.CreateSyncTaskReq) (resp *types.SyncTask, err error) { + mode := req.Mode + if mode == "" { + mode = "offline" + } + reply, err := l.svcCtx.JobRpc.CreateSyncTask(l.ctx, &jobClient.CreateSyncTaskReq{ + Name: req.Name, + SourceDsId: req.SourceDsId, + TargetDsId: req.TargetDsId, + Mode: mode, + MappingJson: req.MappingJson, + Cron: req.Cron, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return syncTaskToTypes(reply.Task), nil +} diff --git a/internal/logic/job/deletesynctasklogic.go b/internal/logic/job/deletesynctasklogic.go new file mode 100644 index 0000000..3149cdb --- /dev/null +++ b/internal/logic/job/deletesynctasklogic.go @@ -0,0 +1,38 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/job/rpc/jobClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteSyncTaskLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除同步任务 +func NewDeleteSyncTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteSyncTaskLogic { + return &DeleteSyncTaskLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 删除同步任务: 网关请求逐字段映射为 rpc 请求后转发, 业务错误直接透传 +func (l *DeleteSyncTaskLogic) DeleteSyncTask(req *types.SyncTaskIdReq) error { + _, err := l.svcCtx.JobRpc.DeleteSyncTask(l.ctx, &jobClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + return err +} diff --git a/internal/logic/job/deploytasklogic.go b/internal/logic/job/deploytasklogic.go new file mode 100644 index 0000000..81186f5 --- /dev/null +++ b/internal/logic/job/deploytasklogic.go @@ -0,0 +1,46 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/job/rpc/jobClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeployTaskLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 部署到调度器 +func NewDeployTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeployTaskLogic { + return &DeployTaskLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 部署到调度器: 网关请求逐字段映射为 rpc 请求, 转发后把部署结果转为网关类型返回 +func (l *DeployTaskLogic) DeployTask(req *types.SyncTaskIdReq) (resp *types.DeployResp, err error) { + reply, err := l.svcCtx.JobRpc.DeployToScheduler(l.ctx, &jobClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.DeployResp{ + Ok: reply.GetOk(), + DsWorkflowCode: reply.GetDsWorkflowCode(), + Msg: reply.GetMsg(), + }, nil +} diff --git a/internal/logic/job/schedulerinfologic.go b/internal/logic/job/schedulerinfologic.go new file mode 100644 index 0000000..df38b66 --- /dev/null +++ b/internal/logic/job/schedulerinfologic.go @@ -0,0 +1,47 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/job/rpc/jobClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SchedulerInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 调度器连接信息 +func NewSchedulerInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SchedulerInfoLogic { + return &SchedulerInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 调度器连接信息: 该接口按租户查询(不针对单个任务, id 置 0), 转发后把连接信息转为网关类型返回 +func (l *SchedulerInfoLogic) SchedulerInfo(req *types.SyncTaskIdReq) (resp *types.SchedulerInfoResp, err error) { + reply, err := l.svcCtx.JobRpc.GetSchedulerInfo(l.ctx, &jobClient.IdReq{ + Id: 0, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.SchedulerInfoResp{ + Configured: reply.GetConfigured(), + Connected: reply.GetConnected(), + BaseUrl: reply.GetBaseUrl(), + Msg: reply.GetMsg(), + }, nil +} diff --git a/internal/logic/job/synctasklistlogic.go b/internal/logic/job/synctasklistlogic.go new file mode 100644 index 0000000..38a676e --- /dev/null +++ b/internal/logic/job/synctasklistlogic.go @@ -0,0 +1,58 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/job/rpc/jobClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SyncTaskListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 同步任务列表 +func NewSyncTaskListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SyncTaskListLogic { + return &SyncTaskListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 同步任务列表: 页码默认值处理(page<=0 取 1, size<=0 取 10)后转发, 业务错误直接透传 +func (l *SyncTaskListLogic) SyncTaskList(req *types.SyncTaskListReq) (resp *types.SyncTaskListResp, err error) { + page, size := req.Page, req.Size + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + reply, err := l.svcCtx.JobRpc.ListSyncTasks(l.ctx, &jobClient.TaskListReq{ + Beid: req.Beid, + Status: req.Status, + Page: page, + Size: size, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + list := make([]types.SyncTask, 0, len(reply.List)) + for _, item := range reply.List { + list = append(list, *syncTaskToTypes(item)) + } + return &types.SyncTaskListResp{ + List: list, + Total: reply.Total, + }, nil +} diff --git a/internal/logic/job/taskstatuslogic.go b/internal/logic/job/taskstatuslogic.go new file mode 100644 index 0000000..585b795 --- /dev/null +++ b/internal/logic/job/taskstatuslogic.go @@ -0,0 +1,46 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/job/rpc/jobClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TaskStatusLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 任务状态 +func NewTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TaskStatusLogic { + return &TaskStatusLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 任务状态: 网关请求逐字段映射为 rpc 请求, 转发后把状态聚合结果转为网关类型返回 +func (l *TaskStatusLogic) TaskStatus(req *types.SyncTaskIdReq) (resp *types.TaskStatusResp, err error) { + reply, err := l.svcCtx.JobRpc.GetTaskStatus(l.ctx, &jobClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.TaskStatusResp{ + Status: reply.GetStatus(), + DsInstanceId: reply.GetDsInstanceId(), + Msg: reply.GetMsg(), + }, nil +} diff --git a/internal/logic/job/triggertasklogic.go b/internal/logic/job/triggertasklogic.go new file mode 100644 index 0000000..7fec837 --- /dev/null +++ b/internal/logic/job/triggertasklogic.go @@ -0,0 +1,46 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package job + +import ( + "context" + + "datacenter/internal/svc" + "datacenter/internal/types" + "datacenter/job/rpc/jobClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +type TriggerTaskLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 手动触发任务 +func NewTriggerTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TriggerTaskLogic { + return &TriggerTaskLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 手动触发任务: 网关请求逐字段映射为 rpc 请求, 转发后把触发结果转为网关类型返回 +func (l *TriggerTaskLogic) TriggerTask(req *types.SyncTaskIdReq) (resp *types.TriggerResp, err error) { + reply, err := l.svcCtx.JobRpc.TriggerTask(l.ctx, &jobClient.IdReq{ + Id: req.Id, + Beid: req.Beid, + }) + if err != nil { + // 业务错误直接透传 rpc error + return nil, err + } + return &types.TriggerResp{ + Ok: reply.GetOk(), + DsProcessInstanceId: reply.GetDsProcessInstanceId(), + Msg: reply.GetMsg(), + }, nil +} diff --git a/internal/svc/servicecontext.go b/internal/svc/servicecontext.go index a1f962e..2c114a2 100644 --- a/internal/svc/servicecontext.go +++ b/internal/svc/servicecontext.go @@ -2,12 +2,15 @@ package svc import ( "context" + "datacenter/ai/rpc/aiClient" "datacenter/catalog/rpc/catalogClient" "datacenter/common/rpc/commonclient" "datacenter/datasource/rpc/dataSourceClient" "datacenter/datasvc/rpc/dataSvcClient" + "datacenter/ide/rpc/ideClient" "datacenter/internal/config" "datacenter/internal/middleware" + "datacenter/job/rpc/jobClient" "datacenter/metadata/rpc/metadataClient" "datacenter/metric/rpc/metricsvc" "datacenter/quality/rpc/qualityClient" @@ -48,6 +51,9 @@ type ServiceContext struct { QualityRpc qualityClient.Quality //数据质量(Phase 2) MetricRpc metricsvc.MetricSvc //数据指标(Phase 2) TagRpc tagClient.Tag //数据标签(Phase 2) + JobRpc jobClient.Job //调度集成(Phase 3) + IdeRpc ideClient.Ide //SQL IDE(Phase 3) + AiRpc aiClient.Ai //AI 问数(Phase 4) Cache cache.Cache RedisConn *redis.Redis } @@ -76,6 +82,9 @@ func NewServiceContext(c config.Config) *ServiceContext { qr2 := qualityClient.NewQuality(zrpc.MustNewClient(c.QualityRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) mr2 := metricsvc.NewMetricSvc(zrpc.MustNewClient(c.MetricRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) tr2 := tagClient.NewTag(zrpc.MustNewClient(c.TagRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + jr3 := jobClient.NewJob(zrpc.MustNewClient(c.JobRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + ir3 := ideClient.NewIde(zrpc.MustNewClient(c.IdeRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) + ar3 := aiClient.NewAi(zrpc.MustNewClient(c.AiRpc, zrpc.WithUnaryClientInterceptor(timeInterceptor))) //缓存 ca := cache.New(c.CacheRedis, syncx.NewSingleFlight(), cache.NewStat("dc"), shared.ErrNotFound) rcon := redis.MustNewRedis(c.CacheRedis[0].RedisConf) @@ -97,6 +106,9 @@ func NewServiceContext(c config.Config) *ServiceContext { QualityRpc: qr2, MetricRpc: mr2, TagRpc: tr2, + JobRpc: jr3, + IdeRpc: ir3, + AiRpc: ar3, Cache: ca, RedisConn: rcon, } diff --git a/internal/types/types.go b/internal/types/types.go index da3be47..94d6ed3 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -97,6 +97,20 @@ type ArticleReq struct { ImageUrl string `json:"ImageUrl"` } +type AskReq struct { + Question string `json:"question"` + Beid int64 `json:"beid,optional"` + MaxCandidates int32 `json:"max_candidates,optional"` +} + +type AskResp struct { + Candidates []Candidate `json:"candidates"` + LlmEnabled bool `json:"llm_enabled"` + GeneratedSql string `json:"generated_sql,optional"` + Answer string `json:"answer"` + CostMs int64 `json:"cost_ms"` +} + type Asset struct { Id int64 `json:"id,optional"` EntityId int64 `json:"entity_id,optional"` @@ -145,6 +159,14 @@ type BindAtomsReq struct { Beid int64 `json:"beid,optional"` } +type Candidate struct { + Type string `json:"type,optional"` //entity/metric + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + Detail string `json:"detail,optional"` + Comment string `json:"comment,optional"` +} + type CategoryListReq struct { Beid int64 `json:"beid,optional" form:"beid,optional"` } @@ -235,6 +257,16 @@ type CreateQualityRuleReq struct { Beid int64 `json:"beid,optional"` } +type CreateSyncTaskReq struct { + Name string `json:"name"` + SourceDsId int64 `json:"source_ds_id"` + TargetDsId int64 `json:"target_ds_id,optional"` + Mode string `json:"mode,optional"` + MappingJson string `json:"mapping_json,optional"` + Cron string `json:"cron,optional"` + Beid int64 `json:"beid,optional"` +} + type CreateTagDefReq struct { ObjectId int64 `json:"object_id"` TagKey string `json:"tag_key"` @@ -286,6 +318,12 @@ type DataSourceListResp struct { Total int64 `json:"total"` } +type DeployResp struct { + Ok bool `json:"ok"` + DsWorkflowCode int32 `json:"ds_workflow_code,optional"` + Msg string `json:"msg,optional"` +} + type DiffResp struct { DiffJson string `json:"diff_json"` } @@ -577,6 +615,64 @@ type RunJobResp struct { LastStatus string `json:"last_status"` } +type RunScriptReq struct { + Id int64 `json:"id"` + Beid int64 `json:"beid,optional"` + MaxRows int32 `json:"max_rows,optional"` +} + +type RunScriptResp struct { + ColumnsJson string `json:"columns_json"` + RowsJson string `json:"rows_json"` + RowCount int64 `json:"row_count"` + CostMs int64 `json:"cost_ms"` + Truncated bool `json:"truncated"` +} + +type SaveScriptReq struct { + Id int64 `json:"id,optional"` + Name string `json:"name"` + SqlText string `json:"sql_text"` + DatasourceId int64 `json:"datasource_id"` + DbName string `json:"db_name,optional"` + Beid int64 `json:"beid,optional"` +} + +type SchedulerInfoResp struct { + Configured bool `json:"configured"` + Connected bool `json:"connected"` + BaseUrl string `json:"base_url,optional"` + Msg string `json:"msg,optional"` +} + +type Script struct { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + SqlText string `json:"sql_text,optional"` + Version int32 `json:"version,optional"` + DatasourceId int64 `json:"datasource_id,optional"` + DbName string `json:"db_name,optional"` + LastRunAt int64 `json:"last_run_at,optional"` + Beid int64 `json:"beid,optional"` +} + +type ScriptIdReq struct { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type ScriptListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Keyword string `json:"keyword,optional" form:"keyword,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type ScriptListResp struct { + List []Script `json:"list"` + Total int64 `json:"total"` +} + type SearchReq struct { Keyword string `json:"keyword,optional" form:"keyword,optional"` Page string `json:"page,optional" form:"page,optional"` @@ -601,6 +697,37 @@ type SnsResp struct { LoginUrl string `json:"login_url"` //微信登陆的地址 } +type SyncTask struct { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + SourceDsId int64 `json:"source_ds_id,optional"` + TargetDsId int64 `json:"target_ds_id,optional"` + Mode string `json:"mode,optional"` + MappingJson string `json:"mapping_json,optional"` + Cron string `json:"cron,optional"` + DsWorkflowCode int32 `json:"ds_workflow_code,optional"` + Status string `json:"status,optional"` + LastRunAt int64 `json:"last_run_at,optional"` + Beid int64 `json:"beid,optional"` +} + +type SyncTaskIdReq struct { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type SyncTaskListReq struct { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Status string `json:"status,optional" form:"status,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} + +type SyncTaskListResp struct { + List []SyncTask `json:"list"` + Total int64 `json:"total"` +} + type TagAssetReq struct { EntityId int64 `json:"entity_id"` CategoryId int64 `json:"category_id,optional"` @@ -683,6 +810,12 @@ type TaggingResp struct { TagId int64 `json:"tag_id"` } +type TaskStatusResp struct { + Status string `json:"status,optional"` + DsInstanceId int32 `json:"ds_instance_id,optional"` + Msg string `json:"msg,optional"` +} + type TestConnResp struct { Ok bool `json:"ok"` LatencyMs int64 `json:"latency_ms"` @@ -693,6 +826,12 @@ type Token struct { Token string `json:"token"` } +type TriggerResp struct { + Ok bool `json:"ok"` + DsProcessInstanceId int32 `json:"ds_process_instance_id,optional"` + Msg string `json:"msg,optional"` +} + type UpdateApiReq struct { Id int64 `json:"id"` Beid int64 `json:"beid,optional"` diff --git a/job.api b/job.api new file mode 100644 index 0000000..e5d3948 --- /dev/null +++ b/job.api @@ -0,0 +1,105 @@ +syntax = "v1" + +//调度集成(Phase 3:管控面,执行面推 DolphinScheduler) +type SyncTask { + Id int64 `json:"id,optional"` + Name string `json:"name,optional"` + SourceDsId int64 `json:"source_ds_id,optional"` + TargetDsId int64 `json:"target_ds_id,optional"` + Mode string `json:"mode,optional"` + MappingJson string `json:"mapping_json,optional"` + Cron string `json:"cron,optional"` + DsWorkflowCode int32 `json:"ds_workflow_code,optional"` + Status string `json:"status,optional"` + LastRunAt int64 `json:"last_run_at,optional"` + Beid int64 `json:"beid,optional"` +} + +type CreateSyncTaskReq { + Name string `json:"name"` + SourceDsId int64 `json:"source_ds_id"` + TargetDsId int64 `json:"target_ds_id,optional"` + Mode string `json:"mode,optional"` + MappingJson string `json:"mapping_json,optional"` + Cron string `json:"cron,optional"` + Beid int64 `json:"beid,optional"` +} + +type SyncTaskIdReq { + Id int64 `json:"id,optional" form:"id,optional"` + Beid int64 `json:"beid,optional" form:"beid,optional"` +} + +type SyncTaskListReq { + Beid int64 `json:"beid,optional" form:"beid,optional"` + Status string `json:"status,optional" form:"status,optional"` + Page int64 `json:"page,optional" form:"page,optional"` + Size int64 `json:"size,optional" form:"size,optional"` +} +type SyncTaskListResp { + List []SyncTask `json:"list"` + Total int64 `json:"total"` +} + +type DeployResp { + Ok bool `json:"ok"` + DsWorkflowCode int32 `json:"ds_workflow_code,optional"` + Msg string `json:"msg,optional"` +} +type TriggerResp { + Ok bool `json:"ok"` + DsProcessInstanceId int32 `json:"ds_process_instance_id,optional"` + Msg string `json:"msg,optional"` +} +type TaskStatusResp { + Status string `json:"status,optional"` + DsInstanceId int32 `json:"ds_instance_id,optional"` + Msg string `json:"msg,optional"` +} +type SchedulerInfoResp { + Configured bool `json:"configured"` + Connected bool `json:"connected"` + BaseUrl string `json:"base_url,optional"` + Msg string `json:"msg,optional"` +} + +@server( + group: job +) +service datacenter-api { + @doc( + summary: "创建同步任务" + ) + @handler createSyncTask + post /job/task/create (CreateSyncTaskReq) returns (SyncTask) + @doc( + summary: "同步任务列表" + ) + @handler syncTaskList + get /job/task/list (SyncTaskListReq) returns (SyncTaskListResp) + @doc( + summary: "删除同步任务" + ) + @handler deleteSyncTask + post /job/task/delete (SyncTaskIdReq) + @doc( + summary: "部署到调度器" + ) + @handler deployTask + post /job/task/deploy (SyncTaskIdReq) returns (DeployResp) + @doc( + summary: "手动触发任务" + ) + @handler triggerTask + post /job/task/trigger (SyncTaskIdReq) returns (TriggerResp) + @doc( + summary: "任务状态" + ) + @handler taskStatus + get /job/task/status (SyncTaskIdReq) returns (TaskStatusResp) + @doc( + summary: "调度器连接信息" + ) + @handler schedulerInfo + get /job/scheduler/info (SyncTaskIdReq) returns (SchedulerInfoResp) +} diff --git a/job/model/synctaskmodel.go b/job/model/synctaskmodel.go new file mode 100644 index 0000000..e948d9a --- /dev/null +++ b/job/model/synctaskmodel.go @@ -0,0 +1,170 @@ +package model + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +const ( + taskTable = "`dc_sync_task`" + taskInsertFieldSize = 7 // 插入字段数(与 taskInsertColumns 对应) + // taskInsertColumns 插入字段(不含自增 id、时间列与部署/运行状态列, + // ds_workflow_code/status 由部署回填,last_run_at 由触发运行回填) + taskInsertColumns = "name,source_ds_id,target_ds_id,mode,mapping_json,cron,beid" + // taskRows 查询列(mapping_json/last_run_at 允许 NULL,分别用 ifnull 兜底为空串/0 毫秒时间戳, + // 避免扫描报错;查询列数与 SyncTask 字段数一一对应, + // 规避 go-zero 少列扫宽 struct 的 not matching destination to scan 报错) + taskRows = "id,name,source_ds_id,target_ds_id,mode," + + "ifnull(mapping_json,'') as mapping_json,cron,ds_workflow_code,status," + + "ifnull(cast(unix_timestamp(last_run_at) * 1000 as signed), 0) as last_run_at," + + "beid,created_at,updated_at" +) + +// 同步任务(管控面:本平台管理定义,执行面推送 DolphinScheduler/SeaTunnel) +type ( + // SyncTask dc_sync_task 行 + SyncTask struct { + Id int64 `db:"id"` + Name string `db:"name"` + SourceDsId int64 `db:"source_ds_id"` + TargetDsId int64 `db:"target_ds_id"` + Mode string `db:"mode"` // offline/cdc + MappingJson string `db:"mapping_json"` + Cron string `db:"cron"` // 调度周期(推送 DS 时使用,本期仅存档) + DsWorkflowCode int64 `db:"ds_workflow_code"` + Status string `db:"status"` + LastRunAt int64 `db:"last_run_at"` // 最近运行时间(毫秒时间戳,未运行为 0) + Beid int64 `db:"beid"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + // SyncTaskModel 同步任务表操作接口 + SyncTaskModel interface { + // Insert 新增同步任务(草稿态),返回自增 id + Insert(ctx context.Context, data *SyncTask) (int64, error) + // FindOne 按主键查询(beid>0 时追加多租户过滤) + FindOne(ctx context.Context, id, beid int64) (*SyncTask, error) + // FindList 条件分页查询:beid 必带,status 可选过滤;返回列表与总数 + FindList(ctx context.Context, beid int64, status string, page, size int64) ([]*SyncTask, int64, error) + // UpdateDeploy 部署回填:写入 DS 工作流编码并把状态流转到 deployed(按 id+beid 定位) + UpdateDeploy(ctx context.Context, id, beid, workflowCode int64, status string) error + // UpdateStatus 状态流转(deployed/running/failed,按 id+beid 定位) + UpdateStatus(ctx context.Context, id, beid int64, status string) error + // UpdateRunStatus 触发运行后回填状态与最近运行时间(按 id+beid 定位) + UpdateRunStatus(ctx context.Context, id, beid int64, status string, runAt time.Time) error + // Delete 删除任务(按 id+beid 定位) + Delete(ctx context.Context, id, beid int64) error + } + + defaultSyncTaskModel struct { + conn sqlx.SqlConn + table string + } +) + +// NewSyncTaskModel 创建同步任务 model +func NewSyncTaskModel(conn sqlx.SqlConn) SyncTaskModel { + return &defaultSyncTaskModel{ + conn: conn, + table: taskTable, + } +} + +// Insert 新增同步任务(草稿态) +func (m *defaultSyncTaskModel) Insert(ctx context.Context, data *SyncTask) (int64, error) { + query := fmt.Sprintf("insert into %s (%s) values (%s)", + m.table, taskInsertColumns, placeholders(taskInsertFieldSize)) + ret, err := m.conn.ExecCtx(ctx, query, + data.Name, data.SourceDsId, data.TargetDsId, data.Mode, + data.MappingJson, data.Cron, data.Beid) + if err != nil { + return 0, err + } + return ret.LastInsertId() +} + +// FindOne 按主键查询同步任务 +func (m *defaultSyncTaskModel) FindOne(ctx context.Context, id, beid int64) (*SyncTask, error) { + query := fmt.Sprintf("select %s from %s where `id` = ?", taskRows, m.table) + args := []any{id} + if beid > 0 { + query += " and `beid` = ?" + args = append(args, beid) + } + var resp SyncTask + err := m.conn.QueryRowCtx(ctx, &resp, query+" limit 1", args...) + switch { + case err == nil: + return &resp, nil + case errors.Is(err, sqlx.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err + } +} + +// FindList 任务列表(beid 多租户必带 + status 可选过滤),返回列表与总数 +func (m *defaultSyncTaskModel) FindList(ctx context.Context, beid int64, status string, + page, size int64) ([]*SyncTask, int64, error) { + if beid <= 0 { + return nil, 0, ErrBeidRequired + } + page, size = normalizePage(page, size) + + where := "`beid` = ?" + args := []any{beid} + if status != "" { + where += " and `status` = ?" + args = append(args, status) + } + + // 总数 + var total int64 + countQuery := fmt.Sprintf("select count(*) from %s where %s", m.table, where) + if err := m.conn.QueryRowCtx(ctx, &total, countQuery, args...); err != nil { + return nil, 0, err + } + + // 分页数据 + listQuery := fmt.Sprintf("select %s from %s where %s order by `id` desc limit ?, ?", + taskRows, m.table, where) + args = append(args, (page-1)*size, size) + var list []*SyncTask + if err := m.conn.QueryRowsCtx(ctx, &list, listQuery, args...); err != nil { + return nil, 0, err + } + return list, total, nil +} + +// UpdateDeploy 部署回填:写入 DS 工作流编码并把状态流转到 deployed +func (m *defaultSyncTaskModel) UpdateDeploy(ctx context.Context, id, beid, workflowCode int64, status string) error { + query := fmt.Sprintf("update %s set `ds_workflow_code` = ?, `status` = ? where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, workflowCode, status, id, beid) + return err +} + +// UpdateStatus 状态流转 +func (m *defaultSyncTaskModel) UpdateStatus(ctx context.Context, id, beid int64, status string) error { + query := fmt.Sprintf("update %s set `status` = ? where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, status, id, beid) + return err +} + +// UpdateRunStatus 触发运行后回填状态与最近运行时间 +func (m *defaultSyncTaskModel) UpdateRunStatus(ctx context.Context, id, beid int64, status string, runAt time.Time) error { + query := fmt.Sprintf("update %s set `status` = ?, `last_run_at` = ? where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, status, runAt, id, beid) + return err +} + +// Delete 删除任务 +func (m *defaultSyncTaskModel) Delete(ctx context.Context, id, beid int64) error { + query := fmt.Sprintf("delete from %s where `id` = ? and `beid` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, id, beid) + return err +} diff --git a/job/model/vars.go b/job/model/vars.go new file mode 100644 index 0000000..06a987a --- /dev/null +++ b/job/model/vars.go @@ -0,0 +1,69 @@ +package model + +import ( + "errors" + "strings" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// 通用错误 +var ( + // ErrNotFound 记录不存在 + ErrNotFound = sqlx.ErrNotFound + // ErrBeidRequired 多租户 beid 缺失 + ErrBeidRequired = errors.New("beid 不能为空") +) + +// 同步任务状态(dc_sync_task.status 状态机:draft --部署--> deployed --触发--> running --终态--> deployed/failed) +const ( + TaskStatusDraft = "draft" // 草稿(未部署) + TaskStatusDeployed = "deployed" // 已部署到调度器并上线 + TaskStatusRunning = "running" // 运行中 + TaskStatusFailed = "failed" // 最近一次运行失败 +) + +// 同步模式(dc_sync_task.mode 白名单) +const ( + TaskModeOffline = "offline" // 离线批量同步 + TaskModeCdc = "cdc" // 实时增量同步 +) + +// taskStatusSet 任务状态白名单(列表过滤入参校验用) +var taskStatusSet = map[string]struct{}{ + TaskStatusDraft: {}, + TaskStatusDeployed: {}, + TaskStatusRunning: {}, + TaskStatusFailed: {}, +} + +// IsValidTaskStatus 任务状态是否在白名单内 +func IsValidTaskStatus(status string) bool { + _, ok := taskStatusSet[status] + return ok +} + +// 分页缺省值与上限 +const ( + defaultPageSize = 10 + maxPageSize = 200 +) + +// placeholders 生成 n 个 ? 占位符(逗号分隔) +func placeholders(n int) string { + return strings.TrimSuffix(strings.Repeat("?,", n), ",") +} + +// normalizePage 分页参数防御:page 缺省 1,size 缺省 10、封顶 200 +func normalizePage(page, size int64) (int64, int64) { + if page < 1 { + page = 1 + } + if size <= 0 { + size = defaultPageSize + } + if size > maxPageSize { + size = maxPageSize + } + return page, size +} diff --git a/job/rpc/internal/logic/convert.go b/job/rpc/internal/logic/convert.go new file mode 100644 index 0000000..2ad20fa --- /dev/null +++ b/job/rpc/internal/logic/convert.go @@ -0,0 +1,23 @@ +package logic + +import ( + "datacenter/job/model" + "datacenter/job/rpc/job" +) + +// taskToPb 同步任务 model -> pb(ds_workflow_code/last_run_at 收窄转换,未运行为 0) +func taskToPb(t *model.SyncTask) *job.SyncTask { + return &job.SyncTask{ + Id: t.Id, + Name: t.Name, + SourceDsId: t.SourceDsId, + TargetDsId: t.TargetDsId, + Mode: t.Mode, + MappingJson: t.MappingJson, + Cron: t.Cron, + DsWorkflowCode: int32(t.DsWorkflowCode), + Status: t.Status, + LastRunAt: t.LastRunAt, + Beid: t.Beid, + } +} diff --git a/job/rpc/internal/logic/createSyncTaskLogic.go b/job/rpc/internal/logic/createSyncTaskLogic.go index b2cdccc..1685d0d 100644 --- a/job/rpc/internal/logic/createSyncTaskLogic.go +++ b/job/rpc/internal/logic/createSyncTaskLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "strings" + "datacenter/job/model" "datacenter/job/rpc/internal/svc" "datacenter/job/rpc/job" @@ -23,9 +25,31 @@ func NewCreateSyncTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Cr } } -// 创建同步任务 +// 创建同步任务(落库为草稿态,部署到调度器走 DeployToScheduler) func (l *CreateSyncTaskLogic) CreateSyncTask(in *job.CreateSyncTaskReq) (*job.TaskReply, error) { - // todo: add your logic here and delete this line - - return &job.TaskReply{}, nil + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + // 名称/mode 白名单(offline/cdc)/source_ds_id>0/mapping_json 合法性静态校验 + name := strings.TrimSpace(in.Name) + if err := ValidateSyncTask(name, in.Mode, in.SourceDsId, in.MappingJson); err != nil { + return nil, err + } + id, err := l.svcCtx.SyncTaskModel.Insert(l.ctx, &model.SyncTask{ + Name: name, + SourceDsId: in.SourceDsId, + TargetDsId: in.TargetDsId, + Mode: in.Mode, + MappingJson: strings.TrimSpace(in.MappingJson), + Cron: strings.TrimSpace(in.Cron), + Beid: in.Beid, + }) + if err != nil { + return nil, err + } + task, err := loadTask(l.ctx, l.svcCtx.SyncTaskModel, id, in.Beid) + if err != nil { + return nil, err + } + return &job.TaskReply{Task: taskToPb(task)}, nil } diff --git a/job/rpc/internal/logic/deleteSyncTaskLogic.go b/job/rpc/internal/logic/deleteSyncTaskLogic.go index ecd7f82..a3382f6 100644 --- a/job/rpc/internal/logic/deleteSyncTaskLogic.go +++ b/job/rpc/internal/logic/deleteSyncTaskLogic.go @@ -23,9 +23,18 @@ func NewDeleteSyncTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *De } } -// 删除任务(已部署的先提示需先下线) +// 删除任务(已部署的先提示需先下线:非草稿状态一律拒绝,避免调度器侧残留工作流) func (l *DeleteSyncTaskLogic) DeleteSyncTask(in *job.IdReq) (*job.IdReq, error) { - // todo: add your logic here and delete this line - - return &job.IdReq{}, nil + task, err := loadTask(l.ctx, l.svcCtx.SyncTaskModel, in.Id, in.Beid) + if err != nil { + return nil, err + } + // 已部署/运行中/失败的任务在调度器侧仍存在工作流,直接拒绝并提示先下线 + if ok, _ := CanDelete(task.Status); !ok { + return nil, ErrTaskDeployed + } + if err := l.svcCtx.SyncTaskModel.Delete(l.ctx, task.Id, in.Beid); err != nil { + return nil, err + } + return &job.IdReq{Id: in.Id, Beid: in.Beid}, nil } diff --git a/job/rpc/internal/logic/deployToSchedulerLogic.go b/job/rpc/internal/logic/deployToSchedulerLogic.go index 20f5f31..03c9261 100644 --- a/job/rpc/internal/logic/deployToSchedulerLogic.go +++ b/job/rpc/internal/logic/deployToSchedulerLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/job/model" "datacenter/job/rpc/internal/svc" "datacenter/job/rpc/job" @@ -23,9 +24,34 @@ func NewDeployToSchedulerLogic(ctx context.Context, svcCtx *svc.ServiceContext) } } -// 部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因) +// 部署到调度器(DolphinScheduler;未配置/不可达时 ok=false 带原因——部署失败是业务结果而非 error): +// 幂等确保工作流定义存在(同名复用)→ 上线(releaseState=ONLINE)→ 回填工作流编码与 deployed 状态。 +// TODO(联调): 需真实 DolphinScheduler 3.x 实例验证;任务的 cron 定时调度未实现 +// (cron 仅存档,上线后需在 DS 侧对该工作流提交 schedule 定时,联调后可在此补充) func (l *DeployToSchedulerLogic) DeployToScheduler(in *job.IdReq) (*job.DeployReply, error) { - // todo: add your logic here and delete this line - - return &job.DeployReply{}, nil + task, err := loadTask(l.ctx, l.svcCtx.SyncTaskModel, in.Id, in.Beid) + if err != nil { + return nil, err + } + client := ds() + if !client.enabled() { + return &job.DeployReply{Ok: false, Msg: "调度器未配置:需设置环境变量 DS_BASE_URL/DS_TOKEN/DS_PROJECT"}, nil + } + // 幂等创建/复用工作流定义 + code, err := client.ensureWorkflow(task.Name) + if err != nil { + l.Errorf("部署任务 %d 创建工作流失败: %v", task.Id, err) + return &job.DeployReply{Ok: false, Msg: dsErrMsg("创建工作流失败", err)}, nil + } + // 上线工作流(未上线不允许被调度/手动运行) + if err := client.release(code); err != nil { + l.Errorf("部署任务 %d 上线工作流 %d 失败: %v", task.Id, code, err) + return &job.DeployReply{Ok: false, DsWorkflowCode: int32(code), Msg: dsErrMsg("上线工作流失败", err)}, nil + } + // 回填工作流编码并流转到 deployed + if err := l.svcCtx.SyncTaskModel.UpdateDeploy(l.ctx, task.Id, in.Beid, int64(code), model.TaskStatusDeployed); err != nil { + return nil, err + } + l.Infof("任务 %d 已部署到调度器,工作流编码 %d", task.Id, code) + return &job.DeployReply{Ok: true, DsWorkflowCode: int32(code), Msg: "部署成功"}, nil } diff --git a/job/rpc/internal/logic/dsclient.go b/job/rpc/internal/logic/dsclient.go new file mode 100644 index 0000000..e45975e --- /dev/null +++ b/job/rpc/internal/logic/dsclient.go @@ -0,0 +1,383 @@ +package logic + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" +) + +// DolphinScheduler 配置来源:config 结构体为脚手架生成不可改,故从环境变量读取。 +// 未配置 = 调度集成未启用(部署/触发返回业务结果 ok=false,而非进程报错): +// +// DS_BASE_URL DS OpenAPI 基址,如 http://ds:12345/dolphinscheduler +// DS_TOKEN API Token(DS 3.2+ 推荐,请求头 token: xxx) +// DS_PROJECT 目标项目:纯数字按项目编码使用,否则作为项目名按名解析编码 +const ( + envDSBaseURL = "DS_BASE_URL" + envDSToken = "DS_TOKEN" + envDSProject = "DS_PROJECT" + + // dsStatusSuccess DS OpenAPI 统一响应壳的业务成功码(StatusSuccess) + dsStatusSuccess = 0 + // maxRespBytes 响应体读取上限(防异常实例拖爆内存) + maxRespBytes = 1 << 20 + // defaultDSTimeout 单请求超时 + defaultDSTimeout = 10 * time.Second +) + +// DS OpenAPI v3 路径模板(%s=项目编码) +const ( + dsPathProjects = "/projects" // GET 项目列表(ping / 按名解析项目编码) + dsPathProcessDefinition = "/projects/%s/process-definition" // GET 查询 / POST 创建工作流定义 + dsPathRelease = "/projects/%s/process-definition/%d/release" // POST 上线工作流(releaseState=ONLINE) + dsPathStart = "/projects/%s/executors/start-process-instance" // POST 手动运行工作流 + dsPathProcessInstances = "/projects/%s/process-instances" // GET 工作流实例列表(查最新状态) +) + +// dsEnvelope DS OpenAPI 统一响应壳:{code, msg, data} +type dsEnvelope struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data json.RawMessage `json:"data"` +} + +// dsClient DolphinScheduler OpenAPI 轻量客户端(纯标准库 net/http + encoding/json) +type dsClient struct { + base string // OpenAPI 基址(末尾无 /) + token string // API Token(请求头 token) + project string // 项目编码或项目名(env DS_PROJECT) + hc *http.Client // 可注入,单测用 httptest.Server +} + +// enabled 是否已启用(DS_BASE_URL 未配置 = 未启用,client 为 nil) +func (c *dsClient) enabled() bool { + return c != nil && c.base != "" +} + +// dsClientFromEnv 按环境变量装配客户端;DS_BASE_URL 未配置返回 nil(未启用)。 +// getenv 与 http.Client 均可注入,便于单测 +func dsClientFromEnv(getenv func(string) string, hc *http.Client) *dsClient { + base := strings.TrimRight(strings.TrimSpace(getenv(envDSBaseURL)), "/") + if base == "" { + return nil + } + if hc == nil { + hc = &http.Client{Timeout: defaultDSTimeout} + } + return &dsClient{ + base: base, + token: strings.TrimSpace(getenv(envDSToken)), + project: strings.TrimSpace(getenv(envDSProject)), + hc: hc, + } +} + +// ds 进程内单例:环境变量驱动的默认客户端(未配置返回 nil) +var ( + dsOnce sync.Once + dsDefault *dsClient +) + +func ds() *dsClient { + dsOnce.Do(func() { + dsDefault = dsClientFromEnv(os.Getenv, &http.Client{Timeout: defaultDSTimeout}) + }) + return dsDefault +} + +// call 发起一次 DS OpenAPI 请求:构造 URL/请求头/请求体并校验 HTTP 状态码。 +// 返回原始响应体(HTTP 层校验),业务码校验在 envelope +func (c *dsClient) call(method, path string, query url.Values, body any) ([]byte, error) { + var buf io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("DS 请求体序列化失败(%s %s): %w", method, path, err) + } + buf = bytes.NewReader(b) + } + u := c.base + path + if len(query) > 0 { + u += "?" + query.Encode() + } + req, err := http.NewRequest(method, u, buf) + if err != nil { + return nil, fmt.Errorf("DS 请求构造失败(%s %s): %w", method, path, err) + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + // TODO(联调): 需真实 DolphinScheduler 3.x 实例验证 —— 3.2+ 推荐 token 请求头鉴权; + // 更早版本可用 POST {base}/login(userName/userPassword 表单) 换 sessionId(cookie) 替代 + if c.token != "" { + req.Header.Set("token", c.token) + } + resp, err := c.hc.Do(req) + if err != nil { + return nil, fmt.Errorf("DS 请求失败(%s %s): %w", method, path, err) + } + defer resp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(resp.Body, maxRespBytes)) + if err != nil { + return nil, fmt.Errorf("DS 响应读取失败(%s %s): %w", method, path, err) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("DS 响应状态码异常(%s %s): http %d: %s", + method, path, resp.StatusCode, dsExcerpt(raw)) + } + return raw, nil +} + +// envelope 解析统一响应壳并校验业务码(DS 约定 code=0 为成功),返回 data 段 +func (c *dsClient) envelope(method, path string, raw []byte) (json.RawMessage, error) { + var env dsEnvelope + if err := json.Unmarshal(raw, &env); err != nil { + return nil, fmt.Errorf("DS 响应解析失败(%s %s): %w: %s", method, path, err, dsExcerpt(raw)) + } + if env.Code != dsStatusSuccess { + return nil, fmt.Errorf("DS 业务失败(%s %s): code=%d msg=%s", method, path, env.Code, env.Msg) + } + return env.Data, nil +} + +// ping 连通性探测:拉取项目列表第一页(同时校验地址可达与 token 有效) +// TODO(联调): 需真实 DolphinScheduler 3.x 实例验证 +func (c *dsClient) ping() error { + query := url.Values{"pageNo": {"1"}, "pageSize": {"1"}} + raw, err := c.call(http.MethodGet, dsPathProjects, query, nil) + if err != nil { + return err + } + _, err = c.envelope(http.MethodGet, dsPathProjects, raw) + return err +} + +// projectItem 项目/工作流列表条目(仅取需要的字段) +type projectItem struct { + Code int64 `json:"code"` + Name string `json:"name"` +} + +// resolveProject 解析项目编码:纯数字直接使用;否则作为项目名查询解析。 +// TODO(联调): 需真实 DolphinScheduler 3.x 实例验证项目列表接口的过滤参数与分页结构 +func (c *dsClient) resolveProject() (string, error) { + p := strings.TrimSpace(c.project) + if p == "" { + return "", errors.New("调度器项目未配置:需设置环境变量 DS_PROJECT(项目编码或项目名)") + } + if isAllDigits(p) { + return p, nil + } + query := url.Values{"pageNo": {"1"}, "pageSize": {"100"}, "searchVal": {p}} + raw, err := c.call(http.MethodGet, dsPathProjects, query, nil) + if err != nil { + return "", err + } + data, err := c.envelope(http.MethodGet, dsPathProjects, raw) + if err != nil { + return "", err + } + var page struct { + TotalList []projectItem `json:"totalList"` + } + if err := json.Unmarshal(data, &page); err != nil { + return "", fmt.Errorf("DS 项目列表解析失败: %w: %s", err, dsExcerpt(data)) + } + for _, item := range page.TotalList { + if item.Name == p && item.Code > 0 { + return strconv.FormatInt(item.Code, 10), nil + } + } + return "", fmt.Errorf("调度器项目 %q 不存在", p) +} + +// ensureWorkflow 幂等确保工作流定义存在:同名(精确匹配)直接复用其编码, +// 否则创建空任务壳并返回新编码 +// TODO(联调): 需真实 DolphinScheduler 3.x 实例验证 —— 3.x 官方创建接口的参数为 +// name/description/locations + taskDefinitionJson/taskRelationJson(表单或 JSON), +// 此处按管控面骨架先提交 name/location/description/tasks 空任务壳,联调时校正字段名 +func (c *dsClient) ensureWorkflow(name string) (int, error) { + project, err := c.resolveProject() + if err != nil { + return 0, err + } + path := fmt.Sprintf(dsPathProcessDefinition, project) + + // 1) 按名查询已有工作流(精确匹配,保证幂等) + query := url.Values{"pageNo": {"1"}, "pageSize": {"50"}, "searchVal": {name}} + raw, err := c.call(http.MethodGet, path, query, nil) + if err != nil { + return 0, err + } + data, err := c.envelope(http.MethodGet, path, raw) + if err != nil { + return 0, err + } + var page struct { + TotalList []projectItem `json:"totalList"` + } + if err := json.Unmarshal(data, &page); err != nil { + return 0, fmt.Errorf("DS 工作流列表解析失败(%s): %w: %s", path, err, dsExcerpt(data)) + } + for _, item := range page.TotalList { + if item.Name == name && item.Code > 0 { + return int(item.Code), nil + } + } + + // 2) 不存在则创建空任务壳(任务定义后续由执行面推送) + body := map[string]any{ + "name": name, + "location": "{}", + "description": "由数据中台 job 服务自动创建的同步任务工作流", + "tasks": []any{}, + } + raw, err = c.call(http.MethodPost, path, nil, body) + if err != nil { + return 0, err + } + data, err = c.envelope(http.MethodPost, path, raw) + if err != nil { + return 0, err + } + return parseCreatedCode(data) +} + +// release 上线工作流(releaseState=ONLINE,未上线的工作流不允许被调度/手动运行) +// TODO(联调): 需真实 DolphinScheduler 3.x 实例验证 +func (c *dsClient) release(code int) error { + project, err := c.resolveProject() + if err != nil { + return err + } + path := fmt.Sprintf(dsPathRelease, project, code) + raw, err := c.call(http.MethodPost, path, nil, map[string]string{"releaseState": "ONLINE"}) + if err != nil { + return err + } + _, err = c.envelope(http.MethodPost, path, raw) + return err +} + +// start 手动运行工作流,返回新实例 id +// TODO(联调): 需真实 DolphinScheduler 3.x 实例验证(补齐 failureStrategy 等运行策略参数) +func (c *dsClient) start(code int) (int, error) { + project, err := c.resolveProject() + if err != nil { + return 0, err + } + path := fmt.Sprintf(dsPathStart, project) + raw, err := c.call(http.MethodPost, path, nil, map[string]any{"processDefinitionCode": code}) + if err != nil { + return 0, err + } + data, err := c.envelope(http.MethodPost, path, raw) + if err != nil { + return 0, err + } + return parseInstanceId(data) +} + +// latestInstanceStatus 查询工作流最新一次实例状态(无实例返回空串) +func (c *dsClient) latestInstanceStatus(code int) (string, error) { + state, _, err := c.latestInstance(code) + return state, err +} + +// latestInstance 查询工作流最新一次实例的状态与实例 id(无实例返回空串/0) +// TODO(联调): 需真实 DolphinScheduler 3.x 实例验证 processDefinitionCode 过滤参数 +func (c *dsClient) latestInstance(code int) (string, int, error) { + project, err := c.resolveProject() + if err != nil { + return "", 0, err + } + path := fmt.Sprintf(dsPathProcessInstances, project) + query := url.Values{ + "pageNo": {"1"}, + "pageSize": {"1"}, + "processDefinitionCode": {strconv.Itoa(code)}, + } + raw, err := c.call(http.MethodGet, path, query, nil) + if err != nil { + return "", 0, err + } + data, err := c.envelope(http.MethodGet, path, raw) + if err != nil { + return "", 0, err + } + var page struct { + TotalList []struct { + Id int64 `json:"id"` + State string `json:"state"` + } `json:"totalList"` + } + if err := json.Unmarshal(data, &page); err != nil { + return "", 0, fmt.Errorf("DS 实例列表解析失败(%s): %w: %s", path, err, dsExcerpt(data)) + } + if len(page.TotalList) == 0 { + return "", 0, nil + } + first := page.TotalList[0] + return first.State, int(first.Id), nil +} + +// parseCreatedCode 解析创建工作流响应中的工作流编码(不同版本 data 为对象 {code:n} 或裸数字) +func parseCreatedCode(data json.RawMessage) (int, error) { + var obj struct { + Code int64 `json:"code"` + } + if err := json.Unmarshal(data, &obj); err == nil && obj.Code > 0 { + return int(obj.Code), nil + } + var n int64 + if err := json.Unmarshal(data, &n); err == nil && n > 0 { + return int(n), nil + } + return 0, fmt.Errorf("DS 创建工作流响应缺少工作流编码: %s", dsExcerpt(data)) +} + +// parseInstanceId 解析启动实例响应中的实例 id(不同版本 data 为对象 {id:n} 或裸数字) +func parseInstanceId(data json.RawMessage) (int, error) { + var obj struct { + Id int64 `json:"id"` + } + if err := json.Unmarshal(data, &obj); err == nil && obj.Id > 0 { + return int(obj.Id), nil + } + var n int64 + if err := json.Unmarshal(data, &n); err == nil && n > 0 { + return int(n), nil + } + return 0, fmt.Errorf("DS 启动实例响应缺少实例 id: %s", dsExcerpt(data)) +} + +// isAllDigits 是否为纯数字串 +func isAllDigits(s string) bool { + if s == "" { + return false + } + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true +} + +// dsExcerpt 响应体摘要(截断到 200 字节,错误信息透出用) +func dsExcerpt(raw []byte) string { + const limit = 200 + if len(raw) > limit { + return string(raw[:limit]) + "...(截断)" + } + return string(raw) +} diff --git a/job/rpc/internal/logic/dsclient_test.go b/job/rpc/internal/logic/dsclient_test.go new file mode 100644 index 0000000..1755343 --- /dev/null +++ b/job/rpc/internal/logic/dsclient_test.go @@ -0,0 +1,425 @@ +package logic + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync" + "testing" +) + +// recorded 单个请求的录制信息(请求构造断言用) +type recorded struct { + Method string + Path string + Query url.Values + Token string + Body string +} + +// recorder 捕获 DS 模拟服务收到的请求(handler 在服务 goroutine 执行,须加锁) +type recorder struct { + mu sync.Mutex + rs []recorded +} + +// record 从请求中提取断言所需字段 +func (r *recorder) record(req *http.Request) { + var body string + if req.Body != nil { + b, _ := io.ReadAll(req.Body) + body = string(b) + } + r.mu.Lock() + defer r.mu.Unlock() + r.rs = append(r.rs, recorded{ + Method: req.Method, + Path: req.URL.Path, + Query: req.URL.Query(), + Token: req.Header.Get("token"), + Body: body, + }) +} + +// last 取第 n(从 0 开始)个请求 +func (r *recorder) last(n int) recorded { + r.mu.Lock() + defer r.mu.Unlock() + return r.rs[n] +} + +// count 已录制请求数 +func (r *recorder) count() int { + r.mu.Lock() + defer r.mu.Unlock() + return len(r.rs) +} + +// dsJSON 模拟 DS 统一响应壳 +func dsJSON(code int, msg string, data any) string { + b, _ := json.Marshal(map[string]any{"code": code, "msg": msg, "data": data}) + return string(b) +} + +// newDSTestEnv 启动模拟 DS 服务并返回注入了测试地址的客户端 +func newDSTestEnv(t *testing.T, handler http.HandlerFunc) (*dsClient, *recorder) { + t.Helper() + rec := &recorder{} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + rec.record(req) + handler(w, req) + })) + t.Cleanup(srv.Close) + client := &dsClient{ + base: srv.URL, + token: "test-token", + project: "888", // 纯数字:直接作为项目编码,不触发项目解析 + hc: srv.Client(), + } + return client, rec +} + +// okRoute 固定返回一个成功响应 +func okRoute(payload string) http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(payload)) + } +} + +// mustNoErr 致命错误断言 +func mustNoErr(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// TestDSClientFromEnv 环境变量装配:未配置=未启用(nil),配置=字段透传 +func TestDSClientFromEnv(t *testing.T) { + if c := dsClientFromEnv(func(string) string { return "" }, nil); c != nil { + t.Fatalf("DS_BASE_URL 未配置时应返回 nil, got %+v", c) + } + env := map[string]string{ + "DS_BASE_URL": "http://ds:12345/dolphinscheduler/", + "DS_TOKEN": " abc ", + "DS_PROJECT": "中台同步", + } + c := dsClientFromEnv(func(k string) string { return env[k] }, nil) + if c == nil { + t.Fatal("已配置时应返回客户端") + } + if c.base != "http://ds:12345/dolphinscheduler" { + t.Fatalf("base 应去尾部斜杠, got %q", c.base) + } + if c.token != "abc" || c.project != "中台同步" { + t.Fatalf("token/project 未正确透传: %q %q", c.token, c.project) + } + if !c.enabled() { + t.Fatal("enabled 应为 true") + } +} + +// TestDSPing ping 的请求构造(GET /projects + token 头)与成功解析 +func TestDSPing(t *testing.T) { + client, rec := newDSTestEnv(t, okRoute(dsJSON(0, "success", map[string]any{ + "totalList": []any{}, "total": 0, + }))) + mustNoErr(t, client.ping()) + r := rec.last(0) + if r.Method != http.MethodGet || r.Path != "/projects" { + t.Fatalf("ping 应 GET /projects, got %s %s", r.Method, r.Path) + } + if r.Query.Get("pageNo") != "1" || r.Query.Get("pageSize") != "1" { + t.Fatalf("ping 分页参数缺失: %v", r.Query) + } + if r.Token != "test-token" { + t.Fatalf("token 请求头缺失: %q", r.Token) + } +} + +// TestDSPingTokenInvalid 鉴权失败(HTTP 200 但 DS 业务码非 0)须透出 DS msg +func TestDSPingTokenInvalid(t *testing.T) { + client, _ := newDSTestEnv(t, okRoute(dsJSON(1001, "token 无效", nil))) + err := client.ping() + if err == nil || !strings.Contains(err.Error(), "token 无效") || !strings.Contains(err.Error(), "1001") { + t.Fatalf("应透出 DS 业务失败信息, got %v", err) + } +} + +// TestDSNon200 HTTP 非 200 的错误透出 +func TestDSNon200(t *testing.T) { + client, _ := newDSTestEnv(t, func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("boom")) + }) + err := client.ping() + if err == nil || !strings.Contains(err.Error(), "500") || !strings.Contains(err.Error(), "boom") { + t.Fatalf("应透出 http 状态码与响应摘要, got %v", err) + } +} + +// TestDSUnreachable 不可达(连接拒绝)须返回错误 +func TestDSUnreachable(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + srv.Close() // 立即关闭制造不可达 + client := &dsClient{base: srv.URL, hc: srv.Client()} + if err := client.ping(); err == nil { + t.Fatal("不可达时应返回错误") + } +} + +// TestDSResolveProjectNumeric 纯数字项目编码直接使用,不触发额外解析请求 +func TestDSResolveProjectNumeric(t *testing.T) { + client, rec := newDSTestEnv(t, okRoute(dsJSON(0, "success", nil))) + code, err := client.resolveProject() + mustNoErr(t, err) + if code != "888" { + t.Fatalf("纯数字应直接作为项目编码, got %q", code) + } + if rec.count() != 0 { + t.Fatalf("纯数字项目编码不应发起请求, 请求数=%d", rec.count()) + } +} + +// TestDSResolveProjectByName 项目名解析:按名精确匹配出编码 +func TestDSResolveProjectByName(t *testing.T) { + client, rec := newDSTestEnv(t, okRoute(dsJSON(0, "success", map[string]any{ + "totalList": []any{ + map[string]any{"code": 123, "name": "其它项目"}, + map[string]any{"code": 888, "name": "中台同步"}, + }, + }))) + client.project = "中台同步" + code, err := client.resolveProject() + mustNoErr(t, err) + if code != "888" { + t.Fatalf("应解析出项目编码 888, got %q", code) + } + r := rec.last(0) + if r.Method != http.MethodGet || r.Path != "/projects" || r.Query.Get("searchVal") != "中台同步" { + t.Fatalf("按名解析应 GET /projects 且带 searchVal 过滤, got %s %s %v", r.Method, r.Path, r.Query) + } +} + +// TestDSResolveProjectNotFound 项目名不存在须报错 +func TestDSResolveProjectNotFound(t *testing.T) { + client, _ := newDSTestEnv(t, okRoute(dsJSON(0, "success", map[string]any{ + "totalList": []any{}, "total": 0, + }))) + client.project = "不存在" + if err := func() error { _, err := client.resolveProject(); return err }(); err == nil || !strings.Contains(err.Error(), "不存在") { + t.Fatalf("项目不存在应报错, got %v", err) + } +} + +// TestDSResolveProjectMissing 未配置项目须报业务错误 +func TestDSResolveProjectMissing(t *testing.T) { + client, _ := newDSTestEnv(t, okRoute(dsJSON(0, "success", nil))) + client.project = "" + _, err := client.resolveProject() + if err == nil || !strings.Contains(err.Error(), "DS_PROJECT") { + t.Fatalf("未配置项目应提示 DS_PROJECT, got %v", err) + } +} + +// TestDSEnsureWorkflowCreate 工作流不存在时创建:先查后建,请求构造与编码解析 +func TestDSEnsureWorkflowCreate(t *testing.T) { + client, rec := newDSTestEnv(t, func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch { + case req.Method == http.MethodGet && strings.HasPrefix(req.URL.Path, "/projects/888/process-definition"): + // 查询:无同名工作流 + _, _ = w.Write([]byte(dsJSON(0, "success", map[string]any{"totalList": []any{}, "total": 0}))) + case req.Method == http.MethodPost && req.URL.Path == "/projects/888/process-definition": + // 创建:返回工作流编码(对象形态 data.code) + _, _ = w.Write([]byte(dsJSON(0, "success", map[string]any{"code": 123, "name": "订单同步"}))) + default: + w.WriteHeader(http.StatusNotFound) + } + }) + code, err := client.ensureWorkflow("订单同步") + mustNoErr(t, err) + if code != 123 { + t.Fatalf("应返回新工作流编码 123, got %d", code) + } + if rec.count() != 2 { + t.Fatalf("应先查后建共 2 次请求, got %d", rec.count()) + } + q := rec.last(0) + if q.Method != http.MethodGet || q.Path != "/projects/888/process-definition" || q.Query.Get("searchVal") != "订单同步" { + t.Fatalf("查询请求构造不符: %+v", q) + } + c := rec.last(1) + if c.Method != http.MethodPost || c.Path != "/projects/888/process-definition" { + t.Fatalf("创建请求构造不符: %+v", c) + } + var body map[string]any + if err := json.Unmarshal([]byte(c.Body), &body); err != nil { + t.Fatalf("创建请求体应为合法 JSON: %v", c.Body) + } + if body["name"] != "订单同步" { + t.Fatalf("创建请求体 name 不符: %v", body["name"]) + } + if tasks, ok := body["tasks"].([]any); !ok || len(tasks) != 0 { + t.Fatalf("创建请求体应为空任务壳 tasks:[], got %v", body["tasks"]) + } + if _, ok := body["location"]; !ok { + t.Fatal("创建请求体缺少 location") + } +} + +// TestDSEnsureWorkflowExists 同名工作流已存在时直接复用(幂等,不发起创建) +func TestDSEnsureWorkflowExists(t *testing.T) { + client, rec := newDSTestEnv(t, func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(dsJSON(0, "success", map[string]any{ + "totalList": []any{ + map[string]any{"code": 77, "name": "别的任务"}, + map[string]any{"code": 66, "name": "订单同步"}, + }, + }))) + }) + code, err := client.ensureWorkflow("订单同步") + mustNoErr(t, err) + if code != 66 { + t.Fatalf("应复用已有工作流编码 66, got %d", code) + } + if rec.count() != 1 { + t.Fatalf("已存在时不应发起创建请求, 请求数=%d", rec.count()) + } +} + +// TestDSEnsureWorkflowCreateBareCode 创建响应 data 为裸数字形态时的编码解析 +func TestDSEnsureWorkflowCreateBareCode(t *testing.T) { + client, _ := newDSTestEnv(t, func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + if req.Method == http.MethodGet { + _, _ = w.Write([]byte(dsJSON(0, "success", map[string]any{"totalList": []any{}, "total": 0}))) + return + } + _, _ = w.Write([]byte(dsJSON(0, "success", 456))) + }) + code, err := client.ensureWorkflow("t") + mustNoErr(t, err) + if code != 456 { + t.Fatalf("裸数字形态应解析出 456, got %d", code) + } +} + +// TestDSEnsureWorkflowBizError 创建接口业务失败须透出 DS msg +func TestDSEnsureWorkflowBizError(t *testing.T) { + client, _ := newDSTestEnv(t, func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + if req.Method == http.MethodGet { + _, _ = w.Write([]byte(dsJSON(0, "success", map[string]any{"totalList": []any{}, "total": 0}))) + return + } + _, _ = w.Write([]byte(dsJSON(10005, "工作流名称已存在", nil))) + }) + _, err := client.ensureWorkflow("t") + if err == nil || !strings.Contains(err.Error(), "工作流名称已存在") { + t.Fatalf("应透出创建失败原因, got %v", err) + } +} + +// TestDSRelease 上线请求构造(POST .../release + releaseState=ONLINE) +func TestDSRelease(t *testing.T) { + client, rec := newDSTestEnv(t, okRoute(dsJSON(0, "success", nil))) + mustNoErr(t, client.release(123)) + r := rec.last(0) + if r.Method != http.MethodPost || r.Path != "/projects/888/process-definition/123/release" { + t.Fatalf("release 请求构造不符: %s %s", r.Method, r.Path) + } + if !strings.Contains(r.Body, `"releaseState":"ONLINE"`) { + t.Fatalf("release 应提交 releaseState=ONLINE, got %s", r.Body) + } +} + +// TestDSReleaseBizError 上线失败透出原因 +func TestDSReleaseBizError(t *testing.T) { + client, _ := newDSTestEnv(t, okRoute(dsJSON(10017, "工作流未保存", nil))) + err := client.release(123) + if err == nil || !strings.Contains(err.Error(), "工作流未保存") { + t.Fatalf("应透出上线失败原因, got %v", err) + } +} + +// TestDSStart 启动实例:请求构造与实例 id 解析(对象形态) +func TestDSStart(t *testing.T) { + client, rec := newDSTestEnv(t, okRoute(dsJSON(0, "success", map[string]any{"id": 42}))) + id, err := client.start(123) + mustNoErr(t, err) + if id != 42 { + t.Fatalf("应返回实例 id 42, got %d", id) + } + r := rec.last(0) + if r.Method != http.MethodPost || r.Path != "/projects/888/executors/start-process-instance" { + t.Fatalf("start 请求构造不符: %s %s", r.Method, r.Path) + } + var body map[string]any + if err := json.Unmarshal([]byte(r.Body), &body); err != nil { + t.Fatalf("start 请求体应为合法 JSON: %v", r.Body) + } + if body["processDefinitionCode"] != float64(123) { + t.Fatalf("start 应提交 processDefinitionCode=123, got %v", body["processDefinitionCode"]) + } +} + +// TestDSStartBareId 启动响应 data 为裸数字形态时的实例 id 解析 +func TestDSStartBareId(t *testing.T) { + client, _ := newDSTestEnv(t, okRoute(dsJSON(0, "success", 43))) + id, err := client.start(123) + mustNoErr(t, err) + if id != 43 { + t.Fatalf("裸数字形态应解析出 43, got %d", id) + } +} + +// TestDSStartMissingId 启动响应缺实例 id 须报错 +func TestDSStartMissingId(t *testing.T) { + client, _ := newDSTestEnv(t, okRoute(dsJSON(0, "success", map[string]any{}))) + if _, err := client.start(123); err == nil || !strings.Contains(err.Error(), "实例 id") { + t.Fatalf("缺实例 id 应报错, got %v", err) + } +} + +// TestDSLatestInstance 实例状态查询:请求构造、状态与实例 id 解析 +func TestDSLatestInstance(t *testing.T) { + client, rec := newDSTestEnv(t, okRoute(dsJSON(0, "success", map[string]any{ + "totalList": []any{map[string]any{"id": 9, "state": "RUNNING_EXECUTION", "name": "订单同步-1-2024"}}, + "total": 1, + }))) + state, id, err := client.latestInstance(123) + mustNoErr(t, err) + if state != "RUNNING_EXECUTION" || id != 9 { + t.Fatalf("应解析出 RUNNING_EXECUTION/9, got %s/%d", state, id) + } + r := rec.last(0) + if r.Method != http.MethodGet || r.Path != "/projects/888/process-instances" { + t.Fatalf("实例查询请求构造不符: %s %s", r.Method, r.Path) + } + if r.Query.Get("processDefinitionCode") != "123" || r.Query.Get("pageSize") != "1" { + t.Fatalf("实例查询过滤参数不符: %v", r.Query) + } + stateOnly, err := client.latestInstanceStatus(123) + mustNoErr(t, err) + if stateOnly != "RUNNING_EXECUTION" { + t.Fatalf("latestInstanceStatus 应返回 %q, got %q", "RUNNING_EXECUTION", stateOnly) + } +} + +// TestDSLatestInstanceEmpty 无运行实例时返回空串(不视为错误) +func TestDSLatestInstanceEmpty(t *testing.T) { + client, _ := newDSTestEnv(t, okRoute(dsJSON(0, "success", map[string]any{ + "totalList": []any{}, "total": 0, + }))) + state, id, err := client.latestInstance(123) + mustNoErr(t, err) + if state != "" || id != 0 { + t.Fatalf("无实例应返回空串/0, got %s/%d", state, id) + } +} diff --git a/job/rpc/internal/logic/error.go b/job/rpc/internal/logic/error.go new file mode 100644 index 0000000..53c708d --- /dev/null +++ b/job/rpc/internal/logic/error.go @@ -0,0 +1,27 @@ +package logic + +import "errors" + +// 业务错误(对齐 quality/metadata 服务写法:errors.New 中文语义错误,调用方用 errors.Is 判断) +var ( + // ErrTaskIdRequired 任务 id 缺失 + ErrTaskIdRequired = errors.New("任务 id 不能为空") + // ErrTaskNameRequired 任务名缺失 + ErrTaskNameRequired = errors.New("任务名不能为空") + // ErrTaskNameTooLong 任务名超长(dc_sync_task.name 为 varchar(128)) + ErrTaskNameTooLong = errors.New("任务名长度不能超过 128 字符") + // ErrInvalidMode 同步模式不在白名单 + ErrInvalidMode = errors.New("mode 非法:仅支持 offline/cdc") + // ErrSourceDsRequired 源数据源缺失 + ErrSourceDsRequired = errors.New("source_ds_id 不能为空(需>0)") + // ErrInvalidMappingJson 字段/表映射 JSON 非法 + ErrInvalidMappingJson = errors.New("mapping_json 非法:需为 JSON 对象") + // ErrInvalidStatus 列表过滤状态不在白名单 + ErrInvalidStatus = errors.New("status 非法:仅支持 draft/deployed/running/failed") + // ErrTaskNotFound 任务不存在(含跨租户不可见) + ErrTaskNotFound = errors.New("同步任务不存在") + // ErrTaskDeployed 已部署任务在调度器侧仍有工作流,需先下线才能删除 + ErrTaskDeployed = errors.New("任务已部署到调度器,请先下线后再删除") + // ErrTaskNotDeployed 未部署任务不能手动触发 + ErrTaskNotDeployed = errors.New("任务尚未部署到调度器,请先执行部署") +) diff --git a/job/rpc/internal/logic/getSchedulerInfoLogic.go b/job/rpc/internal/logic/getSchedulerInfoLogic.go index b4f6529..11b465c 100644 --- a/job/rpc/internal/logic/getSchedulerInfoLogic.go +++ b/job/rpc/internal/logic/getSchedulerInfoLogic.go @@ -23,9 +23,29 @@ func NewGetSchedulerInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } } -// 调度器连接信息(运维视图) +// 调度器连接信息(运维视图:是否配置 / 当前是否可达 / 基址) func (l *GetSchedulerInfoLogic) GetSchedulerInfo(in *job.IdReq) (*job.SchedulerReply, error) { - // todo: add your logic here and delete this line - - return &job.SchedulerReply{}, nil + client := ds() + if !client.enabled() { + return &job.SchedulerReply{ + Configured: false, + Connected: false, + BaseUrl: "", + Msg: "未配置 DolphinScheduler:需设置环境变量 DS_BASE_URL/DS_TOKEN/DS_PROJECT", + }, nil + } + reply := &job.SchedulerReply{ + Configured: true, + BaseUrl: client.base, + } + // 连通性探测:拉取项目列表第一页(校验地址可达与 token 有效) + if err := client.ping(); err != nil { + l.Errorf("调度器连通性探测失败: %v", err) + reply.Connected = false + reply.Msg = "调度器不可达: " + err.Error() + return reply, nil + } + reply.Connected = true + reply.Msg = "调度器连接正常" + return reply, nil } diff --git a/job/rpc/internal/logic/getTaskStatusLogic.go b/job/rpc/internal/logic/getTaskStatusLogic.go index 260abc5..8bf01ee 100644 --- a/job/rpc/internal/logic/getTaskStatusLogic.go +++ b/job/rpc/internal/logic/getTaskStatusLogic.go @@ -2,6 +2,7 @@ package logic import ( "context" + "fmt" "datacenter/job/rpc/internal/svc" "datacenter/job/rpc/job" @@ -23,9 +24,36 @@ func NewGetTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get } } -// 任务状态(本地 + 调度器实例状态) +// 任务状态(本地 status + 若已部署则聚合调度器最新实例状态,展示口径不落库) func (l *GetTaskStatusLogic) GetTaskStatus(in *job.IdReq) (*job.StatusReply, error) { - // todo: add your logic here and delete this line - - return &job.StatusReply{}, nil + task, err := loadTask(l.ctx, l.svcCtx.SyncTaskModel, in.Id, in.Beid) + if err != nil { + return nil, err + } + reply := &job.StatusReply{Status: task.Status} + // 未部署:无调度器实例可查,直接展示本地状态 + if task.DsWorkflowCode <= 0 { + reply.Msg = "任务未部署,展示本地状态" + return reply, nil + } + client := ds() + if !client.enabled() { + reply.Msg = "调度器未配置,展示本地状态" + return reply, nil + } + // 查询调度器最新实例并聚合(DS 状态可识别时以调度器为准) + state, instanceId, err := client.latestInstance(int(task.DsWorkflowCode)) + if err != nil { + l.Errorf("查询任务 %d 的 DS 实例状态失败: %v", task.Id, err) + reply.Msg = "调度器不可达,展示本地状态: " + err.Error() + return reply, nil + } + if state == "" { + reply.Msg = fmt.Sprintf("调度器暂无该任务(工作流编码 %d)的运行实例,展示本地状态", task.DsWorkflowCode) + return reply, nil + } + reply.Status = AggregateStatus(task.Status, state) + reply.DsInstanceId = int32(instanceId) + reply.Msg = fmt.Sprintf("调度器最新实例状态 %s(工作流编码 %d)", state, task.DsWorkflowCode) + return reply, nil } diff --git a/job/rpc/internal/logic/listSyncTasksLogic.go b/job/rpc/internal/logic/listSyncTasksLogic.go index 8575744..673c833 100644 --- a/job/rpc/internal/logic/listSyncTasksLogic.go +++ b/job/rpc/internal/logic/listSyncTasksLogic.go @@ -3,6 +3,7 @@ package logic import ( "context" + "datacenter/job/model" "datacenter/job/rpc/internal/svc" "datacenter/job/rpc/job" @@ -23,9 +24,23 @@ func NewListSyncTasksLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Lis } } -// 任务列表 +// 任务列表(分页 + status 可选过滤) func (l *ListSyncTasksLogic) ListSyncTasks(in *job.TaskListReq) (*job.TaskListReply, error) { - // todo: add your logic here and delete this line - - return &job.TaskListReply{}, nil + // 多租户条件必填 + if in.Beid <= 0 { + return nil, model.ErrBeidRequired + } + // status 过滤入参白名单校验(允许空 = 不过滤) + if in.Status != "" && !model.IsValidTaskStatus(in.Status) { + return nil, ErrInvalidStatus + } + list, total, err := l.svcCtx.SyncTaskModel.FindList(l.ctx, in.Beid, in.Status, in.Page, in.Size) + if err != nil { + return nil, err + } + tasks := make([]*job.SyncTask, 0, len(list)) + for _, t := range list { + tasks = append(tasks, taskToPb(t)) + } + return &job.TaskListReply{List: tasks, Total: total}, nil } diff --git a/job/rpc/internal/logic/status.go b/job/rpc/internal/logic/status.go new file mode 100644 index 0000000..094fbf5 --- /dev/null +++ b/job/rpc/internal/logic/status.go @@ -0,0 +1,137 @@ +package logic + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "unicode/utf8" + + "datacenter/job/model" +) + +// 同步任务状态流转纯函数(对齐 quality 服务 checker 纯函数风格,全部可单测) + +// maxNameLen 任务名长度上限(dc_sync_task.name 为 varchar(128)) +const maxNameLen = 128 + +// IsValidMode 同步模式是否在白名单内 +func IsValidMode(mode string) bool { + return mode == model.TaskModeOffline || mode == model.TaskModeCdc +} + +// ValidateSyncTask 创建同步任务前的静态校验(纯函数,内部做首尾空白防御): +// 名称非空且 ≤128 字符、mode 白名单(offline/cdc)、source_ds_id>0、 +// mapping_json 允许为空,非空时须为 JSON 对象 +func ValidateSyncTask(name, mode string, sourceDsId int64, mappingJson string) error { + name = strings.TrimSpace(name) + if utf8.RuneCountInString(name) == 0 { + return ErrTaskNameRequired + } + if utf8.RuneCountInString(name) > maxNameLen { + return ErrTaskNameTooLong + } + if !IsValidMode(mode) { + return ErrInvalidMode + } + if sourceDsId <= 0 { + return ErrSourceDsRequired + } + if s := strings.TrimSpace(mappingJson); s != "" { + var m map[string]any + if err := json.Unmarshal([]byte(s), &m); err != nil { + return fmt.Errorf("%w: %v", ErrInvalidMappingJson, err) + } + } + return nil +} + +// CanDelete 删除前置检查(纯函数):仅草稿可直接删除; +// 已部署/运行中/失败的任务在调度器侧仍存在工作流,需先下线再删。 +// 返回是否允许删除与拒绝原因 +func CanDelete(status string) (bool, string) { + if status == model.TaskStatusDraft { + return true, "" + } + return false, "任务已部署到调度器,请先在调度器侧下线后再删除" +} + +// DolphinScheduler 工作流实例状态(3.x ExecutionStatus 枚举)→ 本地状态词汇映射: +// 运行中 / 成功(终态回落 deployed)/ 失败(终态回落 failed);无法识别返回空串 +var ( + dsRunningStates = map[string]struct{}{ + "RUNNING": {}, // 兼容旧版本 + "RUNNING_EXECUTION": {}, + "SUBMITTED_SUCCESS": {}, + "READY_PAUSE": {}, + "READY_STOP": {}, + "NEED_FAULT_TOLERANCE": {}, + "DELAY_EXECUTION": {}, + "SERIAL_WAIT": {}, + } + dsSuccessStates = map[string]struct{}{ + "SUCCESS": {}, + "FORCED_SUCCESS": {}, + } + dsFailedStates = map[string]struct{}{ + "FAILURE": {}, + "KILL": {}, + "STOP": {}, + "PAUSE": {}, + } +) + +// mapDSState DS 实例状态 → 本地任务状态(纯函数): +// 运行中→running、成功→deployed(任务仍在调度,仅实例终态为成功)、失败→failed; +// 无法识别返回空串,由调用方保持本地状态 +func mapDSState(state string) string { + switch { + case containsKey(dsRunningStates, state): + return model.TaskStatusRunning + case containsKey(dsSuccessStates, state): + return model.TaskStatusDeployed + case containsKey(dsFailedStates, state): + return model.TaskStatusFailed + default: + return "" + } +} + +// AggregateStatus 本地状态 + DS 最新实例状态聚合(纯函数,GetTaskStatus 展示口径, +// 不落库):DS 状态可识别时以调度器为准,否则回落本地状态 +func AggregateStatus(local, dsState string) string { + if mapped := mapDSState(dsState); mapped != "" { + return mapped + } + return local +} + +// loadTask 按 id+beid 加载同步任务(beid/id 缺失校验 + not found 翻译为业务错误) +func loadTask(ctx context.Context, m model.SyncTaskModel, id, beid int64) (*model.SyncTask, error) { + if beid <= 0 { + return nil, model.ErrBeidRequired + } + if id <= 0 { + return nil, ErrTaskIdRequired + } + task, err := m.FindOne(ctx, id, beid) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, ErrTaskNotFound + } + return nil, err + } + return task, nil +} + +// dsErrMsg 统一拼接 DS 失败原因(部署/触发失败是业务结果而非 error,msg 需可读) +func dsErrMsg(step string, err error) string { + return fmt.Sprintf("%s: %v", step, err) +} + +// containsKey 集合是否包含 key +func containsKey(set map[string]struct{}, key string) bool { + _, ok := set[key] + return ok +} diff --git a/job/rpc/internal/logic/status_test.go b/job/rpc/internal/logic/status_test.go new file mode 100644 index 0000000..d3d4288 --- /dev/null +++ b/job/rpc/internal/logic/status_test.go @@ -0,0 +1,112 @@ +package logic + +import ( + "errors" + "testing" + + "datacenter/job/model" +) + +// TestValidateSyncTask 创建任务的静态校验(名称/mode 白名单/source_ds_id/mapping_json) +func TestValidateSyncTask(t *testing.T) { + cases := []struct { + name string + taskName string + mode string + sourceDsId int64 + mappingJson string + wantErr error + }{ + {name: "离线任务合法", taskName: "订单同步", mode: "offline", sourceDsId: 1}, + {name: "cdc 任务合法", taskName: "订单同步", mode: "cdc", sourceDsId: 1, mappingJson: `{"table":"orders"}`}, + {name: "名称为空", taskName: " ", mode: "offline", sourceDsId: 1, wantErr: ErrTaskNameRequired}, + {name: "名称超长", taskName: repeat("名", 129), mode: "offline", sourceDsId: 1, wantErr: ErrTaskNameTooLong}, + {name: "mode 不在白名单", taskName: "t", mode: "stream", sourceDsId: 1, wantErr: ErrInvalidMode}, + {name: "mode 为空", taskName: "t", mode: "", sourceDsId: 1, wantErr: ErrInvalidMode}, + {name: "源数据源缺失", taskName: "t", mode: "offline", sourceDsId: 0, wantErr: ErrSourceDsRequired}, + {name: "mapping_json 非法", taskName: "t", mode: "offline", sourceDsId: 1, mappingJson: "{bad", wantErr: ErrInvalidMappingJson}, + {name: "mapping_json 须为对象", taskName: "t", mode: "offline", sourceDsId: 1, mappingJson: "[1,2]", wantErr: ErrInvalidMappingJson}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := ValidateSyncTask(c.taskName, c.mode, c.sourceDsId, c.mappingJson) + if !errors.Is(err, c.wantErr) { + t.Fatalf("ValidateSyncTask = %v, want %v", err, c.wantErr) + } + }) + } +} + +// repeat 生成 n 次重复字符串(测试辅助,避免 strings.Builder 样板) +func repeat(s string, n int) string { + b := make([]byte, 0, len(s)*n) + for i := 0; i < n; i++ { + b = append(b, s...) + } + return string(b) +} + +// TestCanDelete 删除前置检查:仅草稿可删,已部署/运行中/失败需先下线 +func TestCanDelete(t *testing.T) { + for _, status := range []string{model.TaskStatusDraft} { + if ok, reason := CanDelete(status); !ok || reason != "" { + t.Fatalf("状态 %s 应可直接删除, got ok=%v reason=%q", status, ok, reason) + } + } + for _, status := range []string{model.TaskStatusDeployed, model.TaskStatusRunning, model.TaskStatusFailed} { + if ok, reason := CanDelete(status); ok || reason == "" { + t.Fatalf("状态 %s 应拒绝删除并给出原因, got ok=%v reason=%q", status, ok, reason) + } + } +} + +// TestMapDSState DS 实例状态 → 本地状态映射 +func TestMapDSState(t *testing.T) { + cases := []struct { + state string + want string + }{ + {"RUNNING_EXECUTION", model.TaskStatusRunning}, + {"RUNNING", model.TaskStatusRunning}, // 兼容旧版本状态名 + {"SUBMITTED_SUCCESS", model.TaskStatusRunning}, + {"DELAY_EXECUTION", model.TaskStatusRunning}, + {"SERIAL_WAIT", model.TaskStatusRunning}, + {"SUCCESS", model.TaskStatusDeployed}, + {"FORCED_SUCCESS", model.TaskStatusDeployed}, + {"FAILURE", model.TaskStatusFailed}, + {"KILL", model.TaskStatusFailed}, + {"STOP", model.TaskStatusFailed}, + {"PAUSE", model.TaskStatusFailed}, + {"UNKNOWN_STATE", ""}, // 无法识别回落空串 + {"", ""}, // 无实例 + } + for _, c := range cases { + if got := mapDSState(c.state); got != c.want { + t.Fatalf("mapDSState(%q) = %q, want %q", c.state, got, c.want) + } + } +} + +// TestAggregateStatus 本地状态 + DS 实例状态聚合:DS 可识别时以调度器为准,否则回落本地 +func TestAggregateStatus(t *testing.T) { + cases := []struct { + local string + dsState string + want string + }{ + // DS 运行中 → running + {model.TaskStatusDeployed, "RUNNING_EXECUTION", model.TaskStatusRunning}, + // DS 成功终态 → deployed(任务仍在调度) + {model.TaskStatusRunning, "SUCCESS", model.TaskStatusDeployed}, + // DS 失败终态 → failed + {model.TaskStatusDeployed, "FAILURE", model.TaskStatusFailed}, + // DS 状态无法识别/无实例 → 保持本地 + {model.TaskStatusDraft, "", model.TaskStatusDraft}, + {model.TaskStatusDeployed, "WHAT_EVER", model.TaskStatusDeployed}, + } + for _, c := range cases { + if got := AggregateStatus(c.local, c.dsState); got != c.want { + t.Fatalf("AggregateStatus(%q, %q) = %q, want %q", c.local, c.dsState, got, c.want) + } + } +} diff --git a/job/rpc/internal/logic/triggerTaskLogic.go b/job/rpc/internal/logic/triggerTaskLogic.go index 0ebd217..e3a20a7 100644 --- a/job/rpc/internal/logic/triggerTaskLogic.go +++ b/job/rpc/internal/logic/triggerTaskLogic.go @@ -2,7 +2,9 @@ package logic import ( "context" + "time" + "datacenter/job/model" "datacenter/job/rpc/internal/svc" "datacenter/job/rpc/job" @@ -23,9 +25,30 @@ func NewTriggerTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Trigg } } -// 手动触发一次 +// 手动触发一次(未部署 → 业务错误;DS 未配置/不可达/启动失败 → 业务结果 ok=false 带原因) func (l *TriggerTaskLogic) TriggerTask(in *job.IdReq) (*job.TriggerReply, error) { - // todo: add your logic here and delete this line - - return &job.TriggerReply{}, nil + task, err := loadTask(l.ctx, l.svcCtx.SyncTaskModel, in.Id, in.Beid) + if err != nil { + return nil, err + } + // 未部署(无工作流编码)不允许触发 + if task.DsWorkflowCode <= 0 { + return nil, ErrTaskNotDeployed + } + client := ds() + if !client.enabled() { + return &job.TriggerReply{Ok: false, Msg: "调度器未配置:需设置环境变量 DS_BASE_URL/DS_TOKEN/DS_PROJECT"}, nil + } + // 调 DS 启动一次工作流实例 + instanceId, err := client.start(int(task.DsWorkflowCode)) + if err != nil { + l.Errorf("触发任务 %d 失败(工作流 %d): %v", task.Id, task.DsWorkflowCode, err) + return &job.TriggerReply{Ok: false, Msg: dsErrMsg("调度器启动实例失败", err)}, nil + } + // 触发成功:状态流转 running 并回填最近运行时间 + if err := l.svcCtx.SyncTaskModel.UpdateRunStatus(l.ctx, task.Id, in.Beid, model.TaskStatusRunning, time.Now()); err != nil { + return nil, err + } + l.Infof("任务 %d 已触发,DS 实例 id %d", task.Id, instanceId) + return &job.TriggerReply{Ok: true, DsProcessInstanceId: int32(instanceId), Msg: "已触发"}, nil } diff --git a/job/rpc/internal/svc/serviceContext.go b/job/rpc/internal/svc/serviceContext.go index 6a03956..a5c41d6 100644 --- a/job/rpc/internal/svc/serviceContext.go +++ b/job/rpc/internal/svc/serviceContext.go @@ -1,13 +1,23 @@ package svc -import "datacenter/job/rpc/internal/config" +import ( + "datacenter/job/model" + "datacenter/job/rpc/internal/config" + + "github.com/zeromicro/go-zero/core/stores/sqlx" +) type ServiceContext struct { - Config config.Config + Config config.Config + SyncTaskModel model.SyncTaskModel } func NewServiceContext(c config.Config) *ServiceContext { + // Mysql 三无依赖:仅本服务 dc_sync_task 表的 model + //(DS 客户端由 logic 包按环境变量自装配,不进 svc,避免 logic→svc 反向依赖成环) + conn := sqlx.NewMysql(c.Mysql.DataSource) return &ServiceContext{ - Config: c, + Config: c, + SyncTaskModel: model.NewSyncTaskModel(conn), } } diff --git a/metadata/model/entitymodel.go b/metadata/model/entitymodel.go index 5ac78a7..4d0e1bb 100644 --- a/metadata/model/entitymodel.go +++ b/metadata/model/entitymodel.go @@ -63,7 +63,7 @@ type ( FindOne(ctx context.Context, id, beid int64) (*Entity, error) // FindOneByFqn 按 beid + 全限定名查询 FindOneByFqn(ctx context.Context, beid int64, fqn string) (*Entity, error) - // FindList 条件分页查询(entity_type/keyword/ds_id 均可选,keyword 对 fqn/name 模糊匹配) + // FindList 条件分页查询(entity_type/keyword/ds_id 均可选,keyword 对 fqn/name/comment 模糊匹配) FindList(ctx context.Context, beid int64, entityType, keyword string, dsId, page, size int64) ([]*Entity, int64, error) // FindFqnMap 批量查询 fqn -> id 映射(采集建边用) FindFqnMap(ctx context.Context, session sqlx.Session, beid int64, fqns []string) (map[string]int64, error) @@ -164,9 +164,10 @@ func (m *defaultEntityModel) FindList(ctx context.Context, beid int64, entityTyp args = append(args, entityType) } if kw := strings.TrimSpace(keyword); kw != "" { - where += " and (fqn like ? or name like ?)" + // keyword 同时匹配 fqn/name/comment(中文场景按表注释检索,如"投票"命中 comment=投票记录表) + where += " and (fqn like ? or name like ? or comment like ?)" like := "%" + escapeLike(kw) + "%" - args = append(args, like, like) + args = append(args, like, like, like) } if dsId > 0 { where += " and ds_id = ?" diff --git a/restart.sh b/restart.sh index 5456c6c..3e2130a 100644 --- a/restart.sh +++ b/restart.sh @@ -65,6 +65,12 @@ RpcServerPlus ${getewayPath} quality RpcServerPlus ${getewayPath} metric #数据标签服务(Phase 2) RpcServerPlus ${getewayPath} tag +#调度集成服务(Phase 3) +RpcServerPlus ${getewayPath} job +#SQL IDE 服务(Phase 3) +RpcServerPlus ${getewayPath} ide +#AI 问数服务(Phase 4) +RpcServerPlus ${getewayPath} ai #问答抽奖服务 RpcServerPlus ${getewayPath} questions #Api服务 From 07c8d535ab12d52e8263250323f5d97302cc1da3 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 14:20:01 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA?= =?UTF-8?q?=EF=BC=9A=E6=95=B0=E6=8D=AE=E4=B8=AD=E5=8F=B0=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=9D=A2=E6=8E=A5=E5=8F=A3=E6=8C=82=20JWT=20?= =?UTF-8?q?=E4=BF=9D=E6=8A=A4=EF=BC=88=E5=AE=9E=E6=B5=8B=20401/200?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 10 个 .api 的 @server 增加 jwt: Auth(datasvc 拆管理组与 execute 外部入口,后者由 app_key+HMAC 签名保护) - 14 组路由重新生成挂 WithJwt;无 token 实测 401、带 token 实测 200 - 中台管理接口(删数据源/执行质量任务/打标/IDE 等)不再匿名可调 --- ai.api | 1 + catalog.api | 1 + datasource.api | 1 + datasvc.api | 7 +++++++ ide.api | 1 + internal/handler/routes.go | 27 +++++++++++++++++++++------ job.api | 1 + metadata.api | 1 + metric.api | 1 + quality.api | 1 + tag.api | 1 + 11 files changed, 37 insertions(+), 6 deletions(-) diff --git a/ai.api b/ai.api index aa73dee..0531ac3 100644 --- a/ai.api +++ b/ai.api @@ -23,6 +23,7 @@ type AskResp { } @server( + jwt: Auth group: ai ) service datacenter-api { diff --git a/catalog.api b/catalog.api index 73caa5a..d0dac76 100644 --- a/catalog.api +++ b/catalog.api @@ -55,6 +55,7 @@ type AssetListResp { } @server( + jwt: Auth group: catalog ) service datacenter-api { diff --git a/datasource.api b/datasource.api index d7c785a..5bc5412 100644 --- a/datasource.api +++ b/datasource.api @@ -67,6 +67,7 @@ type TestConnResp { } @server( + jwt: Auth group: datasource ) service datacenter-api { diff --git a/datasvc.api b/datasvc.api index 0d2439f..500254a 100644 --- a/datasvc.api +++ b/datasvc.api @@ -91,6 +91,7 @@ type ExecuteApiResp { } @server( + jwt: Auth group: datasvc ) service datacenter-api { @@ -139,6 +140,12 @@ service datacenter-api { ) @handler grantApi post /datasvc/grant (GrantApiReq) +} +//对外执行入口:不走 JWT,由 app_key+签名 鉴权 +@server( + group: datasvc +) +service datacenter-api { @doc( summary: "执行数据API(外部统一入口)" ) diff --git a/ide.api b/ide.api index a687247..49d942a 100644 --- a/ide.api +++ b/ide.api @@ -51,6 +51,7 @@ type RunScriptResp { } @server( + jwt: Auth group: ide ) service datacenter-api { diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 23064a4..6568463 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -36,6 +36,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: ai.AskHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( @@ -65,6 +66,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: catalog.CategoryListHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( @@ -140,6 +142,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: datasource.UpdateDataSourceHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( @@ -192,12 +195,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/datasvc/app/register", Handler: datasvc.RegisterAppHandler(serverCtx), }, - { - // 执行数据API(外部统一入口) - Method: http.MethodPost, - Path: "/datasvc/execute", - Handler: datasvc.ExecuteApiHandler(serverCtx), - }, { // 授权API给应用 Method: http.MethodPost, @@ -205,6 +202,18 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: datasvc.GrantApiHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), + ) + + server.AddRoutes( + []rest.Route{ + { + // 执行数据API(外部统一入口) + Method: http.MethodPost, + Path: "/datasvc/execute", + Handler: datasvc.ExecuteApiHandler(serverCtx), + }, + }, ) server.AddRoutes( @@ -240,6 +249,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: ide.SaveScriptHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( @@ -287,6 +297,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: job.TriggerTaskHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( @@ -340,6 +351,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: metadata.VersionListHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( @@ -387,6 +399,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: metric.UpdateMetricHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( @@ -434,6 +447,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: quality.QualityRuleListHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( @@ -566,6 +580,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: tag.TaggingHandler(serverCtx), }, }, + rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) server.AddRoutes( diff --git a/job.api b/job.api index e5d3948..3d43561 100644 --- a/job.api +++ b/job.api @@ -64,6 +64,7 @@ type SchedulerInfoResp { } @server( + jwt: Auth group: job ) service datacenter-api { diff --git a/metadata.api b/metadata.api index 85a0db3..6b77607 100644 --- a/metadata.api +++ b/metadata.api @@ -95,6 +95,7 @@ type CompareReq { type DiffResp { DiffJson string `json:"diff_json"` } @server( + jwt: Auth group: metadata ) service datacenter-api { diff --git a/metric.api b/metric.api index 39a789e..430b81e 100644 --- a/metric.api +++ b/metric.api @@ -62,6 +62,7 @@ type BindAtomsReq { } @server( + jwt: Auth group: metric ) service datacenter-api { diff --git a/quality.api b/quality.api index 6a8e993..a9f157a 100644 --- a/quality.api +++ b/quality.api @@ -98,6 +98,7 @@ type ResultListResp { } @server( + jwt: Auth group: quality ) service datacenter-api { diff --git a/tag.api b/tag.api index 80daa5a..505a456 100644 --- a/tag.api +++ b/tag.api @@ -97,6 +97,7 @@ type ObjectTagsResp { } @server( + jwt: Auth group: tag ) service datacenter-api { From 0d1ee75bd4000dc131e80f9cad601791b4f52bc5 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 14:37:04 +0800 Subject: [PATCH 11/12] =?UTF-8?q?AI=20=E9=97=AE=E6=95=B0=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E9=97=AD=E7=8E=AF=20+=20CI/=E9=83=A8=E7=BD=B2=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=20+=20=E7=BD=91=E5=85=B3=20AskAndRun=20+=20=E5=AD=98?= =?UTF-8?q?=E9=87=8F=E9=85=8D=E7=BD=AE=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai-rpc 新增 AskAndRun:检索→LLM 生成 SQL→经 ide 安全闸自动执行→返回数据(执行失败降级业务态,9 单测);网关 /ai/ask_and_run 路由与编排 - CI:GitHub Actions(build/vet 存量告警过滤/test -race/go mod verify) - 部署:docker-compose 一键全栈(11 服务+依赖 healthcheck+initdb 自动导表)+ 多阶段 Dockerfile + deploy/docker 11 份容器化配置 - Postman 中台集合:11 组 67 请求(自动 Bearer token + login Tests 联动) - 存量修复:etc/datacenter-api.example.yaml 的 questions.rpc 与 DataSourceRpc 连行(原文件复制即坏) --- .github/workflows/ci.yml | 56 + Dockerfile | 58 + ai.api | 26 + ai/rpc/ai.proto | 24 + ai/rpc/ai/ai.pb.go | 256 +- ai/rpc/ai/ai_grpc.pb.go | 44 +- ai/rpc/aiClient/ai.go | 16 +- ai/rpc/internal/logic/askAndRunLogic.go | 161 + ai/rpc/internal/logic/askAndRunLogic_test.go | 384 +++ ai/rpc/internal/logic/askLogic.go | 101 +- ai/rpc/internal/server/aiServer.go | 6 + ai/rpc/internal/svc/serviceContext.go | 15 +- deploy/docker/ai.yaml | 21 + deploy/docker/catalog.yaml | 15 + deploy/docker/datacenter-api.docker.yaml | 99 + deploy/docker/datasource.yaml | 17 + deploy/docker/datasvc.yaml | 22 + deploy/docker/ide.yaml | 15 + deploy/docker/job.yaml | 15 + deploy/docker/metadata.yaml | 15 + deploy/docker/metric.yaml | 15 + deploy/docker/quality.yaml | 15 + deploy/docker/tag.yaml | 15 + docker-compose.yml | 220 ++ docs/datacenter.postman_collection.json | 2842 ++++++++++++++++++ etc/datacenter-api.example.yaml | 3 +- internal/handler/ai/askandrunhandler.go | 32 + internal/handler/routes.go | 6 + internal/logic/ai/askandrunlogic.go | 78 + internal/types/types.go | 22 + 30 files changed, 4563 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 ai/rpc/internal/logic/askAndRunLogic.go create mode 100644 ai/rpc/internal/logic/askAndRunLogic_test.go create mode 100644 deploy/docker/ai.yaml create mode 100644 deploy/docker/catalog.yaml create mode 100644 deploy/docker/datacenter-api.docker.yaml create mode 100644 deploy/docker/datasource.yaml create mode 100644 deploy/docker/datasvc.yaml create mode 100644 deploy/docker/ide.yaml create mode 100644 deploy/docker/job.yaml create mode 100644 deploy/docker/metadata.yaml create mode 100644 deploy/docker/metric.yaml create mode 100644 deploy/docker/quality.yaml create mode 100644 deploy/docker/tag.yaml create mode 100644 docker-compose.yml create mode 100644 docs/datacenter.postman_collection.json create mode 100644 internal/handler/ai/askandrunhandler.go create mode 100644 internal/logic/ai/askandrunlogic.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3dd5232 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +# ============================================================ +# 数据中台 CI(Go 1.26 + go-zero) +# 触发:push 到 main / phase1/**,以及全部 Pull Request +# 步骤:checkout -> setup-go -> go mod verify -> build -> vet -> test -race +# 说明:仓库存量 24 处 pb 值传递(copylocks / lock by value)vet 告警, +# 见 issue #18;此处过滤该类历史告警后按剩余输出判断退出码, +# 新代码零告警由 review 保证。 +# ============================================================ +name: ci + +on: + push: + branches: [main, 'phase1/**'] + pull_request: + +jobs: + build-test: + runs-on: ubuntu-latest + steps: + # 检出代码 + - name: 检出代码 + uses: actions/checkout@v4 + + # 安装 Go 1.26 并开启模块缓存 + - name: 安装 Go 1.26 + uses: actions/setup-go@v5 + with: + go-version: '1.26' + cache: true + + # go.sum 完整性校验 + - name: go mod verify(go.sum 完整性) + run: go mod verify + + # 全量编译 + - name: go build + run: go build ./... + + # 静态检查:过滤存量 pb 值传递(lock by value)告警后判断退出码 + # - 存量 24 处告警见 issue #18,随 pb 重生成一并修复; + # - 过滤规则:剔除 "lock by value" 告警行与 "# 包名" 标题行, + # 其余任何输出(即新增告警)都视为失败。 + - name: go vet(过滤存量 pb 值传递告警,见 issue #18) + run: | + vet_out=$(go vet ./... 2>&1) || true + filtered=$(printf '%s\n' "$vet_out" | grep -v "lock by value" | grep -v "^#" || true) + if [ -n "$filtered" ]; then + printf '%s\n' "$filtered" + echo "::error::go vet 发现新增告警(存量 pb 值传递告警见 issue #18,不在拦截范围)" + exit 1 + fi + echo "go vet 通过(已过滤存量 pb 值传递告警,新代码零告警由 review 保证)" + + # 单元测试:开竞态检测、禁用缓存 + - name: go test(-race -count=1) + run: go test ./... -race -count=1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1a1a419 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# ============================================================ +# 数据中台一体镜像:单镜像打包 11 个服务二进制 + 容器化配置 +# 网关: datacenter-api(对外 8887) +# RPC 服务: datasource/metadata/datasvc/catalog/quality/metric/tag/job/ide/ai +# 注意:本机无 docker 构建型 CI,首次使用 `docker compose build` 验证。 +# 用法:docker compose build && docker compose up -d +# ============================================================ + +# ---------- 阶段一:builder,编译全部 11 个服务二进制 ---------- +FROM golang:1.26-alpine AS builder + +# git 用于 go mod 拉取依赖;ca-certificates 用于 https 模块下载 +RUN apk add --no-cache git ca-certificates + +WORKDIR /src + +# 先拷贝依赖清单并下载,最大化利用 docker 层缓存(源码改动不重复拉依赖) +COPY go.mod go.sum ./ +RUN go mod download + +# 再拷贝全部源码 +COPY . . + +# CGO_ENABLED=0 纯静态编译,运行层无需 glibc +ENV CGO_ENABLED=0 GOOS=linux +RUN mkdir -p /app/bin \ + && go build -o /app/bin/datacenter-api . \ + && go build -o /app/bin/datasource-server ./datasource/rpc \ + && go build -o /app/bin/metadata-server ./metadata/rpc \ + && go build -o /app/bin/datasvc-server ./datasvc/rpc \ + && go build -o /app/bin/catalog-server ./catalog/rpc \ + && go build -o /app/bin/quality-server ./quality/rpc \ + && go build -o /app/bin/metric-server ./metric/rpc \ + && go build -o /app/bin/tag-server ./tag/rpc \ + && go build -o /app/bin/job-server ./job/rpc \ + && go build -o /app/bin/ide-server ./ide/rpc \ + && go build -o /app/bin/ai-server ./ai/rpc + +# ---------- 阶段二:运行层,轻量 alpine ---------- +FROM alpine:3.20 + +# ca-certificates:访问外部 https(如 LLM 接口) +# tzdata + Asia/Shanghai:容器时区与宿主一致 +RUN apk add --no-cache ca-certificates tzdata \ + && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo "Asia/Shanghai" > /etc/timezone + +# 拷贝 11 个服务二进制 +COPY --from=builder /app/bin /app/bin +# 拷贝容器化配置(主机名均为 compose 服务名:mysql/redis/etcd) +COPY deploy/docker /app/etc +# 网关静态资源目录(/static/** 路由使用) +COPY assets /app/assets + +# 工作目录设为 /app/bin,compose 中各服务以不同 command 启动: +# ./datasource-server -f /app/etc/datasource.yaml +# ./datacenter-api -f /app/etc/datacenter-api.docker.yaml +WORKDIR /app/bin diff --git a/ai.api b/ai.api index 0531ac3..5c1bc94 100644 --- a/ai.api +++ b/ai.api @@ -22,6 +22,27 @@ type AskResp { CostMs int64 `json:"cost_ms"` } +type AskAndRunReq { + Question string `json:"question"` + Beid int64 `json:"beid,optional"` + MaxCandidates int32 `json:"max_candidates,optional"` + AutoRun bool `json:"auto_run,optional"` //生成 SQL 后经 ide 安全闸自动执行 + DatasourceId int64 `json:"datasource_id,optional"` //auto_run 时必填:执行目标库 + MaxRows int32 `json:"max_rows,optional"` //默认 20,上限 100 +} +type AskAndRunResp { + Candidates []Candidate `json:"candidates"` + LlmEnabled bool `json:"llm_enabled"` + GeneratedSql string `json:"generated_sql,optional"` + Answer string `json:"answer"` + CostMs int64 `json:"cost_ms"` + Executed bool `json:"executed"` + ColumnsJson string `json:"columns_json,optional"` + RowsJson string `json:"rows_json,optional"` + RowCount int64 `json:"row_count"` + RunError string `json:"run_error,optional"` +} + @server( jwt: Auth group: ai @@ -32,4 +53,9 @@ service datacenter-api { ) @handler ask post /ai/ask (AskReq) returns (AskResp) + @doc( + summary: "AI 问数并执行(LLM 生成 SQL 后经 ide 安全闸自动执行)" + ) + @handler askAndRun + post /ai/ask_and_run (AskAndRunReq) returns (AskAndRunResp) } diff --git a/ai/rpc/ai.proto b/ai/rpc/ai.proto index 243d422..1b1fd52 100644 --- a/ai/rpc/ai.proto +++ b/ai/rpc/ai.proto @@ -26,7 +26,31 @@ message AskReply { int64 cost_ms = 5; } +message AskAndRunReq { + string question = 1; + int64 beid = 2; + int32 max_candidates = 3; + bool auto_run = 4; //生成 SQL 后自动经 ide 安全闸执行 + int32 max_rows = 5; //执行返回行数上限,默认 20 + int64 datasource_id = 6; //执行目标数据源(AI 无法确定生成 SQL 的目标库,由调用方指定;auto_run=true 时必填) +} +message AskAndRunReply { + repeated Candidate candidates = 1; + bool llm_enabled = 2; + string generated_sql = 3; + string answer = 4; + int64 cost_ms = 5; + bool executed = 6; //是否实际执行 + string columns_json = 7; + string rows_json = 8; + int64 row_count = 9; + string run_error = 10; //执行失败原因(LLM SQL 不合法/超时等,业务态不返回 error) +} + service Ai { //AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 rpc Ask(AskReq) returns (AskReply); + //问数并执行:Ask 全流程复用,auto_run=true 时把生成 SQL 先存为临时脚本, + //再经 ide-rpc 安全闸(仅 SELECT、行数截断)真实执行并返回数据;执行失败降级写入 run_error + rpc AskAndRun(AskAndRunReq) returns (AskAndRunReply); } diff --git a/ai/rpc/ai/ai.pb.go b/ai/rpc/ai/ai.pb.go index 0c525e8..89b775a 100644 --- a/ai/rpc/ai/ai.pb.go +++ b/ai/rpc/ai/ai.pb.go @@ -234,6 +234,206 @@ func (x *AskReply) GetCostMs() int64 { return 0 } +type AskAndRunReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Question string `protobuf:"bytes,1,opt,name=question,proto3" json:"question,omitempty"` + Beid int64 `protobuf:"varint,2,opt,name=beid,proto3" json:"beid,omitempty"` + MaxCandidates int32 `protobuf:"varint,3,opt,name=max_candidates,json=maxCandidates,proto3" json:"max_candidates,omitempty"` + AutoRun bool `protobuf:"varint,4,opt,name=auto_run,json=autoRun,proto3" json:"auto_run,omitempty"` //生成 SQL 后自动经 ide 安全闸执行 + MaxRows int32 `protobuf:"varint,5,opt,name=max_rows,json=maxRows,proto3" json:"max_rows,omitempty"` //执行返回行数上限,默认 20 + DatasourceId int64 `protobuf:"varint,6,opt,name=datasource_id,json=datasourceId,proto3" json:"datasource_id,omitempty"` //执行目标数据源(AI 无法确定生成 SQL 的目标库,由调用方指定;auto_run=true 时必填) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AskAndRunReq) Reset() { + *x = AskAndRunReq{} + mi := &file_ai_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AskAndRunReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AskAndRunReq) ProtoMessage() {} + +func (x *AskAndRunReq) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AskAndRunReq.ProtoReflect.Descriptor instead. +func (*AskAndRunReq) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{3} +} + +func (x *AskAndRunReq) GetQuestion() string { + if x != nil { + return x.Question + } + return "" +} + +func (x *AskAndRunReq) GetBeid() int64 { + if x != nil { + return x.Beid + } + return 0 +} + +func (x *AskAndRunReq) GetMaxCandidates() int32 { + if x != nil { + return x.MaxCandidates + } + return 0 +} + +func (x *AskAndRunReq) GetAutoRun() bool { + if x != nil { + return x.AutoRun + } + return false +} + +func (x *AskAndRunReq) GetMaxRows() int32 { + if x != nil { + return x.MaxRows + } + return 0 +} + +func (x *AskAndRunReq) GetDatasourceId() int64 { + if x != nil { + return x.DatasourceId + } + return 0 +} + +type AskAndRunReply struct { + state protoimpl.MessageState `protogen:"open.v1"` + Candidates []*Candidate `protobuf:"bytes,1,rep,name=candidates,proto3" json:"candidates,omitempty"` + LlmEnabled bool `protobuf:"varint,2,opt,name=llm_enabled,json=llmEnabled,proto3" json:"llm_enabled,omitempty"` + GeneratedSql string `protobuf:"bytes,3,opt,name=generated_sql,json=generatedSql,proto3" json:"generated_sql,omitempty"` + Answer string `protobuf:"bytes,4,opt,name=answer,proto3" json:"answer,omitempty"` + CostMs int64 `protobuf:"varint,5,opt,name=cost_ms,json=costMs,proto3" json:"cost_ms,omitempty"` + Executed bool `protobuf:"varint,6,opt,name=executed,proto3" json:"executed,omitempty"` //是否实际执行 + ColumnsJson string `protobuf:"bytes,7,opt,name=columns_json,json=columnsJson,proto3" json:"columns_json,omitempty"` + RowsJson string `protobuf:"bytes,8,opt,name=rows_json,json=rowsJson,proto3" json:"rows_json,omitempty"` + RowCount int64 `protobuf:"varint,9,opt,name=row_count,json=rowCount,proto3" json:"row_count,omitempty"` + RunError string `protobuf:"bytes,10,opt,name=run_error,json=runError,proto3" json:"run_error,omitempty"` //执行失败原因(LLM SQL 不合法/超时等,业务态不返回 error) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AskAndRunReply) Reset() { + *x = AskAndRunReply{} + mi := &file_ai_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AskAndRunReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AskAndRunReply) ProtoMessage() {} + +func (x *AskAndRunReply) ProtoReflect() protoreflect.Message { + mi := &file_ai_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AskAndRunReply.ProtoReflect.Descriptor instead. +func (*AskAndRunReply) Descriptor() ([]byte, []int) { + return file_ai_proto_rawDescGZIP(), []int{4} +} + +func (x *AskAndRunReply) GetCandidates() []*Candidate { + if x != nil { + return x.Candidates + } + return nil +} + +func (x *AskAndRunReply) GetLlmEnabled() bool { + if x != nil { + return x.LlmEnabled + } + return false +} + +func (x *AskAndRunReply) GetGeneratedSql() string { + if x != nil { + return x.GeneratedSql + } + return "" +} + +func (x *AskAndRunReply) GetAnswer() string { + if x != nil { + return x.Answer + } + return "" +} + +func (x *AskAndRunReply) GetCostMs() int64 { + if x != nil { + return x.CostMs + } + return 0 +} + +func (x *AskAndRunReply) GetExecuted() bool { + if x != nil { + return x.Executed + } + return false +} + +func (x *AskAndRunReply) GetColumnsJson() string { + if x != nil { + return x.ColumnsJson + } + return "" +} + +func (x *AskAndRunReply) GetRowsJson() string { + if x != nil { + return x.RowsJson + } + return "" +} + +func (x *AskAndRunReply) GetRowCount() int64 { + if x != nil { + return x.RowCount + } + return 0 +} + +func (x *AskAndRunReply) GetRunError() string { + if x != nil { + return x.RunError + } + return "" +} + var File_ai_proto protoreflect.FileDescriptor const file_ai_proto_rawDesc = "" + @@ -257,10 +457,33 @@ const file_ai_proto_rawDesc = "" + "llmEnabled\x12#\n" + "\rgenerated_sql\x18\x03 \x01(\tR\fgeneratedSql\x12\x16\n" + "\x06answer\x18\x04 \x01(\tR\x06answer\x12\x17\n" + - "\acost_ms\x18\x05 \x01(\x03R\x06costMs2%\n" + + "\acost_ms\x18\x05 \x01(\x03R\x06costMs\"\xc0\x01\n" + + "\fAskAndRunReq\x12\x1a\n" + + "\bquestion\x18\x01 \x01(\tR\bquestion\x12\x12\n" + + "\x04beid\x18\x02 \x01(\x03R\x04beid\x12%\n" + + "\x0emax_candidates\x18\x03 \x01(\x05R\rmaxCandidates\x12\x19\n" + + "\bauto_run\x18\x04 \x01(\bR\aautoRun\x12\x19\n" + + "\bmax_rows\x18\x05 \x01(\x05R\amaxRows\x12#\n" + + "\rdatasource_id\x18\x06 \x01(\x03R\fdatasourceId\"\xcc\x02\n" + + "\x0eAskAndRunReply\x12-\n" + + "\n" + + "candidates\x18\x01 \x03(\v2\r.ai.CandidateR\n" + + "candidates\x12\x1f\n" + + "\vllm_enabled\x18\x02 \x01(\bR\n" + + "llmEnabled\x12#\n" + + "\rgenerated_sql\x18\x03 \x01(\tR\fgeneratedSql\x12\x16\n" + + "\x06answer\x18\x04 \x01(\tR\x06answer\x12\x17\n" + + "\acost_ms\x18\x05 \x01(\x03R\x06costMs\x12\x1a\n" + + "\bexecuted\x18\x06 \x01(\bR\bexecuted\x12!\n" + + "\fcolumns_json\x18\a \x01(\tR\vcolumnsJson\x12\x1b\n" + + "\trows_json\x18\b \x01(\tR\browsJson\x12\x1b\n" + + "\trow_count\x18\t \x01(\x03R\browCount\x12\x1b\n" + + "\trun_error\x18\n" + + " \x01(\tR\brunError2X\n" + "\x02Ai\x12\x1f\n" + "\x03Ask\x12\n" + - ".ai.AskReq\x1a\f.ai.AskReplyB\x06Z\x04./aib\x06proto3" + ".ai.AskReq\x1a\f.ai.AskReply\x121\n" + + "\tAskAndRun\x12\x10.ai.AskAndRunReq\x1a\x12.ai.AskAndRunReplyB\x06Z\x04./aib\x06proto3" var ( file_ai_proto_rawDescOnce sync.Once @@ -274,21 +497,26 @@ func file_ai_proto_rawDescGZIP() []byte { return file_ai_proto_rawDescData } -var file_ai_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ai_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_ai_proto_goTypes = []any{ - (*Candidate)(nil), // 0: ai.Candidate - (*AskReq)(nil), // 1: ai.AskReq - (*AskReply)(nil), // 2: ai.AskReply + (*Candidate)(nil), // 0: ai.Candidate + (*AskReq)(nil), // 1: ai.AskReq + (*AskReply)(nil), // 2: ai.AskReply + (*AskAndRunReq)(nil), // 3: ai.AskAndRunReq + (*AskAndRunReply)(nil), // 4: ai.AskAndRunReply } var file_ai_proto_depIdxs = []int32{ 0, // 0: ai.AskReply.candidates:type_name -> ai.Candidate - 1, // 1: ai.Ai.Ask:input_type -> ai.AskReq - 2, // 2: ai.Ai.Ask:output_type -> ai.AskReply - 2, // [2:3] is the sub-list for method output_type - 1, // [1:2] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 0, // 1: ai.AskAndRunReply.candidates:type_name -> ai.Candidate + 1, // 2: ai.Ai.Ask:input_type -> ai.AskReq + 3, // 3: ai.Ai.AskAndRun:input_type -> ai.AskAndRunReq + 2, // 4: ai.Ai.Ask:output_type -> ai.AskReply + 4, // 5: ai.Ai.AskAndRun:output_type -> ai.AskAndRunReply + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_ai_proto_init() } @@ -302,7 +530,7 @@ func file_ai_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ai_proto_rawDesc), len(file_ai_proto_rawDesc)), NumEnums: 0, - NumMessages: 3, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/ai/rpc/ai/ai_grpc.pb.go b/ai/rpc/ai/ai_grpc.pb.go index 6149108..2835df7 100644 --- a/ai/rpc/ai/ai_grpc.pb.go +++ b/ai/rpc/ai/ai_grpc.pb.go @@ -19,7 +19,8 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Ai_Ask_FullMethodName = "/ai.Ai/Ask" + Ai_Ask_FullMethodName = "/ai.Ai/Ask" + Ai_AskAndRun_FullMethodName = "/ai.Ai/AskAndRun" ) // AiClient is the client API for Ai service. @@ -28,6 +29,9 @@ const ( type AiClient interface { //AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 Ask(ctx context.Context, in *AskReq, opts ...grpc.CallOption) (*AskReply, error) + //问数并执行:Ask 全流程复用,auto_run=true 时把生成 SQL 先存为临时脚本, + //再经 ide-rpc 安全闸(仅 SELECT、行数截断)真实执行并返回数据;执行失败降级写入 run_error + AskAndRun(ctx context.Context, in *AskAndRunReq, opts ...grpc.CallOption) (*AskAndRunReply, error) } type aiClient struct { @@ -48,12 +52,25 @@ func (c *aiClient) Ask(ctx context.Context, in *AskReq, opts ...grpc.CallOption) return out, nil } +func (c *aiClient) AskAndRun(ctx context.Context, in *AskAndRunReq, opts ...grpc.CallOption) (*AskAndRunReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AskAndRunReply) + err := c.cc.Invoke(ctx, Ai_AskAndRun_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AiServer is the server API for Ai service. // All implementations must embed UnimplementedAiServer // for forward compatibility. type AiServer interface { //AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 Ask(context.Context, *AskReq) (*AskReply, error) + //问数并执行:Ask 全流程复用,auto_run=true 时把生成 SQL 先存为临时脚本, + //再经 ide-rpc 安全闸(仅 SELECT、行数截断)真实执行并返回数据;执行失败降级写入 run_error + AskAndRun(context.Context, *AskAndRunReq) (*AskAndRunReply, error) mustEmbedUnimplementedAiServer() } @@ -67,6 +84,9 @@ type UnimplementedAiServer struct{} func (UnimplementedAiServer) Ask(context.Context, *AskReq) (*AskReply, error) { return nil, status.Error(codes.Unimplemented, "method Ask not implemented") } +func (UnimplementedAiServer) AskAndRun(context.Context, *AskAndRunReq) (*AskAndRunReply, error) { + return nil, status.Error(codes.Unimplemented, "method AskAndRun not implemented") +} func (UnimplementedAiServer) mustEmbedUnimplementedAiServer() {} func (UnimplementedAiServer) testEmbeddedByValue() {} @@ -106,6 +126,24 @@ func _Ai_Ask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) return interceptor(ctx, in, info, handler) } +func _Ai_AskAndRun_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AskAndRunReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AiServer).AskAndRun(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Ai_AskAndRun_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AiServer).AskAndRun(ctx, req.(*AskAndRunReq)) + } + return interceptor(ctx, in, info, handler) +} + // Ai_ServiceDesc is the grpc.ServiceDesc for Ai service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -117,6 +155,10 @@ var Ai_ServiceDesc = grpc.ServiceDesc{ MethodName: "Ask", Handler: _Ai_Ask_Handler, }, + { + MethodName: "AskAndRun", + Handler: _Ai_AskAndRun_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "ai.proto", diff --git a/ai/rpc/aiClient/ai.go b/ai/rpc/aiClient/ai.go index 2201918..03ef13f 100644 --- a/ai/rpc/aiClient/ai.go +++ b/ai/rpc/aiClient/ai.go @@ -14,13 +14,17 @@ import ( ) type ( - AskReply = ai.AskReply - AskReq = ai.AskReq - Candidate = ai.Candidate + AskAndRunReply = ai.AskAndRunReply + AskAndRunReq = ai.AskAndRunReq + AskReply = ai.AskReply + AskReq = ai.AskReq + Candidate = ai.Candidate Ai interface { // AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 Ask(ctx context.Context, in *AskReq, opts ...grpc.CallOption) (*AskReply, error) + // 问数并执行:Ask 全流程复用,auto_run=true 时把生成 SQL 先存为临时脚本, + AskAndRun(ctx context.Context, in *AskAndRunReq, opts ...grpc.CallOption) (*AskAndRunReply, error) } defaultAi struct { @@ -39,3 +43,9 @@ func (m *defaultAi) Ask(ctx context.Context, in *AskReq, opts ...grpc.CallOption client := ai.NewAiClient(m.cli.Conn()) return client.Ask(ctx, in, opts...) } + +// 问数并执行:Ask 全流程复用,auto_run=true 时把生成 SQL 先存为临时脚本, +func (m *defaultAi) AskAndRun(ctx context.Context, in *AskAndRunReq, opts ...grpc.CallOption) (*AskAndRunReply, error) { + client := ai.NewAiClient(m.cli.Conn()) + return client.AskAndRun(ctx, in, opts...) +} diff --git a/ai/rpc/internal/logic/askAndRunLogic.go b/ai/rpc/internal/logic/askAndRunLogic.go new file mode 100644 index 0000000..f399ff8 --- /dev/null +++ b/ai/rpc/internal/logic/askAndRunLogic.go @@ -0,0 +1,161 @@ +package logic + +import ( + "context" + "fmt" + "strings" + "time" + + "datacenter/ai/rpc/ai" + "datacenter/ai/rpc/internal/svc" + "datacenter/ide/rpc/ideClient" + + "github.com/zeromicro/go-zero/core/logx" +) + +const ( + defaultRunRows = 20 //执行返回行数缺省上限(与 proto 注释一致) + maxRunRowsLimit = 100 //行数入参上限防御(ide 侧另有仅 SELECT 与保护性截断) + runScriptPrefix = "ai-gen-" //AI 生成 SQL 落为临时脚本的名称前缀 +) + +type AskAndRunLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewAskAndRunLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AskAndRunLogic { + return &AskAndRunLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// 问数并执行:Ask 全流程复用,auto_run=true 时把生成 SQL 先存为临时脚本, +// 再经 ide-rpc 安全闸(仅 SELECT、行数截断)真实执行并返回数据; +// 执行失败(SQL 不合法/数据源不可用等)是常见业务态:降级写入 run_error,不返回 error +func (l *AskAndRunLogic) AskAndRun(in *ai.AskAndRunReq) (*ai.AskAndRunReply, error) { + start := time.Now() + + //1. 入参校验:question 非空;auto_run=true 时 datasource_id 必填(AI 无法确定生成 SQL 的目标库), + // beid 必填(ide 脚本存取与执行均按租户隔离) + question := strings.TrimSpace(in.Question) + if question == "" { + return nil, fmt.Errorf("question 不能为空") + } + if in.AutoRun { + if in.DatasourceId <= 0 { + return nil, fmt.Errorf("auto_run=true 时 datasource_id 必填:AI 生成的 SQL 目标库未知,需调用方指定执行数据源") + } + if in.Beid <= 0 { + return nil, fmt.Errorf("auto_run=true 时 beid 必填:脚本保存与执行均按租户隔离") + } + } + + //2. 复用 Ask 主流程(检索→候选打分→可选 LLM 生成 SQL) + ask := NewAskLogic(l.ctx, l.svcCtx) + core := ask.askCore(question, in.Beid, normalizeMaxCandidates(in.MaxCandidates)) + + reply := &ai.AskAndRunReply{ + Candidates: core.candidates, + LlmEnabled: core.llmEnabled, + GeneratedSql: core.generatedSql, + Answer: core.answer, + } + + //3. 自动执行(auto_run=true 才走):LLM 生成成功且有 SQL 才真实执行,否则说明原因写 run_error + if in.AutoRun { + switch { + case !core.llmEnabled: + reply.RunError = "未启用 LLM 或 LLM 调用失败已降级,无生成 SQL 可执行" + case strings.TrimSpace(core.generatedSql) == "": + reply.RunError = "LLM 未生成 SQL(回复中未提取到 ```sql 代码块),无可执行内容" + default: + run, err := runGeneratedSqlViaIde(l.ctx, l.svcCtx.IdeRpc, + core.generatedSql, in.Beid, in.DatasourceId, normalizeRunRows(in.MaxRows)) + if err != nil { + //降级:run_error 带原因,不返回 error + reply.RunError = err.Error() + l.Errorf("AI 生成 SQL 自动执行失败已降级: question=%s err=%v", question, err) + } else { + reply.Executed = true + reply.ColumnsJson = run.ColumnsJson + reply.RowsJson = run.RowsJson + reply.RowCount = run.RowCount + } + } + //执行摘要追加到 answer 尾部:不改审计表,dc_ask_log.answer 随之记录 executed 与 row_count + reply.Answer = appendRunSummary(reply.Answer, reply.Executed, reply.RunError, in.DatasourceId, reply.RowCount) + } + + //4. 总耗时(含执行)+ 审计:回填最终 answer/耗时后复用 Ask 的审计落库 + reply.CostMs = time.Since(start).Milliseconds() + core.answer = reply.Answer + core.costMs = reply.CostMs + ask.writeAskAudit(core, in.Beid) + + return reply, nil +} + +// runGeneratedSqlViaIde AI 生成 SQL 的执行链路(独立函数便于单测替换 ideClient.Ide 桩): +// ide 的 RunScript 只认已保存脚本 id,而 AI 生成的 SQL 是临时语句,故先 SaveScript 落为临时脚本 +// (名称 ai-gen-<时间戳>,datasource_id 取调用方指定值),再经 ide 安全闸真实执行 +func runGeneratedSqlViaIde(ctx context.Context, ideRpc ideClient.Ide, sql string, + beid, datasourceId int64, maxRows int32) (*ideClient.RunScriptReply, error) { + if ideRpc == nil { + return nil, fmt.Errorf("ide rpc 客户端未装配") + } + //1. 存为临时脚本:SQL 原样保存,合法性由 ide 执行前的安全闸把关 + saveReply, err := ideRpc.SaveScript(ctx, &ideClient.SaveScriptReq{ + Name: runScriptPrefix + time.Now().Format("20060102150405"), + SqlText: sql, + DatasourceId: datasourceId, + Beid: beid, + }) + if err != nil { + return nil, fmt.Errorf("保存 AI 生成脚本失败: %w", err) + } + if saveReply.GetScript() == nil || saveReply.GetScript().GetId() <= 0 { + return nil, fmt.Errorf("保存 AI 生成脚本失败: 未返回脚本 id") + } + //2. 经 ide 安全闸执行(仅放行单条 SELECT,行数保护截断) + runReply, err := ideRpc.RunScript(ctx, &ideClient.RunScriptReq{ + Id: saveReply.GetScript().GetId(), + Beid: beid, + MaxRows: maxRows, + }) + if err != nil { + return nil, fmt.Errorf("ide 执行失败(安全闸仅放行单条 SELECT): %w", err) + } + return runReply, nil +} + +// appendRunSummary 回答尾部附加执行摘要(纯函数便于单测): +// 成功摘要含数据源与行数;未执行/失败摘要带 run_error 原因与排查提示 +func appendRunSummary(answer string, executed bool, runError string, datasourceId, rowCount int64) string { + var b strings.Builder + b.WriteString(answer) + if executed { + fmt.Fprintf(&b, "\n\n[自动执行] 生成 SQL 已在数据源 %d 经安全闸执行,返回 %d 行(完整结果见 columns_json/rows_json)。", + datasourceId, rowCount) + return b.String() + } + if runError != "" { + fmt.Fprintf(&b, "\n\n[自动执行未成功] 原因: %s。请检查生成 SQL 是否为单条 SELECT、目标数据源 %d 配置是否可用。", + runError, datasourceId) + } + return b.String() +} + +// normalizeRunRows 执行行数归一化(纯函数):<=0 取默认 20,超上限截断(ide 侧另有保护性截断) +func normalizeRunRows(n int32) int32 { + if n <= 0 { + return defaultRunRows + } + if n > maxRunRowsLimit { + return maxRunRowsLimit + } + return n +} diff --git a/ai/rpc/internal/logic/askAndRunLogic_test.go b/ai/rpc/internal/logic/askAndRunLogic_test.go new file mode 100644 index 0000000..4164440 --- /dev/null +++ b/ai/rpc/internal/logic/askAndRunLogic_test.go @@ -0,0 +1,384 @@ +package logic + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "datacenter/ai/model" + "datacenter/ai/rpc/ai" + "datacenter/ai/rpc/internal/config" + "datacenter/ai/rpc/internal/svc" + "datacenter/ide/rpc/ideClient" + "datacenter/metadata/rpc/metadataClient" + "datacenter/metric/rpc/metricsvc" + + "google.golang.org/grpc" +) + +// ---------- 测试桩 ---------- + +// fakeIde ideClient.Ide 测试桩:内嵌接口兜底未触达方法,SaveScript/RunScript 按预置返回并记录入参 +type fakeIde struct { + ideClient.Ide + + saveErr error //SaveScript 预置错误(nil=成功) + runErr error //RunScript 预置错误(nil=成功) + runReply *ideClient.RunScriptReply //RunScript 预置成功响应(nil=默认一行结果) + + savedReq *ideClient.SaveScriptReq //记录 SaveScript 入参 + runReq *ideClient.RunScriptReq //记录 RunScript 入参 + saveCalls int + runCalls int +} + +func (f *fakeIde) SaveScript(ctx context.Context, in *ideClient.SaveScriptReq, opts ...grpc.CallOption) (*ideClient.ScriptReply, error) { + f.saveCalls++ + f.savedReq = in + if f.saveErr != nil { + return nil, f.saveErr + } + return &ideClient.ScriptReply{Script: &ideClient.Script{Id: 101, Name: in.Name}}, nil +} + +func (f *fakeIde) RunScript(ctx context.Context, in *ideClient.RunScriptReq, opts ...grpc.CallOption) (*ideClient.RunScriptReply, error) { + f.runCalls++ + f.runReq = in + if f.runErr != nil { + return nil, f.runErr + } + if f.runReply != nil { + return f.runReply, nil + } + return &ideClient.RunScriptReply{ColumnsJson: `["cnt"]`, RowsJson: `[{"cnt":42}]`, RowCount: 1}, nil +} + +// fakeMetadata 元数据 rpc 测试桩:ListEntities 固定返回一张订单表实体 +type fakeMetadata struct { + metadataClient.Metadata +} + +func (f *fakeMetadata) ListEntities(ctx context.Context, in *metadataClient.EntityListReq, opts ...grpc.CallOption) (*metadataClient.EntityListReply, error) { + return &metadataClient.EntityListReply{List: []*metadataClient.MetaEntity{ + {Id: 1, EntityType: "table", Name: "订单表", Fqn: "mysql://mall.orders", Comment: "订单主表"}, + }}, nil +} + +// fakeMetricSvc 指标 rpc 测试桩:ListMetrics 固定返回一个 GMV 指标 +type fakeMetricSvc struct { + metricsvc.MetricSvc +} + +func (f *fakeMetricSvc) ListMetrics(ctx context.Context, in *metricsvc.MetricListReq, opts ...grpc.CallOption) (*metricsvc.MetricListReply, error) { + return &metricsvc.MetricListReply{List: []*metricsvc.Metric{ + {Id: 2, NameEn: "gmv", NameCn: "成交额", AggType: "sum", BizField: "amount", Comment: "成交金额口径"}, + }}, nil +} + +// fakeAskLogModel 审计 model 测试桩:记录落库的审计行 +type fakeAskLogModel struct { + model.AskLogModel + logs []*model.AskLog +} + +func (f *fakeAskLogModel) Insert(ctx context.Context, data *model.AskLog) (int64, error) { + f.logs = append(f.logs, data) + return int64(len(f.logs)), nil +} + +// stubLLMTransport LLM HTTP 桩:任意请求返回固定 content 的 OpenAI 兼容响应 +type stubLLMTransport struct { + content string +} + +func (s *stubLLMTransport) RoundTrip(req *http.Request) (*http.Response, error) { + raw, err := json.Marshal(s.content) + if err != nil { + return nil, err + } + body := fmt.Sprintf(`{"choices":[{"message":{"content":%s}}]}`, raw) + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(body)), + Header: http.Header{}, + }, nil +} + +const llmSqlContent = "按订单表统计如下:\n```sql\nSELECT count(*) AS cnt FROM mall.orders\n```" + +// newAskAndRunSvcCtx 构造带全套桩的 ServiceContext(LLM 可关,ide 桩由调用方注入) +func newAskAndRunSvcCtx(llmEnabled bool, ide ideClient.Ide) (*svc.ServiceContext, *fakeAskLogModel) { + var cfg config.Config + cfg.Llm.Enabled = llmEnabled + cfg.Llm.BaseUrl = "http://llm.fake" + cfg.Llm.ApiKey = "test-key" + cfg.Llm.Model = "gpt-4o-mini" + audit := &fakeAskLogModel{} + return &svc.ServiceContext{ + Config: cfg, + AskLogModel: audit, + MetadataRpc: &fakeMetadata{}, + MetricRpc: &fakeMetricSvc{}, + HttpClient: &http.Client{Transport: &stubLLMTransport{content: llmSqlContent}}, + IdeRpc: ide, + }, audit +} + +// ---------- 入参校验 ---------- + +// 测试入参校验:question 为空、auto_run 缺 datasource_id/beid 均拒绝; +// 校验先于任何下游依赖,svcCtx 空结构体即可 +func TestAskAndRunValidation(t *testing.T) { + l := NewAskAndRunLogic(context.Background(), &svc.ServiceContext{}) + cases := []struct { + name string + in *ai.AskAndRunReq + wantErr string + }{ + { + name: "question 为空", + in: &ai.AskAndRunReq{Question: " ", AutoRun: true, DatasourceId: 1, Beid: 1}, + wantErr: "question", + }, + { + name: "auto_run 缺 datasource_id", + in: &ai.AskAndRunReq{Question: "订单表有多少数据", AutoRun: true, DatasourceId: 0, Beid: 1}, + wantErr: "datasource_id", + }, + { + name: "auto_run 缺 beid", + in: &ai.AskAndRunReq{Question: "订单表有多少数据", AutoRun: true, DatasourceId: 3, Beid: 0}, + wantErr: "beid", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + _, err := l.AskAndRun(c.in) + if err == nil || !strings.Contains(err.Error(), c.wantErr) { + t.Fatalf("AskAndRun(%+v) err = %v, 期望包含 %q", c.in, err, c.wantErr) + } + }) + } +} + +// ---------- 纯函数 ---------- + +// 测试执行行数归一化:<=0 取默认 20,超上限截断 +func TestNormalizeRunRows(t *testing.T) { + cases := []struct { + in int32 + want int32 + }{ + {in: 0, want: 20}, + {in: -5, want: 20}, + {in: 7, want: 7}, + {in: 100000, want: 100}, + } + for _, c := range cases { + if got := normalizeRunRows(c.in); got != c.want { + t.Errorf("normalizeRunRows(%d) = %d, 期望 %d", c.in, got, c.want) + } + } +} + +// 测试执行摘要追加:成功/失败/未开启三种形态 +func TestAppendRunSummary(t *testing.T) { + //成功:摘要含数据源与行数 + got := appendRunSummary("答案", true, "", 3, 20) + if !strings.Contains(got, "答案") || !strings.Contains(got, "[自动执行]") || + !strings.Contains(got, "数据源 3") || !strings.Contains(got, "20 行") { + t.Errorf("appendRunSummary 成功态 = %q, 缺少预期片段", got) + } + //失败:摘要带原因 + got = appendRunSummary("答案", false, "SQL 不合法", 3, 0) + if !strings.Contains(got, "[自动执行未成功]") || !strings.Contains(got, "SQL 不合法") { + t.Errorf("appendRunSummary 失败态 = %q, 缺少预期片段", got) + } + //未开启自动执行:原样返回 + got = appendRunSummary("答案", false, "", 0, 0) + if got != "答案" { + t.Errorf("appendRunSummary 未执行态 = %q, 期望原样返回", got) + } +} + +// ---------- ide 执行链路 ---------- + +// 测试执行链路成功:先存脚本(带 datasource_id/beid、名称前缀 ai-gen-)再执行(带脚本 id 与行数上限) +func TestRunGeneratedSqlViaIdeSuccess(t *testing.T) { + fake := &fakeIde{} + run, err := runGeneratedSqlViaIde(context.Background(), fake, + "SELECT count(*) FROM mall.orders", 7, 3, 25) + if err != nil { + t.Fatalf("runGeneratedSqlViaIde err = %v, 期望成功", err) + } + if run == nil || run.RowCount != 1 { + t.Fatalf("run = %+v, 期望返回结果集", run) + } + //存脚本入参:临时脚本名前缀 + 调用方指定的数据源 + if fake.savedReq == nil || !strings.HasPrefix(fake.savedReq.Name, "ai-gen-") { + t.Errorf("SaveScript 入参 = %+v, 名称应带 ai-gen- 前缀", fake.savedReq) + } + if fake.savedReq.SqlText != "SELECT count(*) FROM mall.orders" || fake.savedReq.DatasourceId != 3 || fake.savedReq.Beid != 7 { + t.Errorf("SaveScript 入参 = %+v, SQL/数据源/租户不符", fake.savedReq) + } + //执行入参:脚本 id 取自保存回包,行数上限原样透传 + if fake.runReq == nil || fake.runReq.Id != 101 || fake.runReq.Beid != 7 || fake.runReq.MaxRows != 25 { + t.Errorf("RunScript 入参 = %+v, 脚本 id/租户/行数不符", fake.runReq) + } +} + +// 测试执行链路降级:存脚本失败与执行失败均带出原因 +func TestRunGeneratedSqlViaIdeDegraded(t *testing.T) { + //存脚本失败 + fake := &fakeIde{saveErr: errors.New("etcd 无 ide 节点")} + if _, err := runGeneratedSqlViaIde(context.Background(), fake, "select 1", 1, 2, 20); err == nil || + !strings.Contains(err.Error(), "保存 AI 生成脚本失败") { + t.Fatalf("存脚本失败 err = %v, 期望带保存失败原因", err) + } + //执行失败(如安全闸拒绝非 SELECT) + fake = &fakeIde{runErr: errors.New("SQL 不合法: 仅放行单条 SELECT")} + _, err := runGeneratedSqlViaIde(context.Background(), fake, "delete from t", 1, 2, 20) + if err == nil || !strings.Contains(err.Error(), "ide 执行失败") || !strings.Contains(err.Error(), "SELECT") { + t.Fatalf("执行失败 err = %v, 期望带执行失败原因", err) + } + //未装配客户端 + if _, err := runGeneratedSqlViaIde(context.Background(), nil, "select 1", 1, 2, 20); err == nil { + t.Fatal("nil 客户端应返回错误") + } +} + +// ---------- AskAndRun 全流程 ---------- + +// 全链路(LLM/元数据/指标/ide/审计全桩):auto_run=true 时生成 SQL 真实执行并返回数据 +func TestAskAndRunFullFlow(t *testing.T) { + fake := &fakeIde{} + svcCtx, audit := newAskAndRunSvcCtx(true, fake) + l := NewAskAndRunLogic(context.Background(), svcCtx) + + reply, err := l.AskAndRun(&ai.AskAndRunReq{ + Question: "订单表有多少数据", + Beid: 7, + AutoRun: true, + MaxRows: 0, //缺省走默认 20 + DatasourceId: 3, + MaxCandidates: 5, + }) + if err != nil { + t.Fatalf("AskAndRun err = %v, 期望成功", err) + } + //生成侧:LLM 命中并提取到 SQL + if !reply.LlmEnabled || !strings.Contains(reply.GeneratedSql, "SELECT count(*)") { + t.Errorf("LlmEnabled=%v GeneratedSql=%q, 期望生成 SELECT", reply.LlmEnabled, reply.GeneratedSql) + } + if len(reply.Candidates) == 0 { + t.Error("候选为空, 期望元数据/指标桩命中") + } + //执行侧:executed=true,结果集透传,行数上限取默认 20 + if !reply.Executed || reply.RunError != "" { + t.Errorf("Executed=%v RunError=%q, 期望执行成功", reply.Executed, reply.RunError) + } + if reply.ColumnsJson != `["cnt"]` || reply.RowsJson != `[{"cnt":42}]` || reply.RowCount != 1 { + t.Errorf("执行结果 = %s/%s/%d, 与 ide 桩不符", reply.ColumnsJson, reply.RowsJson, reply.RowCount) + } + if fake.runReq == nil || fake.runReq.MaxRows != 20 { + t.Errorf("RunScript MaxRows = %+v, 期望缺省 20", fake.runReq) + } + //回答尾部带执行摘要;审计落库同文(不改表,执行信息随 answer 记录) + if !strings.Contains(reply.Answer, "[自动执行]") || !strings.Contains(reply.Answer, "数据源 3") { + t.Errorf("answer = %q, 缺少执行摘要", reply.Answer) + } + if len(audit.logs) != 1 || !strings.Contains(audit.logs[0].Answer, "[自动执行]") || + audit.logs[0].GeneratedSql != reply.GeneratedSql || audit.logs[0].Beid != 7 { + t.Errorf("审计落库 = %+v, 期望含执行摘要且与回复一致", audit.logs) + } +} + +// 降级路径:ide 执行失败不返回 error,run_error 带原因,回答附未成功摘要 +func TestAskAndRunIdeErrorDegraded(t *testing.T) { + fake := &fakeIde{runErr: errors.New("SQL 不合法: 仅放行单条 SELECT")} + svcCtx, audit := newAskAndRunSvcCtx(true, fake) + l := NewAskAndRunLogic(context.Background(), svcCtx) + + reply, err := l.AskAndRun(&ai.AskAndRunReq{ + Question: "订单表有多少数据", + Beid: 7, + AutoRun: true, + DatasourceId: 3, + }) + if err != nil { + t.Fatalf("执行失败应降级不返回 error, 实际 err = %v", err) + } + if reply.Executed { + t.Error("Executed=true, 期望未执行") + } + if !strings.Contains(reply.RunError, "仅放行单条 SELECT") { + t.Errorf("RunError = %q, 期望带安全闸拒绝原因", reply.RunError) + } + if !strings.Contains(reply.Answer, "[自动执行未成功]") { + t.Errorf("answer = %q, 缺少未成功摘要", reply.Answer) + } + if len(audit.logs) != 1 || !strings.Contains(audit.logs[0].Answer, "[自动执行未成功]") { + t.Errorf("审计落库 = %+v, 期望记录未成功摘要", audit.logs) + } +} + +// 未启用 LLM 时 auto_run 不触发执行,run_error 说明原因 +func TestAskAndRunNoLlmNoRun(t *testing.T) { + fake := &fakeIde{} + svcCtx, audit := newAskAndRunSvcCtx(false, fake) //Llm.Enabled=false 零值配置 + l := NewAskAndRunLogic(context.Background(), svcCtx) + + reply, err := l.AskAndRun(&ai.AskAndRunReq{ + Question: "订单表有多少数据", + Beid: 7, + AutoRun: true, + DatasourceId: 3, + }) + if err != nil { + t.Fatalf("AskAndRun err = %v, 期望成功", err) + } + if reply.LlmEnabled || reply.Executed { + t.Errorf("LlmEnabled=%v Executed=%v, 期望均 false", reply.LlmEnabled, reply.Executed) + } + if !strings.Contains(reply.RunError, "LLM") { + t.Errorf("RunError = %q, 期望说明无生成 SQL 可执行", reply.RunError) + } + if fake.saveCalls != 0 || fake.runCalls != 0 { + t.Errorf("ide 调用 save=%d run=%d, 期望均未触达", fake.saveCalls, fake.runCalls) + } + if !strings.Contains(reply.Answer, "[自动执行未成功]") { + t.Errorf("answer = %q, 缺少未成功摘要", reply.Answer) + } + if len(audit.logs) != 1 { + t.Fatalf("审计落库 %d 条, 期望 1 条", len(audit.logs)) + } +} + +// auto_run=false:纯问数,不触达 ide,answer 不追加执行摘要 +func TestAskAndRunWithoutAutoRun(t *testing.T) { + fake := &fakeIde{} + svcCtx, audit := newAskAndRunSvcCtx(true, fake) + l := NewAskAndRunLogic(context.Background(), svcCtx) + + reply, err := l.AskAndRun(&ai.AskAndRunReq{Question: "订单表有多少数据", Beid: 7}) + if err != nil { + t.Fatalf("AskAndRun err = %v, 期望成功", err) + } + if reply.Executed || reply.RunError != "" { + t.Errorf("Executed=%v RunError=%q, 期望未开启执行", reply.Executed, reply.RunError) + } + if strings.Contains(reply.Answer, "[自动执行") { + t.Errorf("answer = %q, 不应带执行摘要", reply.Answer) + } + if fake.saveCalls != 0 || fake.runCalls != 0 { + t.Errorf("ide 调用 save=%d run=%d, 期望均未触达", fake.saveCalls, fake.runCalls) + } + if len(audit.logs) != 1 || strings.Contains(audit.logs[0].Answer, "[自动执行") { + t.Errorf("审计落库 = %+v, 期望无执行摘要", audit.logs) + } +} diff --git a/ai/rpc/internal/logic/askLogic.go b/ai/rpc/internal/logic/askLogic.go index 86cee33..099a64e 100644 --- a/ai/rpc/internal/logic/askLogic.go +++ b/ai/rpc/internal/logic/askLogic.go @@ -36,72 +36,111 @@ func NewAskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AskLogic { } } +// askCoreResult Ask 主流程(检索→LLM 生成)的中间产物,Ask 与 AskAndRun 共用: +// AskAndRun 在此基础上追加执行结果,并把最终 answer/耗时回填后再落审计 +type askCoreResult struct { + question string //清洗后的问句 + candidates []*ai.Candidate //语义检索候选(永不为 nil,空结果为空数组) + llmEnabled bool //LLM 是否生成成功 + generatedSql string //LLM 生成的 SQL(未启用/失败为空) + answer string //回答(含降级说明) + costMs int64 //本阶段耗时 +} + // AI 问数:语义检索(元数据+指标) → 命中候选 → (可选 LLM) 生成 SQL 与回答,全程审计 func (l *AskLogic) Ask(in *ai.AskReq) (*ai.AskReply, error) { - start := time.Now() - //1. question 非空校验 question := strings.TrimSpace(in.Question) if question == "" { return nil, fmt.Errorf("question 不能为空") } - maxCand := int(in.MaxCandidates) - if maxCand <= 0 { - maxCand = defaultMaxCandidates + //2. 复用主流程:检索 → 可选 LLM 生成 + core := l.askCore(question, in.Beid, normalizeMaxCandidates(in.MaxCandidates)) + + reply := &ai.AskReply{ + Candidates: core.candidates, + LlmEnabled: core.llmEnabled, + GeneratedSql: core.generatedSql, + Answer: core.answer, + CostMs: core.costMs, } - if maxCand > maxCandidatesLimit { - maxCand = maxCandidatesLimit + if reply.Candidates == nil { + reply.Candidates = []*ai.Candidate{} } - //2. 关键词提取(MVP 分词降级法:去停用词 → 整句 + 分词) + //3. 审计:无论成败写 dc_ask_log + l.writeAskAudit(core, in.Beid) + return reply, nil +} + +// askCore 问数主体(检索→候选打分→可选 LLM 生成 SQL),Ask 与 AskAndRun 共用; +// 检索失败降级、LLM 失败降级均为常见业务态,不返回 error +func (l *AskLogic) askCore(question string, beid int64, maxCand int) *askCoreResult { + start := time.Now() + + //1. 关键词提取(MVP 分词降级法:去停用词 → 整句 + 分词) keywords := ExtractKeywords(question) - //3. 语义检索 + 打分排序:元数据实体 + 指标合并候选,截取 top max_candidates - candidates := RankCandidates(l.searchCandidates(l.ctx, in.Beid, keywords, maxCand), keywords, maxCand) + //2. 语义检索 + 打分排序:元数据实体 + 指标合并候选,截取 top max_candidates + candidates := RankCandidates(l.searchCandidates(l.ctx, beid, keywords, maxCand), keywords, maxCand) - reply := &ai.AskReply{ - Candidates: candidates, + core := &askCoreResult{ + question: question, + candidates: candidates, } - if reply.Candidates == nil { - reply.Candidates = []*ai.Candidate{} + if core.candidates == nil { + core.candidates = []*ai.Candidate{} } - //4. 生成回答:LLM 可选路径(Config.Llm.Enabled),失败降级为纯检索,不返回 error + //3. 生成回答:LLM 可选路径(Config.Llm.Enabled),失败降级为纯检索,不返回 error if l.svcCtx.Config.Llm.Enabled { // TODO(联调): 需配置真实 LLM ApiKey 验证生成质量 client := llm.NewClient(l.svcCtx.Config.Llm.BaseUrl, l.svcCtx.Config.Llm.ApiKey, l.svcCtx.Config.Llm.Model, l.svcCtx.HttpClient) - content, err := client.ChatCompletion(l.ctx, buildAskPrompt(question, candidates)) + content, err := client.ChatCompletion(l.ctx, buildAskPrompt(question, core.candidates)) if err != nil { //降级:answer 写明降级原因 + 错误摘要,llm_enabled=false l.Errorf("LLM 调用失败已降级为纯检索: %v", err) - reply.Answer = fmt.Sprintf("%s\n\n[LLM 调用失败已降级为纯检索] 错误摘要: %v", buildSearchSummary(candidates), err) + core.answer = fmt.Sprintf("%s\n\n[LLM 调用失败已降级为纯检索] 错误摘要: %v", buildSearchSummary(candidates), err) } else { - reply.LlmEnabled = true - reply.GeneratedSql = extractSQLBlock(content) - reply.Answer = content + core.llmEnabled = true + core.generatedSql = extractSQLBlock(content) + core.answer = content } } else { - reply.Answer = buildSearchSummary(candidates) + core.answer = buildSearchSummary(candidates) } - reply.CostMs = time.Since(start).Milliseconds() + core.costMs = time.Since(start).Milliseconds() + return core +} - //5. 审计:无论成败写 dc_ask_log;审计写失败仅记日志,不阻断返回 +// writeAskAudit 落 dc_ask_log 审计(Ask 与 AskAndRun 共用):写失败仅记日志,不阻断返回; +// AskAndRun 在执行完成后回填 core.answer(尾部带执行摘要)/core.costMs 再调用,执行信息随 answer 落审计 +func (l *AskLogic) writeAskAudit(r *askCoreResult, beid int64) { askLog := &model.AskLog{ - Question: truncateRunes(question, maxQuestionRunes), - CandidatesJson: truncateBytes(marshalCandidates(candidates), maxTextBytes), - GeneratedSql: truncateBytes(reply.GeneratedSql, maxTextBytes), - Answer: truncateBytes(reply.Answer, maxTextBytes), - LlmEnabled: boolToInt64(reply.LlmEnabled), - CostMs: reply.CostMs, - Beid: in.Beid, + Question: truncateRunes(r.question, maxQuestionRunes), + CandidatesJson: truncateBytes(marshalCandidates(r.candidates), maxTextBytes), + GeneratedSql: truncateBytes(r.generatedSql, maxTextBytes), + Answer: truncateBytes(r.answer, maxTextBytes), + LlmEnabled: boolToInt64(r.llmEnabled), + CostMs: r.costMs, + Beid: beid, } if _, err := l.svcCtx.AskLogModel.Insert(l.ctx, askLog); err != nil { l.Errorf("写 dc_ask_log 审计失败(不阻断返回): %v", err) } +} - return reply, nil +// normalizeMaxCandidates 候选数归一化(Ask 与 AskAndRun 共用,纯函数):<=0 取默认,超上限截断 +func normalizeMaxCandidates(n int32) int { + if n <= 0 { + return defaultMaxCandidates + } + if int(n) > maxCandidatesLimit { + return maxCandidatesLimit + } + return int(n) } // buildSearchSummary 无 LLM(或降级)时的检索摘要回答:命中候选清单 + 引导提示 diff --git a/ai/rpc/internal/server/aiServer.go b/ai/rpc/internal/server/aiServer.go index ae8c6ff..24b2dc8 100644 --- a/ai/rpc/internal/server/aiServer.go +++ b/ai/rpc/internal/server/aiServer.go @@ -28,3 +28,9 @@ func (s *AiServer) Ask(ctx context.Context, in *ai.AskReq) (*ai.AskReply, error) l := logic.NewAskLogic(ctx, s.svcCtx) return l.Ask(in) } + +// 问数并执行:Ask 全流程复用,auto_run=true 时把生成 SQL 先存为临时脚本, +func (s *AiServer) AskAndRun(ctx context.Context, in *ai.AskAndRunReq) (*ai.AskAndRunReply, error) { + l := logic.NewAskAndRunLogic(ctx, s.svcCtx) + return l.AskAndRun(in) +} diff --git a/ai/rpc/internal/svc/serviceContext.go b/ai/rpc/internal/svc/serviceContext.go index c532262..6c9e806 100644 --- a/ai/rpc/internal/svc/serviceContext.go +++ b/ai/rpc/internal/svc/serviceContext.go @@ -6,6 +6,7 @@ import ( "datacenter/ai/model" "datacenter/ai/rpc/internal/config" "datacenter/ai/rpc/internal/llm" + "datacenter/ide/rpc/ideClient" "datacenter/metadata/rpc/metadataClient" "datacenter/metric/rpc/metricsvc" @@ -18,15 +19,17 @@ import ( const ( metadataEtcdKey = "metadata.rpc" //元数据 rpc metricEtcdKey = "metric.rpc" //指标 rpc + ideEtcdKey = "ide.rpc" //ide rpc(AI 生成 SQL 经其安全闸执行) ) type ServiceContext struct { - Config config.Config + Config config.Config //服务配置 Conn sqlx.SqlConn //中台主库连接(dc_ask_log 审计表) AskLogModel model.AskLogModel //问数审计 model MetadataRpc metadataClient.Metadata //元数据 rpc(语义检索实体) MetricRpc metricsvc.MetricSvc //指标 rpc(语义检索指标) HttpClient *http.Client //LLM HTTP 客户端(30s 超时,llm 包内另有 ctx 超时兜底) + IdeRpc ideClient.Ide //ide rpc(AskAndRun:AI 生成 SQL 先存脚本再经安全闸真实执行) } func NewServiceContext(c config.Config) *ServiceContext { @@ -47,6 +50,15 @@ func NewServiceContext(c config.Config) *ServiceContext { }, NonBlock: true, })) + //ide rpc:AskAndRun 自动执行链路依赖;NonBlock 保证 ide 未就绪时不阻塞本服务启动, + //ide 不可用只影响自动执行(run_error 降级),不影响 Ask 检索/生成主流程 + ideRpc := ideClient.NewIde(zrpc.MustNewClient(zrpc.RpcClientConf{ + Etcd: discov.EtcdConf{ + Hosts: c.Etcd.Hosts, + Key: ideEtcdKey, + }, + NonBlock: true, + })) return &ServiceContext{ Config: c, Conn: conn, @@ -54,5 +66,6 @@ func NewServiceContext(c config.Config) *ServiceContext { MetadataRpc: mdRpc, MetricRpc: metricRpc, HttpClient: &http.Client{Timeout: llm.RequestTimeout}, + IdeRpc: ideRpc, } } diff --git a/deploy/docker/ai.yaml b/deploy/docker/ai.yaml new file mode 100644 index 0000000..434108e --- /dev/null +++ b/deploy/docker/ai.yaml @@ -0,0 +1,21 @@ +# ============================================================ +# AI 问数服务容器化配置(docker compose 专用) +# 与 ai/rpc/etc/ai.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# LLM 默认关闭(语义检索可用);如需生成 SQL,改 Enabled 为 true 并填 ApiKey +# ============================================================ +Name: ai.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8094 +Etcd: + Hosts: + - etcd:2379 + Key: ai.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +Llm: + Enabled: false + BaseUrl: https://api.openai.com/v1 + ApiKey: sk-xxxx + Model: gpt-4o-mini diff --git a/deploy/docker/catalog.yaml b/deploy/docker/catalog.yaml new file mode 100644 index 0000000..66c3205 --- /dev/null +++ b/deploy/docker/catalog.yaml @@ -0,0 +1,15 @@ +# ============================================================ +# 资产目录服务容器化配置(docker compose 专用,镜像内由网关同镜发起) +# 与 catalog/rpc/etc/catalog.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# ============================================================ +Name: catalog.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8088 +Etcd: + Hosts: + - etcd:2379 + Key: catalog.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/deploy/docker/datacenter-api.docker.yaml b/deploy/docker/datacenter-api.docker.yaml new file mode 100644 index 0000000..646eaba --- /dev/null +++ b/deploy/docker/datacenter-api.docker.yaml @@ -0,0 +1,99 @@ +# ============================================================ +# 数据中台网关(API)容器化配置(docker compose 专用) +# 与 etc/datacenter-api.example.yaml 对齐,主机名改为 compose 服务名: +# redis -> redis 服务, etcd -> etcd 服务(容器网络内互访) +# 命名说明:仓库 .gitignore 全局忽略 datacenter-api.yaml(无路径模式), +# 为保证该协作资产可入库,文件名加 .docker 后缀区分。 +# 注意:UserRpc/CommonRpc/VotesRpc/SearchRpc/QuestionsRpc 五个遗留 RPC +# 不在 compose 编排内(go-zero etcd 发现为懒加载,网关可正常启动), +# 如需 /user/login 等接口请按 restart.sh 另起对应服务。 +# ============================================================ +Name: datacenter-api +Log: + Mode: console +Host: 0.0.0.0 +Port: 8887 +Auth: + AccessSecret: ad87xxxxxxx5d54b7d + AccessExpire: 86400 +CacheRedis: + - Host: redis:6379 + Pass: admin + Type: node +UserRpc: + Etcd: + Hosts: + - etcd:2379 + Key: user.rpc +CommonRpc: + Etcd: + Hosts: + - etcd:2379 + Key: common.rpc +VotesRpc: + Etcd: + Hosts: + - etcd:2379 + Key: votes.rpc +SearchRpc: + Etcd: + Hosts: + - etcd:2379 + Key: search.rpc +QuestionsRpc: + Etcd: + Hosts: + - etcd:2379 + Key: questions.rpc +# ---------- Phase 1 数据中台服务 ---------- +DataSourceRpc: + Etcd: + Hosts: + - etcd:2379 + Key: datasource.rpc +MetadataRpc: + Etcd: + Hosts: + - etcd:2379 + Key: metadata.rpc +DataSvcRpc: + Etcd: + Hosts: + - etcd:2379 + Key: datasvc.rpc +CatalogRpc: + Etcd: + Hosts: + - etcd:2379 + Key: catalog.rpc +QualityRpc: + Etcd: + Hosts: + - etcd:2379 + Key: quality.rpc +MetricRpc: + Etcd: + Hosts: + - etcd:2379 + Key: metric.rpc +TagRpc: + Etcd: + Hosts: + - etcd:2379 + Key: tag.rpc +# ---------- Phase 3/4 数据中台服务 ---------- +JobRpc: + Etcd: + Hosts: + - etcd:2379 + Key: job.rpc +IdeRpc: + Etcd: + Hosts: + - etcd:2379 + Key: ide.rpc +AiRpc: + Etcd: + Hosts: + - etcd:2379 + Key: ai.rpc diff --git a/deploy/docker/datasource.yaml b/deploy/docker/datasource.yaml new file mode 100644 index 0000000..309eab8 --- /dev/null +++ b/deploy/docker/datasource.yaml @@ -0,0 +1,17 @@ +# ============================================================ +# 数据源服务容器化配置(docker compose 专用) +# 与 datasource/rpc/etc/datasource.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# ============================================================ +Name: datasource.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8085 +Etcd: + Hosts: + - etcd:2379 + Key: datasource.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +# AES-GCM 密钥(32 字节),数据源连接口令加密存储 +EncryptKey: 0123456789abcdef0123456789abcdef diff --git a/deploy/docker/datasvc.yaml b/deploy/docker/datasvc.yaml new file mode 100644 index 0000000..f996ec6 --- /dev/null +++ b/deploy/docker/datasvc.yaml @@ -0,0 +1,22 @@ +# ============================================================ +# 数据服务(API 网关出 API)容器化配置(docker compose 专用) +# 与 datasvc/rpc/etc/datasvc.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / redis / etcd(容器网络内服务名互访) +# ============================================================ +Name: datasvc.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8087 +Etcd: + Hosts: + - etcd:2379 + Key: datasvc.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai +# API 结果缓存(ExecuteApi 使用) +CacheRedis: + - Host: redis:6379 + Pass: admin + Type: node +# AES-GCM 密钥(32 字节),app_secret 加密存储 +EncryptKey: 0123456789abcdef0123456789abcdef diff --git a/deploy/docker/ide.yaml b/deploy/docker/ide.yaml new file mode 100644 index 0000000..d9f4e9b --- /dev/null +++ b/deploy/docker/ide.yaml @@ -0,0 +1,15 @@ +# ============================================================ +# SQL IDE 服务容器化配置(docker compose 专用,镜像内由网关同镜发起) +# 与 ide/rpc/etc/ide.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# ============================================================ +Name: ide.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8093 +Etcd: + Hosts: + - etcd:2379 + Key: ide.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/deploy/docker/job.yaml b/deploy/docker/job.yaml new file mode 100644 index 0000000..738c5cd --- /dev/null +++ b/deploy/docker/job.yaml @@ -0,0 +1,15 @@ +# ============================================================ +# 调度集成服务容器化配置(docker compose 专用,镜像内由网关同镜发起) +# 与 job/rpc/etc/job.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# ============================================================ +Name: job.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8092 +Etcd: + Hosts: + - etcd:2379 + Key: job.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/deploy/docker/metadata.yaml b/deploy/docker/metadata.yaml new file mode 100644 index 0000000..83020d3 --- /dev/null +++ b/deploy/docker/metadata.yaml @@ -0,0 +1,15 @@ +# ============================================================ +# 元数据服务容器化配置(docker compose 专用,镜像内由网关同镜发起) +# 与 metadata/rpc/etc/metadata.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# ============================================================ +Name: metadata.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8086 +Etcd: + Hosts: + - etcd:2379 + Key: metadata.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/deploy/docker/metric.yaml b/deploy/docker/metric.yaml new file mode 100644 index 0000000..219126f --- /dev/null +++ b/deploy/docker/metric.yaml @@ -0,0 +1,15 @@ +# ============================================================ +# 数据指标服务容器化配置(docker compose 专用,镜像内由网关同镜发起) +# 与 metric/rpc/etc/metric.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# ============================================================ +Name: metric.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8090 +Etcd: + Hosts: + - etcd:2379 + Key: metric.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/deploy/docker/quality.yaml b/deploy/docker/quality.yaml new file mode 100644 index 0000000..f8a0134 --- /dev/null +++ b/deploy/docker/quality.yaml @@ -0,0 +1,15 @@ +# ============================================================ +# 数据质量服务容器化配置(docker compose 专用,镜像内由网关同镜发起) +# 与 quality/rpc/etc/quality.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# ============================================================ +Name: quality.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8089 +Etcd: + Hosts: + - etcd:2379 + Key: quality.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/deploy/docker/tag.yaml b/deploy/docker/tag.yaml new file mode 100644 index 0000000..ac88616 --- /dev/null +++ b/deploy/docker/tag.yaml @@ -0,0 +1,15 @@ +# ============================================================ +# 数据标签服务容器化配置(docker compose 专用,镜像内由网关同镜发起) +# 与 tag/rpc/etc/tag.example.yaml 对齐,主机名改为 compose 服务名: +# 127.0.0.1 -> mysql / etcd(容器网络内服务名互访) +# ============================================================ +Name: tag.rpc +Log: + Mode: console +ListenOn: 0.0.0.0:8091 +Etcd: + Hosts: + - etcd:2379 + Key: tag.rpc +Mysql: + DataSource: root:admin@tcp(mysql:3306)/datacenter?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4b6eea0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,220 @@ +# ============================================================ +# 数据中台一键编排:4 个依赖(mysql/redis/etcd/elasticsearch)+ 11 个服务 +# +# 用法: +# docker compose build # 首次构建镜像(见 Dockerfile) +# docker compose up -d # 启动全部(mysql 健康后才会起服务) +# docker compose ps / logs -f datacenter-api +# +# 访问:仅网关 8887 对外(http://localhost:8887),其余容器网络内互通; +# 数据库等依赖如需从宿主机直连,可临时 `docker compose port` 或 +# 自行加端口映射。 +# +# 说明: +# 1. 全部服务共用 Dockerfile 构建出的同一镜像,靠不同 command 启动; +# 2. 各服务配置来自 deploy/docker/*.yaml(主机名即 compose 服务名); +# 3. 初始化 SQL 首次启动空库时按文件名顺序自动执行: +# 01-schema.sql(./sql.sql) -> 02-phase1 -> 03-phase2 -> 04-phase3; +# 4. 网关还注册了 user/common/votes/search/questions 五个遗留 RPC, +# 不在本编排内(etcd 发现懒加载,网关可正常启动);如需 /user/login +# 等接口请按 restart.sh 另起对应服务后再调用。 +# ============================================================ +name: datacenter + +# Go 服务公共模板:同镜像 + 等依赖健康后启动 +x-go-service: &go-service + image: datacenter:latest + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + environment: + TZ: Asia/Shanghai + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + etcd: + condition: service_healthy + networks: + - datacenter + +services: + # -------------------- 依赖 -------------------- + mysql: + image: mysql:8.4 + container_name: datacenter-mysql + environment: + MYSQL_ROOT_PASSWORD: admin + # initdb 脚本默认落在该库(sql.sql/phase*.sql 内无 CREATE DATABASE) + MYSQL_DATABASE: datacenter + TZ: Asia/Shanghai + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_general_ci + volumes: + # 数据持久化到宿主机 ./data/mysql + - ./data/mysql:/var/lib/mysql + # 初始化 SQL:docker-entrypoint-initdb.d 按文件名顺序自动执行 + - ./sql.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro + - ./docs/schema/phase1.sql:/docker-entrypoint-initdb.d/02-phase1.sql:ro + - ./docs/schema/phase2.sql:/docker-entrypoint-initdb.d/03-phase2.sql:ro + - ./docs/schema/phase3.sql:/docker-entrypoint-initdb.d/04-phase3.sql:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-padmin"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 30s + networks: + - datacenter + + redis: + image: redis:8-alpine + container_name: datacenter-redis + command: ["redis-server", "--requirepass", "admin"] + healthcheck: + test: ["CMD", "redis-cli", "-a", "admin", "ping"] + interval: 10s + timeout: 5s + retries: 10 + networks: + - datacenter + + etcd: + image: quay.io/coreos/etcd:v3.5.21 + container_name: datacenter-etcd + environment: + ETCDCTL_API: "3" + command: + - /usr/local/bin/etcd + - --name=s1 + - --data-dir=/etcd-data + # 单节点:client 口对所有网卡,advertise 用服务名(容器内互访) + - --listen-client-urls=http://0.0.0.0:2379 + - --advertise-client-urls=http://etcd:2379 + - --listen-peer-urls=http://0.0.0.0:2380 + - --initial-advertise-peer-urls=http://etcd:2380 + - --initial-cluster=s1=http://etcd:2380 + - --initial-cluster-state=new + - --initial-cluster-token=datacenter-etcd + healthcheck: + test: ["CMD", "etcdctl", "--endpoints=http://127.0.0.1:2379", "endpoint", "health"] + interval: 10s + timeout: 5s + retries: 10 + networks: + - datacenter + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.28 + container_name: datacenter-es + environment: + # 单节点 + 开启安全(elastic/admin),供数据源/检索类场景使用 + discovery.type: single-node + xpack.security.enabled: "true" + ELASTIC_PASSWORD: admin + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + healthcheck: + test: ["CMD-SHELL", "curl -s -u elastic:admin http://127.0.0.1:9200/_cluster/health | grep -q status"] + interval: 15s + timeout: 10s + retries: 12 + start_period: 60s + networks: + - datacenter + + # -------------------- Phase 1:数据中台服务 -------------------- + datasource: + <<: *go-service + container_name: datacenter-datasource + command: ["./datasource-server", "-f", "/app/etc/datasource.yaml"] + + metadata: + <<: *go-service + container_name: datacenter-metadata + command: ["./metadata-server", "-f", "/app/etc/metadata.yaml"] + + datasvc: + <<: *go-service + container_name: datacenter-datasvc + command: ["./datasvc-server", "-f", "/app/etc/datasvc.yaml"] + + catalog: + <<: *go-service + container_name: datacenter-catalog + command: ["./catalog-server", "-f", "/app/etc/catalog.yaml"] + + # -------------------- Phase 2 -------------------- + quality: + <<: *go-service + container_name: datacenter-quality + command: ["./quality-server", "-f", "/app/etc/quality.yaml"] + + metric: + <<: *go-service + container_name: datacenter-metric + command: ["./metric-server", "-f", "/app/etc/metric.yaml"] + + tag: + <<: *go-service + container_name: datacenter-tag + command: ["./tag-server", "-f", "/app/etc/tag.yaml"] + + # -------------------- Phase 3 -------------------- + job: + <<: *go-service + container_name: datacenter-job + command: ["./job-server", "-f", "/app/etc/job.yaml"] + + ide: + <<: *go-service + container_name: datacenter-ide + command: ["./ide-server", "-f", "/app/etc/ide.yaml"] + + # -------------------- Phase 4 -------------------- + ai: + <<: *go-service + container_name: datacenter-ai + command: ["./ai-server", "-f", "/app/etc/ai.yaml"] + + # -------------------- 网关(唯一对外端口) -------------------- + datacenter-api: + <<: *go-service + container_name: datacenter-api + command: ["./datacenter-api", "-f", "/app/etc/datacenter-api.docker.yaml"] + ports: + - "8887:8887" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + etcd: + condition: service_healthy + # 10 个 RPC 注册到 etcd 后再起网关(服务发现懒加载,此为顺序优化) + datasource: + condition: service_started + metadata: + condition: service_started + datasvc: + condition: service_started + catalog: + condition: service_started + quality: + condition: service_started + metric: + condition: service_started + tag: + condition: service_started + job: + condition: service_started + ide: + condition: service_started + ai: + condition: service_started + +networks: + datacenter: + driver: bridge diff --git a/docs/datacenter.postman_collection.json b/docs/datacenter.postman_collection.json new file mode 100644 index 0000000..23f00ef --- /dev/null +++ b/docs/datacenter.postman_collection.json @@ -0,0 +1,2842 @@ +{ + "info": { + "_postman_id": "139fccba-75f3-4586-a1e0-eb00a2094d93", + "name": "数据中台接口(datacenter)", + "description": "数据中台 Postman 集合(由 *.api 文件生成,Postman v2.1)。\n\n使用步骤:\n1. 启动服务:docker compose up -d(网关 http://localhost:8887);\n2. 调 auth 组「用户登录」/user/login 获取 JWT,Tests 脚本自动把 access_token 写入集合变量 token;\n3. 其余请求自动携带 Authorization: Bearer {{token}};\n4. 例外:/datasvc/execute 为对外执行入口,走 app_key+签名鉴权,不带 JWT。\n\n注意:user/common/votes/search/questions 五个遗留 RPC 未包含在 docker compose 编排内,/user/login 需先按 restart.sh 另起 user 服务。", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "base", + "value": "http://localhost:8887" + }, + { + "key": "token", + "value": "" + } + ], + "item": [ + { + "name": "auth(认证,先登录拿 token)", + "item": [ + { + "name": "连通性检查", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/user/ping", + "host": [ + "{{base}}" + ], + "path": [ + "user", + "ping" + ] + }, + "description": "来源:user.api\n路由:POST /user/ping\n无需 JWT" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "try {", + " var data = pm.response.json();", + " if (data && data.access_token) {", + " pm.collectionVariables.set('token', data.access_token);", + " console.log('已将 access_token 写入集合变量 token');", + " } else {", + " console.log('响应中无 access_token,请检查账号/服务');", + " }", + "} catch (e) {", + " console.log('解析响应失败: ' + e);", + "}" + ] + } + } + ] + }, + { + "name": "用户注册", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/user/register", + "host": [ + "{{base}}" + ], + "path": [ + "user", + "register" + ] + }, + "description": "来源:user.api\n路由:POST /user/register\n无需 JWT", + "body": { + "mode": "raw", + "raw": "{\n \"mobile\": \"13800138000\",\n \"password\": \"123456\",\n \"smscode\": \"123456\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "try {", + " var data = pm.response.json();", + " if (data && data.access_token) {", + " pm.collectionVariables.set('token', data.access_token);", + " console.log('已将 access_token 写入集合变量 token');", + " } else {", + " console.log('响应中无 access_token,请检查账号/服务');", + " }", + "} catch (e) {", + " console.log('解析响应失败: ' + e);", + "}" + ] + } + } + ] + }, + { + "name": "用户登录(获取 JWT)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/user/login", + "host": [ + "{{base}}" + ], + "path": [ + "user", + "login" + ] + }, + "description": "来源:user.api\n路由:POST /user/login\n无需 JWT", + "body": { + "mode": "raw", + "raw": "{\n \"mobile\": \"13800138000\",\n \"type\": 1,\n \"password\": \"123456\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "try {", + " var data = pm.response.json();", + " if (data && data.access_token) {", + " pm.collectionVariables.set('token', data.access_token);", + " console.log('已将 access_token 写入集合变量 token');", + " } else {", + " console.log('响应中无 access_token,请检查账号/服务');", + " }", + "} catch (e) {", + " console.log('解析响应失败: ' + e);", + "}" + ] + } + } + ] + }, + { + "name": "微信登录", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/user/wx/login", + "host": [ + "{{base}}" + ], + "path": [ + "user", + "wx", + "login" + ] + }, + "description": "来源:user.api\n路由:POST /user/wx/login\n无需 JWT", + "body": { + "mode": "raw", + "raw": "{\n \"beid\": 1,\n \"code\": \"微信授权code\",\n \"ptyid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "try {", + " var data = pm.response.json();", + " if (data && data.access_token) {", + " pm.collectionVariables.set('token', data.access_token);", + " console.log('已将 access_token 写入集合变量 token');", + " } else {", + " console.log('响应中无 access_token,请检查账号/服务');", + " }", + "} catch (e) {", + " console.log('解析响应失败: ' + e);", + "}" + ] + } + } + ] + }, + { + "name": "微信 code2Session", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/user/wx/login", + "host": [ + "{{base}}" + ], + "path": [ + "user", + "wx", + "login" + ] + }, + "description": "来源:user.api\n路由:GET /user/wx/login\n无需 JWT" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "try {", + " var data = pm.response.json();", + " if (data && data.access_token) {", + " pm.collectionVariables.set('token', data.access_token);", + " console.log('已将 access_token 写入集合变量 token');", + " } else {", + " console.log('响应中无 access_token,请检查账号/服务');", + " }", + "} catch (e) {", + " console.log('解析响应失败: ' + e);", + "}" + ] + } + } + ] + } + ] + }, + { + "name": "datasource(数据源管理)", + "item": [ + { + "name": "创建数据源", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasource/create", + "host": [ + "{{base}}" + ], + "path": [ + "datasource", + "create" + ] + }, + "description": "来源:datasource.api\n路由:POST /datasource/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"示例 MySQL 数据源\",\n \"type\": \"mysql\",\n \"host\": \"mysql\",\n \"port\": 3306,\n \"db_name\": \"datacenter\",\n \"username\": \"root\",\n \"password\": \"admin\",\n \"params_json\": \"{}\",\n \"owner\": \"admin\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "更新数据源", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasource/update", + "host": [ + "{{base}}" + ], + "path": [ + "datasource", + "update" + ] + }, + "description": "来源:datasource.api\n路由:POST /datasource/update\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1,\n \"name\": \"示例 MySQL 数据源\",\n \"type\": \"mysql\",\n \"host\": \"mysql\",\n \"port\": 3306,\n \"db_name\": \"datacenter\",\n \"username\": \"root\",\n \"password\": \"admin\",\n \"status\": 1,\n \"owner\": \"admin\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "删除数据源", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasource/delete?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "datasource", + "delete" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:datasource.api\n路由:POST /datasource/delete\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "数据源详情", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/datasource/info?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "datasource", + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:datasource.api\n路由:GET /datasource/info\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "数据源列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/datasource/list?beid=1&type=mysql&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "datasource", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "type", + "value": "mysql" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:datasource.api\n路由:GET /datasource/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "测试数据源连接", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/datasource/test?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "datasource", + "test" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:datasource.api\n路由:GET /datasource/test\n需要 JWT(登录后自动携带)" + } + } + ] + }, + { + "name": "metadata(元数据管理)", + "item": [ + { + "name": "采集库表元数据", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/metadata/collect", + "host": [ + "{{base}}" + ], + "path": [ + "metadata", + "collect" + ] + }, + "description": "来源:metadata.api\n路由:POST /metadata/collect\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"ds_id\": 1,\n \"beid\": 1,\n \"db_type\": \"mysql\",\n \"dsn\": \"\",\n \"db_name\": \"datacenter\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "实体详情", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/metadata/entity/info?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "metadata", + "entity", + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:metadata.api\n路由:GET /metadata/entity/info\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "实体列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/metadata/entity/list?beid=1&entity_type=table&keyword=&ds_id=1&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "metadata", + "entity", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "entity_type", + "value": "table" + }, + { + "key": "keyword", + "value": "" + }, + { + "key": "ds_id", + "value": "1" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:metadata.api\n路由:GET /metadata/entity/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "登记血缘", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/metadata/lineage/add", + "host": [ + "{{base}}" + ], + "path": [ + "metadata", + "lineage", + "add" + ] + }, + "description": "来源:metadata.api\n路由:POST /metadata/lineage/add\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"from_id\": 1,\n \"to_id\": 2,\n \"props_json\": \"{}\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "血缘图", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/metadata/lineage?entity_id=1&direction=downstream&depth=2&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "metadata", + "lineage" + ], + "query": [ + { + "key": "entity_id", + "value": "1" + }, + { + "key": "direction", + "value": "downstream" + }, + { + "key": "depth", + "value": "2" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:metadata.api\n路由:GET /metadata/lineage\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "定版快照", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/metadata/version/create?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "metadata", + "version", + "create" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:metadata.api\n路由:POST /metadata/version/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "版本列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/metadata/version/list?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "metadata", + "version", + "list" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:metadata.api\n路由:GET /metadata/version/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "版本对比", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/metadata/version/compare?version_a=1&version_b=2", + "host": [ + "{{base}}" + ], + "path": [ + "metadata", + "version", + "compare" + ], + "query": [ + { + "key": "version_a", + "value": "1" + }, + { + "key": "version_b", + "value": "2" + } + ] + }, + "description": "来源:metadata.api\n路由:GET /metadata/version/compare\n需要 JWT(登录后自动携带)" + } + } + ] + }, + { + "name": "datasvc(数据服务 API)", + "item": [ + { + "name": "创建数据API", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasvc/api/create", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "api", + "create" + ] + }, + "description": "来源:datasvc.api\n路由:POST /datasvc/api/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"订单按天统计\",\n \"path\": \"/api/v1/order/daily\",\n \"mode\": \"sql\",\n \"datasource_id\": 1,\n \"sql_text\": \"select date(created_at) as d, count(*) as cnt from orders group by d\",\n \"params_json\": \"{}\",\n \"cache_ttl\": 60,\n \"owner\": \"admin\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "更新数据API", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasvc/api/update", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "api", + "update" + ] + }, + "description": "来源:datasvc.api\n路由:POST /datasvc/api/update\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1,\n \"name\": \"示例名称\",\n \"sql_text\": \"select id, name from orders limit 10\",\n \"params_json\": \"{}\",\n \"cache_ttl\": 60\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "删除数据API", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasvc/api/delete?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "api", + "delete" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:datasvc.api\n路由:POST /datasvc/api/delete\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "API详情", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/datasvc/api/info?path=/api/v1/demo&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "api", + "info" + ], + "query": [ + { + "key": "path", + "value": "/api/v1/demo" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:datasvc.api\n路由:GET /datasvc/api/info\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "API列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/datasvc/api/list?beid=1&status=string&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "api", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "status", + "value": "string" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:datasvc.api\n路由:GET /datasvc/api/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "发布API", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasvc/api/publish?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "api", + "publish" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:datasvc.api\n路由:POST /datasvc/api/publish\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "下线API", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasvc/api/offline?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "api", + "offline" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:datasvc.api\n路由:POST /datasvc/api/offline\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "注册调用应用", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasvc/app/register", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "app", + "register" + ] + }, + "description": "来源:datasvc.api\n路由:POST /datasvc/app/register\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"示例名称\",\n \"rate_limit_qps\": 1,\n \"ip_whitelist\": \"\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "授权API给应用", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasvc/grant", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "grant" + ] + }, + "description": "来源:datasvc.api\n路由:POST /datasvc/grant\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"api_id\": 1,\n \"app_id\": 1,\n \"expire_at\": 0,\n \"field_mask\": \"\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "执行数据API(外部统一入口)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/datasvc/execute", + "host": [ + "{{base}}" + ], + "path": [ + "datasvc", + "execute" + ] + }, + "description": "来源:datasvc.api\n路由:POST /datasvc/execute\n无需 JWT", + "body": { + "mode": "raw", + "raw": "{\n \"app_key\": \"your-app-key\",\n \"sign\": \"按约定算法计算的签名\",\n \"ts\": \"1700000000\",\n \"path\": \"/api/v1/order/daily\",\n \"params_json\": \"{}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + } + ] + }, + { + "name": "catalog(资产目录)", + "item": [ + { + "name": "创建资产分类", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/catalog/category/create", + "host": [ + "{{base}}" + ], + "path": [ + "catalog", + "category", + "create" + ] + }, + "description": "来源:catalog.api\n路由:POST /catalog/category/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"parent_id\": 0,\n \"name\": \"示例名称\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "分类列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/catalog/category/list?beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "catalog", + "category", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:catalog.api\n路由:GET /catalog/category/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "登记资产", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/catalog/asset/tag", + "host": [ + "{{base}}" + ], + "path": [ + "catalog", + "asset", + "tag" + ] + }, + "description": "来源:catalog.api\n路由:POST /catalog/asset/tag\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"entity_id\": 1,\n \"category_id\": 1,\n \"name\": \"示例名称\",\n \"entity_type\": \"table\",\n \"comment\": \"备注\",\n \"classification\": \"内部\",\n \"asset_tags_json\": \"[]\",\n \"visibility\": \"public\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "资产列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/catalog/asset/list?beid=1&category_id=1&keyword=&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "catalog", + "asset", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "category_id", + "value": "1" + }, + { + "key": "keyword", + "value": "" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:catalog.api\n路由:GET /catalog/asset/list\n需要 JWT(登录后自动携带)" + } + } + ] + }, + { + "name": "quality(数据质量)", + "item": [ + { + "name": "创建质量规则", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/quality/rule/create", + "host": [ + "{{base}}" + ], + "path": [ + "quality", + "rule", + "create" + ] + }, + "description": "来源:quality.api\n路由:POST /quality/rule/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"示例名称\",\n \"rule_type\": \"not_null\",\n \"target_table\": \"orders\",\n \"target_column\": \"id\",\n \"params_json\": \"{}\",\n \"threshold_json\": \"{}\",\n \"owner\": \"admin\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "删除质量规则", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/quality/rule/delete?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "quality", + "rule", + "delete" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:quality.api\n路由:POST /quality/rule/delete\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "质量规则列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/quality/rule/list?beid=1&rule_type=not_null&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "quality", + "rule", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "rule_type", + "value": "not_null" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:quality.api\n路由:GET /quality/rule/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "创建质量任务", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/quality/job/create", + "host": [ + "{{base}}" + ], + "path": [ + "quality", + "job", + "create" + ] + }, + "description": "来源:quality.api\n路由:POST /quality/job/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"示例名称\",\n \"datasource_id\": 1,\n \"db_name\": \"datacenter\",\n \"rule_ids\": \"1\",\n \"cron\": \"0 0 * * *\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "质量任务列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/quality/job/list?beid=1&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "quality", + "job", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:quality.api\n路由:GET /quality/job/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "执行质量任务", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/quality/job/run?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "quality", + "job", + "run" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:quality.api\n路由:POST /quality/job/run\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "质量结果列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/quality/result/list?job_id=1&pass_filter=0&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "quality", + "result", + "list" + ], + "query": [ + { + "key": "job_id", + "value": "1" + }, + { + "key": "pass_filter", + "value": "0" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:quality.api\n路由:GET /quality/result/list\n需要 JWT(登录后自动携带)" + } + } + ] + }, + { + "name": "metric(数据指标)", + "item": [ + { + "name": "创建指标", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/metric/create", + "host": [ + "{{base}}" + ], + "path": [ + "metric", + "create" + ] + }, + "description": "来源:metric.api\n路由:POST /metric/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"name_en\": \"order_amount\",\n \"name_cn\": \"订单金额\",\n \"metric_type\": \"atom\",\n \"biz_field\": \"string\",\n \"agg_type\": \"string\",\n \"expr\": \"sum(amount)\",\n \"dimension_json\": \"{}\",\n \"comment\": \"备注\",\n \"owner\": \"admin\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "更新指标", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/metric/update", + "host": [ + "{{base}}" + ], + "path": [ + "metric", + "update" + ] + }, + "description": "来源:metric.api\n路由:POST /metric/update\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1,\n \"name_cn\": \"订单金额\",\n \"expr\": \"sum(amount)\",\n \"dimension_json\": \"{}\",\n \"comment\": \"备注\",\n \"status\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "删除指标", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/metric/delete?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "metric", + "delete" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:metric.api\n路由:POST /metric/delete\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "指标详情", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/metric/info?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "metric", + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:metric.api\n路由:GET /metric/info\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "指标列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/metric/list?beid=1&metric_type=atom&keyword=&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "metric", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "metric_type", + "value": "atom" + }, + { + "key": "keyword", + "value": "" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:metric.api\n路由:GET /metric/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "派生指标绑定原子指标", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/metric/bind/atoms", + "host": [ + "{{base}}" + ], + "path": [ + "metric", + "bind", + "atoms" + ] + }, + "description": "来源:metric.api\n路由:POST /metric/bind/atoms\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"derive_metric_id\": 1,\n \"atom_metric_ids\": [\n 1\n ],\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "派生指标引用的原子指标", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/metric/atoms?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "metric", + "atoms" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:metric.api\n路由:GET /metric/atoms\n需要 JWT(登录后自动携带)" + } + } + ] + }, + { + "name": "tag(数据标签)", + "item": [ + { + "name": "创建标签对象", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/tag/object/create", + "host": [ + "{{base}}" + ], + "path": [ + "tag", + "object", + "create" + ] + }, + "description": "来源:tag.api\n路由:POST /tag/object/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"object_type\": \"table\",\n \"name\": \"示例名称\",\n \"ds_id\": 1,\n \"db_name\": \"datacenter\",\n \"table_name\": \"orders\",\n \"pk_column\": \"id\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "标签对象列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/tag/object/list?beid=1&object_type=table&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "tag", + "object", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "object_type", + "value": "table" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:tag.api\n路由:GET /tag/object/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "创建标签定义", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/tag/def/create", + "host": [ + "{{base}}" + ], + "path": [ + "tag", + "def", + "create" + ] + }, + "description": "来源:tag.api\n路由:POST /tag/def/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"object_id\": 1,\n \"tag_key\": \"vip_level\",\n \"tag_name\": \"会员等级\",\n \"tag_type\": \"enum\",\n \"rule_sql\": \"select id from orders where amount > 0\",\n \"refresh_cron\": \"0 0 * * *\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "标签定义列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/tag/def/list?beid=1&object_id=1&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "tag", + "def", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "object_id", + "value": "1" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:tag.api\n路由:GET /tag/def/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "删除标签定义", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/tag/def/delete?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "tag", + "def", + "delete" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:tag.api\n路由:POST /tag/def/delete\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "执行打标", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/tag/tagging", + "host": [ + "{{base}}" + ], + "path": [ + "tag", + "tagging" + ] + }, + "description": "来源:tag.api\n路由:POST /tag/tagging\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"tag_id\": 1,\n \"object_id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "对象标签画像", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/tag/object/tags?object_pk=1&object_id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "tag", + "object", + "tags" + ], + "query": [ + { + "key": "object_pk", + "value": "1" + }, + { + "key": "object_id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:tag.api\n路由:GET /tag/object/tags\n需要 JWT(登录后自动携带)" + } + } + ] + }, + { + "name": "job(调度集成)", + "item": [ + { + "name": "创建同步任务", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/job/task/create", + "host": [ + "{{base}}" + ], + "path": [ + "job", + "task", + "create" + ] + }, + "description": "来源:job.api\n路由:POST /job/task/create\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"示例名称\",\n \"source_ds_id\": 1,\n \"target_ds_id\": 1,\n \"mode\": \"sql\",\n \"mapping_json\": \"{}\",\n \"cron\": \"0 0 * * *\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "同步任务列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/job/task/list?beid=1&status=string&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "job", + "task", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "status", + "value": "string" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:job.api\n路由:GET /job/task/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "删除同步任务", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/job/task/delete?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "job", + "task", + "delete" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:job.api\n路由:POST /job/task/delete\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "部署到调度器", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/job/task/deploy?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "job", + "task", + "deploy" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:job.api\n路由:POST /job/task/deploy\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "手动触发任务", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/job/task/trigger?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "job", + "task", + "trigger" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:job.api\n路由:POST /job/task/trigger\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "任务状态", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/job/task/status?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "job", + "task", + "status" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:job.api\n路由:GET /job/task/status\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "调度器连接信息", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/job/scheduler/info?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "job", + "scheduler", + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:job.api\n路由:GET /job/scheduler/info\n需要 JWT(登录后自动携带)" + } + } + ] + }, + { + "name": "ide(SQL IDE)", + "item": [ + { + "name": "保存脚本", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/ide/script/save", + "host": [ + "{{base}}" + ], + "path": [ + "ide", + "script", + "save" + ] + }, + "description": "来源:ide.api\n路由:POST /ide/script/save\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"name\": \"示例名称\",\n \"sql_text\": \"select id, name from orders limit 10\",\n \"datasource_id\": 1,\n \"db_name\": \"datacenter\",\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "脚本列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/ide/script/list?beid=1&keyword=&page=1&size=10", + "host": [ + "{{base}}" + ], + "path": [ + "ide", + "script", + "list" + ], + "query": [ + { + "key": "beid", + "value": "1" + }, + { + "key": "keyword", + "value": "" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "size", + "value": "10" + } + ] + }, + "description": "来源:ide.api\n路由:GET /ide/script/list\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "脚本详情", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{base}}/ide/script/info?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "ide", + "script", + "info" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:ide.api\n路由:GET /ide/script/info\n需要 JWT(登录后自动携带)" + } + }, + { + "name": "删除脚本", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/ide/script/delete?id=1&beid=1", + "host": [ + "{{base}}" + ], + "path": [ + "ide", + "script", + "delete" + ], + "query": [ + { + "key": "id", + "value": "1" + }, + { + "key": "beid", + "value": "1" + } + ] + }, + "description": "来源:ide.api\n路由:POST /ide/script/delete\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "执行脚本(仅 SELECT,行数保护)", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/ide/script/run", + "host": [ + "{{base}}" + ], + "path": [ + "ide", + "script", + "run" + ] + }, + "description": "来源:ide.api\n路由:POST /ide/script/run\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"beid\": 1,\n \"max_rows\": 100\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + } + ] + }, + { + "name": "ai(AI 问数)", + "item": [ + { + "name": "AI 问数", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{base}}/ai/ask", + "host": [ + "{{base}}" + ], + "path": [ + "ai", + "ask" + ] + }, + "description": "来源:ai.api\n路由:POST /ai/ask\n需要 JWT(登录后自动携带)", + "body": { + "mode": "raw", + "raw": "{\n \"question\": \"上个月订单总额是多少\",\n \"beid\": 1,\n \"max_candidates\": 5\n}", + "options": { + "raw": { + "language": "json" + } + } + } + } + } + ] + } + ] +} diff --git a/etc/datacenter-api.example.yaml b/etc/datacenter-api.example.yaml index 67457df..3bce4cb 100644 --- a/etc/datacenter-api.example.yaml +++ b/etc/datacenter-api.example.yaml @@ -34,7 +34,8 @@ QuestionsRpc: Etcd: Hosts: - 127.0.0.1:2379 - Key: questions.rpc DataSourceRpc: + Key: questions.rpc +DataSourceRpc: Etcd: Hosts: - 127.0.0.1:2379 diff --git a/internal/handler/ai/askandrunhandler.go b/internal/handler/ai/askandrunhandler.go new file mode 100644 index 0000000..6ba279c --- /dev/null +++ b/internal/handler/ai/askandrunhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ai + +import ( + "net/http" + + "datacenter/internal/logic/ai" + "datacenter/internal/svc" + "datacenter/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// AI 问数并执行(LLM 生成 SQL 后经 ide 安全闸自动执行) +func AskAndRunHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AskAndRunReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := ai.NewAskAndRunLogic(r.Context(), svcCtx) + resp, err := l.AskAndRun(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 6568463..fb437fe 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -35,6 +35,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/ai/ask", Handler: ai.AskHandler(serverCtx), }, + { + // AI 问数并执行(LLM 生成 SQL 后经 ide 安全闸自动执行) + Method: http.MethodPost, + Path: "/ai/ask_and_run", + Handler: ai.AskAndRunHandler(serverCtx), + }, }, rest.WithJwt(serverCtx.Config.Auth.AccessSecret), ) diff --git a/internal/logic/ai/askandrunlogic.go b/internal/logic/ai/askandrunlogic.go new file mode 100644 index 0000000..87e5a5d --- /dev/null +++ b/internal/logic/ai/askandrunlogic.go @@ -0,0 +1,78 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.2 + +package ai + +import ( + "context" + + "datacenter/ai/rpc/aiClient" + "datacenter/internal/svc" + "datacenter/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AskAndRunLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// AI 问数并执行(LLM 生成 SQL 后经 ide 安全闸自动执行) +func NewAskAndRunLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AskAndRunLogic { + return &AskAndRunLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AskAndRunLogic) AskAndRun(req *types.AskAndRunReq) (resp *types.AskAndRunResp, err error) { + // max_rows 默认 20(rpc 侧上限 100) + maxRows := req.MaxRows + if maxRows <= 0 { + maxRows = 20 + } + // max_candidates 默认 5(与 Ask 口径一致) + maxCandidates := req.MaxCandidates + if maxCandidates <= 0 { + maxCandidates = 5 + } + + reply, err := l.svcCtx.AiRpc.AskAndRun(l.ctx, &aiClient.AskAndRunReq{ + Question: req.Question, + Beid: req.Beid, + MaxCandidates: int32(maxCandidates), + AutoRun: req.AutoRun, + DatasourceId: req.DatasourceId, + MaxRows: int32(maxRows), + }) + if err != nil { + return nil, err //业务错误直接透传 rpc error + } + + candidates := make([]types.Candidate, 0, len(reply.Candidates)) + for _, c := range reply.Candidates { + candidates = append(candidates, types.Candidate{ + Type: c.GetType(), + Id: c.GetId(), + Name: c.GetName(), + Detail: c.GetDetail(), + Comment: c.GetComment(), + }) + } + + return &types.AskAndRunResp{ + Candidates: candidates, + LlmEnabled: reply.GetLlmEnabled(), + GeneratedSql: reply.GetGeneratedSql(), + Answer: reply.GetAnswer(), + CostMs: reply.GetCostMs(), + Executed: reply.GetExecuted(), + ColumnsJson: reply.GetColumnsJson(), + RowsJson: reply.GetRowsJson(), + RowCount: reply.GetRowCount(), + RunError: reply.GetRunError(), + }, nil +} diff --git a/internal/types/types.go b/internal/types/types.go index 94d6ed3..ac511f1 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -97,6 +97,28 @@ type ArticleReq struct { ImageUrl string `json:"ImageUrl"` } +type AskAndRunReq struct { + Question string `json:"question"` + Beid int64 `json:"beid,optional"` + MaxCandidates int32 `json:"max_candidates,optional"` + AutoRun bool `json:"auto_run,optional"` //生成 SQL 后经 ide 安全闸自动执行 + DatasourceId int64 `json:"datasource_id,optional"` //auto_run 时必填:执行目标库 + MaxRows int32 `json:"max_rows,optional"` //默认 20,上限 100 +} + +type AskAndRunResp struct { + Candidates []Candidate `json:"candidates"` + LlmEnabled bool `json:"llm_enabled"` + GeneratedSql string `json:"generated_sql,optional"` + Answer string `json:"answer"` + CostMs int64 `json:"cost_ms"` + Executed bool `json:"executed"` + ColumnsJson string `json:"columns_json,optional"` + RowsJson string `json:"rows_json,optional"` + RowCount int64 `json:"row_count"` + RunError string `json:"run_error,optional"` +} + type AskReq struct { Question string `json:"question"` Beid int64 `json:"beid,optional"` From 6867b113d9765b25122bb8030f5b1deccb147917 Mon Sep 17 00:00:00 2001 From: jackluo Date: Sat, 10 Oct 2026 14:50:15 +0800 Subject: [PATCH 12/12] =?UTF-8?q?CI=20=E4=BF=AE=E5=A4=8D=EF=BC=9Avet=20?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E8=A7=84=E5=88=99=E8=A1=A5=E5=85=A8=20copylo?= =?UTF-8?q?cks=20=E7=AC=AC=E4=BA=8C=E7=B1=BB=E6=8E=AA=E8=BE=9E=EF=BC=88cop?= =?UTF-8?q?ies=20lock=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首跑失败原因:存量 pb 值传递告警有两种措辞('lock by value' 与 'copies lock value'/'copies lock'), 原过滤只匹配前者,导致 15 条存量告警误报为新增。本地复现验证过滤后零残留。 --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3dd5232..71a4d16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,14 +36,14 @@ jobs: - name: go build run: go build ./... - # 静态检查:过滤存量 pb 值传递(lock by value)告警后判断退出码 + # 静态检查:过滤存量 pb 值传递(copylocks)告警后判断退出码 # - 存量 24 处告警见 issue #18,随 pb 重生成一并修复; - # - 过滤规则:剔除 "lock by value" 告警行与 "# 包名" 标题行, - # 其余任何输出(即新增告警)都视为失败。 + # - 过滤规则:剔除 copylocks 两类措辞("lock by value"/"copies lock")的 + # 告警行与 "# 包名" 标题行,其余任何输出(即新增告警)都视为失败。 - name: go vet(过滤存量 pb 值传递告警,见 issue #18) run: | vet_out=$(go vet ./... 2>&1) || true - filtered=$(printf '%s\n' "$vet_out" | grep -v "lock by value" | grep -v "^#" || true) + filtered=$(printf '%s\n' "$vet_out" | grep -v "lock by value" | grep -v "copies lock" | grep -v "^#" || true) if [ -n "$filtered" ]; then printf '%s\n' "$filtered" echo "::error::go vet 发现新增告警(存量 pb 值传递告警见 issue #18,不在拦截范围)"