Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
56 changes: 56 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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 值传递(copylocks)告警后判断退出码
# - 存量 24 处告警见 issue #18,随 pb 重生成一并修复;
# - 过滤规则:剔除 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 "copies lock" | 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
17 changes: 16 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,19 @@ user/api/etc/user-api.yaml
mysql8
redis
etcd
elasticsearch
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
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
job/rpc/etc/job.yaml
ide/rpc/etc/ide.yaml
ai/rpc/etc/ai.yaml
58 changes: 58 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
83 changes: 51 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,22 @@ 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

# 以下为本开源项目正文

# 基于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)

## 架构图
Expand All @@ -87,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":"<app_key>","ts":"<unix秒>","sign":"<hex签名>","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 服务
Expand All @@ -109,39 +158,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
### elasticsearch 的帐号密码已经通过 server.sh 内置(`ELASTIC_PASSWORD=admin`)
### 即帐号: elastic ,密码: admin,将其填入 search/rpc/etc/search.yaml 文件中即可

Changed password for user logstash_system
PASSWORD logstash_system = aqjVEdMsG7P2CXm9sQNk

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数据中 ,如果有工具自行导入,下面仅参考
Expand Down
61 changes: 61 additions & 0 deletions ai.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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"`
}

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
)
service datacenter-api {
@doc(
summary: "AI 问数"
)
@handler ask
post /ai/ask (AskReq) returns (AskResp)
@doc(
summary: "AI 问数并执行(LLM 生成 SQL 后经 ide 安全闸自动执行)"
)
@handler askAndRun
post /ai/ask_and_run (AskAndRunReq) returns (AskAndRunResp)
}
62 changes: 62 additions & 0 deletions ai/model/asklogmodel.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading
Loading