fix dockerfile unhealth problem
This commit is contained in:
parent
1168d49178
commit
4cd221af5e
|
|
@ -0,0 +1,10 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoImports">
|
||||
<option name="excludedPackages">
|
||||
<array>
|
||||
<option value="golang.org/x/net/context" />
|
||||
</array>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="userId" value="73c9beac:19eed3331be:-7ebe" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/haixun-backend.iml" filepath="$PROJECT_DIR$/.idea/haixun-backend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
# Agent Handoff Notes
|
||||
|
||||
這個資料夾是新的巡樓後端核心,請優先維持乾淨邊界,不要把舊 Next.js 或 `template-monorepo` 的業務包袱搬進來。
|
||||
|
||||
## 核心原則
|
||||
|
||||
- 全系統時間一律 **UTC+0**;寫入 Mongo / API 的時間欄位一律 **unix nanoseconds**(`int64`)。排程的 `timezone` 只用於 cron 解讀與下發 payload,不作為儲存時區。
|
||||
- 複製模式,不複製舊業務。
|
||||
- `logic` 做 API 編排,`model/usecase` 做可重複使用能力。
|
||||
- provider adapter 不讀 setting、不碰 Mongo、不知道 HTTP。
|
||||
- setting 是通用 key-value model,不依賴 AI 或其他業務。
|
||||
- token / API key 第一版每次 request 帶入,不寫入 config。
|
||||
- SSE contract 由本服務 normalize,前端不要讀 provider 原始 chunk。
|
||||
- JSON API 必須使用 `code/message/data/error` envelope 與 `SSCCCDDD` 錯誤碼。
|
||||
- 列表 API 必須使用 `page/pageSize` query,並在 `data` 回傳 `pagination/list`。
|
||||
- Job 狀態轉移必須使用 guarded/conditional update;不要在 API/worker 直接裸 `Update` 覆蓋 job 狀態。
|
||||
- Redis job lock 的 value 是 `workerID`;release / refresh 必須檢查 owner,長任務必須 heartbeat。
|
||||
- Auth 目前是 native email/password + JWT,不包含 OAuth / OTP / MFA / Zitadel。不要為了相容 template-monorepo 把重依賴搬進來。
|
||||
- AI provider token 與會員 JWT 是兩種不同 token;AI token 每次 request header 帶入,會員 JWT 由 `/api/v1/auth/*` 簽發。
|
||||
|
||||
## 設計文件
|
||||
|
||||
- `docs/job-system-plan.md`:通用 job system 規劃,包含 template、run、schedule、Redis queue/lock、取消語意與 API 草案。
|
||||
|
||||
## 新增 API 流程
|
||||
|
||||
1. 修改 `generate/api/*.api`。
|
||||
2. 優先使用 `make gen-api` 重新產生 handler/logic/types。
|
||||
3. 若手寫 handler,仍需遵守 `response.Write` 與 validator 流程。
|
||||
4. SSE endpoint 不使用 `response.Write`,直接輸出 `text/event-stream`。
|
||||
5. 更新 `README.md` 的 API 與架構說明。
|
||||
|
||||
## Response / Error Code
|
||||
|
||||
錯誤碼格式是 `SSCCCDDD`:
|
||||
|
||||
```text
|
||||
SS = scope
|
||||
CCC = category
|
||||
DDD = detail
|
||||
```
|
||||
|
||||
目前 scope:
|
||||
|
||||
```text
|
||||
10 = Facade
|
||||
32 = Setting
|
||||
33 = AI
|
||||
34 = Job
|
||||
35 = Auth
|
||||
36 = Member
|
||||
37 = Permission
|
||||
```
|
||||
|
||||
建立錯誤時使用:
|
||||
|
||||
```go
|
||||
errs.For(code.AI).InputMissingRequired("缺少 AI provider token")
|
||||
errs.For(code.Setting).ResNotFound("找不到設定")
|
||||
errs.For(code.Job).ResInvalidState("job state changed; update rejected")
|
||||
errs.For(code.Auth).AuthUnauthorized("missing bearer token")
|
||||
```
|
||||
|
||||
不要直接手寫 `33104000` 這種數字,也不要回傳裸 `error` 給 handler 後讓使用者看到內部錯誤。
|
||||
|
||||
## Pagination
|
||||
|
||||
列表 API 使用:
|
||||
|
||||
```text
|
||||
?page=1&pageSize=10
|
||||
```
|
||||
|
||||
response:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 102000,
|
||||
"message": "SUCCESS",
|
||||
"data": {
|
||||
"pagination": {
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"totalPages": 10
|
||||
},
|
||||
"list": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`page/pageSize` 必須是 server 正規化後的值。不要使用 `offset/limit/items`。
|
||||
|
||||
## 新增 Model 流程
|
||||
|
||||
模組放在:
|
||||
|
||||
```text
|
||||
internal/model/<module>/
|
||||
domain/entity
|
||||
domain/repository
|
||||
domain/usecase
|
||||
repository
|
||||
usecase
|
||||
```
|
||||
|
||||
依賴方向:
|
||||
|
||||
```text
|
||||
handler -> logic -> model/domain/usecase
|
||||
model/usecase -> model/domain/repository
|
||||
model/repository -> Mongo / Redis
|
||||
```
|
||||
|
||||
不要讓 `logic` import `model/<module>/repository`。
|
||||
|
||||
## Auth / Permission 擴充
|
||||
|
||||
目前已接:
|
||||
|
||||
```text
|
||||
POST /api/v1/auth/register
|
||||
POST /api/v1/auth/login
|
||||
POST /api/v1/auth/refresh
|
||||
POST /api/v1/auth/logout # requires member JWT
|
||||
GET /api/v1/members/me
|
||||
PATCH /api/v1/members/me
|
||||
GET /api/v1/permissions/catalog
|
||||
GET /api/v1/permissions/me
|
||||
```
|
||||
|
||||
Auth matrix(`internal/handler/routes.go`):
|
||||
|
||||
| 路由 | 需要會員 JWT |
|
||||
|------|----------------|
|
||||
| `GET /api/v1/health` | 否 |
|
||||
| `POST /api/v1/auth/register/login/refresh` | 否 |
|
||||
| `POST /api/v1/auth/logout` | 是(`Authorization`) |
|
||||
| `GET /api/v1/ai/providers` | 否 |
|
||||
| `POST /api/v1/ai/chat/stream/models` | 是(`X-Member-Authorization`)+ provider token(`Authorization`) |
|
||||
| `/api/v1/members/*`、`/api/v1/permissions/me` | 是(`Authorization`) |
|
||||
| `/api/v1/permissions/catalog`、`/api/v1/settings/*`、`/api/v1/jobs*`、`/api/v1/job/*` | 是(`Authorization`) |
|
||||
|
||||
規則:
|
||||
|
||||
- 保護路由用 `internal/middleware.Auth`(`Authorization: Bearer <access_token>`);AI 變更路由用 `middleware.MemberAuth`(`X-Member-Authorization`),因 `Authorization` 保留給 provider API key。
|
||||
- logic 從 `authctx.ActorFromContext` 讀 `tenant_id` / `uid`。
|
||||
- 不要在 handler 直接 parse JWT;token 驗證集中在 `model/auth/usecase`。
|
||||
- 密碼只存 bcrypt hash,不回傳、不寫 log。
|
||||
- `members.roles` 第一版是簡化 role key。正式 RBAC 可逐步補 roles collection,但不要破壞 `role_permissions` 的 tenant + role_key contract。
|
||||
- `Auth.DevHeaderFallback` 只給本機開發,正式環境應關閉。
|
||||
|
||||
## AI Provider 擴充
|
||||
|
||||
新增 provider 時:
|
||||
|
||||
1. 在 `internal/model/ai/domain/enum` 新增 provider id。
|
||||
2. 在 `internal/model/ai/provider` 新增 adapter。
|
||||
3. 在 `internal/model/ai/usecase` registry 註冊 provider 與 models。
|
||||
4. 確保 adapter 回傳統一 `StreamEvent`。
|
||||
5. 不要改 `logic/ai` 的 SSE 格式。
|
||||
|
||||
## Job Worker 擴充
|
||||
|
||||
新增 job step 時優先註冊 runner handler:
|
||||
|
||||
```go
|
||||
runner.RegisterStepHandler("analyze_8d", func(ctx context.Context, step job.StepContext) error {
|
||||
if err := step.Heartbeat(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// do work, check cancel via job usecase if needed
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
規則:
|
||||
|
||||
- Handler 不要直接操作 Mongo / Redis,透過 job usecase 更新進度、完成、失敗或取消。
|
||||
- 長任務每個 checkpoint 呼叫 `StepContext.Heartbeat` 或 `RefreshRunLock`。
|
||||
- 收到 cancel signal 後呼叫 `AcknowledgeCancel(jobId, workerID)`,不要自行把狀態改成 `cancelled`。
|
||||
- release lock 時必須帶 `workerID`;不要新增無 owner 的 release helper。
|
||||
|
||||
## 前端設計規則(`web/`)
|
||||
|
||||
巡樓 Console 前端在 `haixun-backend/web/`,風格參考 [simular.co](https://simular.co/):**明亮、年輕、圓角多、配色克制**。不要把舊 Next.js / `template-monorepo` UI 搬進來,也不要引入重型 UI 框架。
|
||||
|
||||
### 技術棧與指令
|
||||
|
||||
```text
|
||||
web/
|
||||
src/
|
||||
api/ # API client(envelope、JWT refresh)
|
||||
auth/ # AuthContext
|
||||
components/ # Layout、ui、ThemeToggle、AuthShell
|
||||
theme/ # ThemeContext(淺色 / 深色)
|
||||
pages/ # 路由頁面
|
||||
lib/ # jobStatus 等共用工具
|
||||
index.css # 設計 token 唯一來源
|
||||
```
|
||||
|
||||
```bash
|
||||
make web-dev # dev server :5173,proxy 到 :8890
|
||||
make web-build # tsc + vite build
|
||||
```
|
||||
|
||||
### 字型
|
||||
|
||||
| 語言 | 字型 | 載入方式 |
|
||||
|------|------|----------|
|
||||
| 繁體中文 | **台北黑體 Taipei Sans TC** | npm `taipei-sans-tc`,在 `index.css` `@import` Light / Regular / Bold |
|
||||
| 英文 | **Inter**(與 simular.co 相同,Google Fonts 免費) | `web/index.html` link |
|
||||
|
||||
規則:
|
||||
|
||||
- `body` / 中文標題:`Inter` + `Taipei Sans TC` 混排(`--font-sans`)。
|
||||
- 純英文裝飾字(導覽副標、Hero 小字):加 class `display-en`,使用 `--font-en`。
|
||||
- 中文 `line-height` 維持 **1.7+**;不要用過細字重當標題(標題用 `font-bold` / `font-black`)。
|
||||
- 只載入 Taipei Sans TC **Regular + Bold**,不要載入 Light,避免小字過細。
|
||||
- 不要改回 Noto Sans TC,也不要手寫 `#333` 這類裸色碼當主色。
|
||||
|
||||
### 對比度與字級
|
||||
|
||||
- 內文、表頭、表單 label、卡片說明:優先 `text-ink` / `text-ink-secondary`,**不要**拿 `text-muted` 當主要閱讀文字。
|
||||
- `text-muted` 只給次要提示(筆數、hint、placeholder 用 `text-subtle`)。
|
||||
- 表單輸入字級 **15px**(`text-[15px]`),輸入框底用 `bg-surface` 白底,確保與背景拉開。
|
||||
- 淺色 `muted` 約 `#5a6578`、深色約 `#b8c4d6`;改色時以「小字仍可舒適閱讀」為準,不要回到 `#94a3b8` 那種淡灰。
|
||||
|
||||
### 主題(淺色 / 深色)
|
||||
|
||||
- `ThemeProvider`(`src/theme/ThemeContext.tsx`)包住 App;偏好存 `localStorage` key:`haixun.theme`(`light` | `dark`)。
|
||||
- `index.html` 內嵌 script 在 React 載入前設定 `data-theme`,避免閃爍。
|
||||
- 所有顏色必須走 CSS 變數 `--hx-*`,再映射到 Tailwind `@theme`(`bg-canvas`、`text-brand` 等)。
|
||||
- 切換按鈕用 `ThemeToggle`;Layout 頂欄與 `AuthShell` 都要有。
|
||||
- **禁止**在元件裡寫死 `bg-slate-*`、`text-emerald-*`、`bg-amber-*` 等 Tailwind 預設色;語意狀態用 `text-success` / `text-warning` / `text-danger` 或 `jobStatus.ts` 的 badge class。
|
||||
|
||||
淺色預設明亮藍白底;深色為深藍黑底。兩套都只允許 **一個主色 brand(靛藍)** + success / warning / danger 語意色,不要再加褐色、墨綠、多種 accent 亂配。
|
||||
|
||||
### 色彩 token(語意命名)
|
||||
|
||||
開發時只用這些 Tailwind class(值定義在 `web/src/index.css`):
|
||||
|
||||
| Token | 用途 |
|
||||
|-------|------|
|
||||
| `canvas` | 全頁背景 |
|
||||
| `surface` / `surface-muted` | 卡片、輸入框底 |
|
||||
| `ink` / `ink-secondary` / `muted` | 主文 / 次文 / 輔助 |
|
||||
| `line` | 邊框 |
|
||||
| `brand` / `brand-hover` / `brand-soft` | 主 CTA、active 導覽、連結 hover |
|
||||
| `glow` | 裝飾色塊(`.glow-blob-alt`) |
|
||||
| `success` / `warning` / `danger`(含 `*-soft`) | 狀態、錯誤、Job badge |
|
||||
|
||||
主按鈕一律 `Button variant="primary"` → `bg-brand`,不要用全黑按鈕。
|
||||
|
||||
### 圓角與陰影
|
||||
|
||||
```text
|
||||
--radius-sm 0.75rem 小元素、code
|
||||
--radius-md 1.25rem Input / Textarea
|
||||
--radius-lg 1.75rem Card
|
||||
--radius-xl 2.25rem Hero、QuickLink、StatCard
|
||||
--radius-pill 9999px Button、Badge、導覽 pill
|
||||
```
|
||||
|
||||
陰影用 utility:`shadow-card`(一般卡片)、`shadow-soft`(主按鈕、Hero)。Hero 背景用 class `hero-panel`;裝飾 blob 用 `glow-blob` / `glow-blob-alt`。
|
||||
|
||||
### 共用元件(優先復用)
|
||||
|
||||
新頁面必須從 `src/components/ui.tsx` 組裝,不要另寫一套按鈕樣式:
|
||||
|
||||
| 元件 | 用途 |
|
||||
|------|------|
|
||||
| `PageTitle` | 頁面標題 + 副標 |
|
||||
| `Card` | 內容區塊 |
|
||||
| `Field` + `Input` / `Textarea` | 表單 |
|
||||
| `Button` | `primary` / `ghost` / `danger` / `soft` |
|
||||
| `Badge` | 標籤 pill(`brand` / `sky` / `success` / `warning` / `danger` / `neutral`) |
|
||||
| `StatCard` / `QuickLinkCard` | 總覽統計與快捷入口 |
|
||||
| `ErrorText` / `CopyableId` | 錯誤與可複製 ID |
|
||||
|
||||
`Button` 必須渲染 `{children}`;文案用**中文動詞**(例:「建立背景任務」「重新載入任務列表」),不要留空白小框。
|
||||
|
||||
### RWD(手機)
|
||||
|
||||
- `< lg`:隱藏左側欄;**底部固定導覽**最多 **4 格**(總覽 / 任務 / 排程 / **更多**),不要把漢堡或 ⋯ 選單放在左上角。
|
||||
- 「更多」以底部 sheet 展開:AI、模板、設定、會員、權限、主題切換、登出。
|
||||
- 主內容加 `layout-main` 底部 padding,避開 tab bar + `safe-area-inset-bottom`。
|
||||
- 寬表格包 `overflow-x-auto` + `min-w-*`,避免小螢幕擠爆版面。
|
||||
|
||||
### 版面與導覽
|
||||
|
||||
- 已登入(桌面):`Layout` = 左側欄 + 頂部 sticky bar(UID + `ThemeToggle`)+ `Outlet`。
|
||||
- 已登入(手機):頂欄品牌 + 主題切換;導覽走 `MobileBottomNav`。
|
||||
- 側欄分組:**工作區**(總覽、背景任務、排程、AI)、**管理**(模板、設定、會員、權限)。
|
||||
- Active 導覽:`bg-brand text-white shadow-soft`;hover:`bg-brand-soft text-brand`。
|
||||
- 未登入:`AuthShell` 置中卡片 + 右上主題切換;背景用柔和 blob,不要花俏插圖牆。
|
||||
- 語氣:年輕、直接、短句;Hero 可有一句主標 + brand 色強調詞,避免長篇企業八股。
|
||||
|
||||
### API 與狀態
|
||||
|
||||
- JSON 一律走 `api/client.ts`(`code/message/data` envelope);需登入加 `{ auth: true }`。
|
||||
- AI 路由用 `X-Member-Authorization`;provider token 用 `Authorization`(見後端 Auth matrix)。
|
||||
- Job 狀態中文與 badge 色:`src/lib/jobStatus.ts`(`jobStatusLabel` / `jobStatusBadgeClass`),列表有進行中任務時可每 3 秒 refresh。
|
||||
- 不要在前端 parse JWT;`uid` / `tenant_id` 從 `AuthContext` 讀。
|
||||
|
||||
### 新增頁面流程
|
||||
|
||||
1. 在 `App.tsx` 掛路由(需登入的放在 `Layout` 底下)。
|
||||
2. 頁面用 `PageTitle` + `Card` + 既有元件;色票只引用 semantic token。
|
||||
3. 若需新語意色,**先**改 `index.css` 的 `--hx-*` 與 `@theme`,再改元件;不要頁面內硬編色碼。
|
||||
4. 完成後執行 `make web-build`。
|
||||
|
||||
### 前端禁忌
|
||||
|
||||
- 不要引入 MUI / Ant Design / Chakra 等大型 UI 庫。
|
||||
- 不要為單頁新增第三套配色或漸層彩虹按鈕。
|
||||
- 不要讓 SSE / AI 直接吃 provider 原始 chunk(後端已 normalize)。
|
||||
- 不要用 `offset/limit` 呼叫列表 API;用 `page` / `pageSize`。
|
||||
|
||||
## 驗證
|
||||
|
||||
完成變更後至少執行:
|
||||
|
||||
```bash
|
||||
cd haixun-backend
|
||||
go mod tidy
|
||||
make fmt
|
||||
go test ./...
|
||||
```
|
||||
|
||||
有動到前端時另執行:
|
||||
|
||||
```bash
|
||||
make web-build
|
||||
```
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
GO ?= go
|
||||
GOFMT ?= gofmt
|
||||
GOCTL ?= goctl
|
||||
GO_ZERO_STYLE := go_zero
|
||||
API_ENTRY := ./generate/api/gateway.api
|
||||
GOFILES := $(shell find . -name '*.go')
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
help: ## 顯示可用指令
|
||||
@echo "Haixun Backend"
|
||||
@echo ""
|
||||
@grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf " make %-12s %s\n", $$1, $$2}'
|
||||
|
||||
tools: ## 安裝 goctl / goimports
|
||||
@command -v $(GOCTL) >/dev/null 2>&1 || (echo ">> installing goctl" && $(GO) install github.com/zeromicro/go-zero/tools/goctl@latest)
|
||||
@command -v goimports >/dev/null 2>&1 || (echo ">> installing goimports" && $(GO) install golang.org/x/tools/cmd/goimports@latest)
|
||||
|
||||
gen-api: tools ## 由 .api 生成 handler / logic / types
|
||||
$(GOCTL) api go -api $(API_ENTRY) -dir . -style $(GO_ZERO_STYLE) -home generate/goctl
|
||||
|
||||
fmt: ## gofmt + goimports
|
||||
$(GOFMT) -s -w $(GOFILES)
|
||||
@command -v goimports >/dev/null 2>&1 && goimports -w . || true
|
||||
|
||||
test: ## 執行測試
|
||||
$(GO) test ./...
|
||||
|
||||
run: ## 啟動 API
|
||||
$(GO) run ./gateway.go -f etc/gateway.yaml
|
||||
|
||||
CONFIG ?= etc/gateway.yaml
|
||||
INIT_TENANT ?= default
|
||||
INIT_EMAIL ?= admin@30cm.net
|
||||
INIT_PASSWORD ?= Fafafa54088!
|
||||
|
||||
tool-init: ## 初始化 Mongo indexes、預設權限與 admin 帳號
|
||||
$(GO) run ./cmd/tool init -f $(CONFIG) -tenant $(INIT_TENANT) -email $(INIT_EMAIL) -password '$(INIT_PASSWORD)'
|
||||
|
||||
tool: ## 執行 cmd/tool(例:make tool ARGS="init -f etc/gateway.yaml")
|
||||
$(GO) run ./cmd/tool $(ARGS)
|
||||
|
||||
web-install: ## 安裝前端依賴
|
||||
cd web && npm install
|
||||
|
||||
web-dev: web-install ## 啟動前端 dev server(proxy 到 :8890)
|
||||
cd web && npm run dev
|
||||
|
||||
web-build: web-install ## 建置前端靜態檔
|
||||
cd web && npm run build
|
||||
|
||||
check: fmt test ## 格式化並測試
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
# Haixun Backend
|
||||
|
||||
新的巡樓後端核心。這個資料夾刻意不直接複製 `template-monorepo` 的產物碼,只沿用它的架構模式、goctl handler template 概念與必要 runtime library,讓後續可以用更乾淨的邊界重建服務。
|
||||
|
||||
## 目前範圍
|
||||
|
||||
第一版先放六個核心能力:
|
||||
|
||||
- `setting`:通用設定模型,支援 `scope + scope_id + key` 儲存不同類型設定。
|
||||
- `ai`:可替換 AI provider interface,第一版支援 OpenCode Go 與 Grok/xAI,並提供 SSE 串流回應。
|
||||
- `job`:通用背景任務系統,支援 template/run/schedule/event、Redis queue/lock、進度、retry 與 cooperative cancel。
|
||||
- `auth`:native email/password 登入、JWT access/refresh token、logout revoke。
|
||||
- `member`:目前登入會員的 profile 讀寫。
|
||||
- `permission`:permission catalog 與目前會員權限查詢。
|
||||
|
||||
暫時不包含 template-monorepo 裡較重的 OAuth / OTP / MFA / Zitadel 整合,也不包含 notification、Playwright worker。這些之後要接時再按服務邊界新增。
|
||||
|
||||
## 快速開始
|
||||
|
||||
```bash
|
||||
cd haixun-backend
|
||||
go mod download
|
||||
make run
|
||||
```
|
||||
|
||||
預設服務:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8890
|
||||
```
|
||||
|
||||
健康檢查:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8890/api/v1/health
|
||||
```
|
||||
|
||||
## 專案結構
|
||||
|
||||
```text
|
||||
haixun-backend/
|
||||
gateway.go # go-zero server 入口
|
||||
Makefile # gen-api / fmt / test / run
|
||||
etc/ # runtime config
|
||||
generate/
|
||||
api/ # goctl .api 定義
|
||||
goctl/api/handler.tpl # 從 template-monorepo 精簡改來的 handler 模板
|
||||
internal/
|
||||
config/ # config struct
|
||||
handler/ # HTTP handler,目前手寫;之後可由 goctl 生成
|
||||
logic/ # API 編排層
|
||||
model/
|
||||
setting/ # 通用設定 model
|
||||
ai/ # AI provider interface + adapter
|
||||
job/ # Job template/run/schedule/event usecase + repository
|
||||
auth/ # JWT token issue/refresh/logout + Redis revoke store
|
||||
member/ # Native member profile + password hash
|
||||
permission/ # Permission catalog + role permission mapping
|
||||
worker/ # 常駐背景 worker / scheduler / reaper
|
||||
library/ # 最小 runtime library
|
||||
response/ # 統一 JSON response envelope
|
||||
svc/ # ServiceContext 組裝依賴
|
||||
types/ # API request/response types
|
||||
```
|
||||
|
||||
## 分層規則
|
||||
|
||||
## Response 與錯誤碼標準
|
||||
|
||||
所有一般 JSON API 都必須回傳同一層 envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 102000,
|
||||
"message": "SUCCESS",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
成功固定:
|
||||
|
||||
```text
|
||||
HTTP 200
|
||||
code = 102000
|
||||
message = SUCCESS
|
||||
```
|
||||
|
||||
失敗格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 33101000,
|
||||
"message": "缺少 AI provider token",
|
||||
"error": {
|
||||
"biz_code": "33101000",
|
||||
"scope": 33,
|
||||
"category": 104,
|
||||
"detail": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
錯誤碼採 `SSCCCDDD`:
|
||||
|
||||
```text
|
||||
SS = scope,服務或模組範圍
|
||||
CCC = category,錯誤分類
|
||||
DDD = detail,細分錯誤碼,未細分時為 000
|
||||
```
|
||||
|
||||
目前 scope:
|
||||
|
||||
```text
|
||||
10 = Facade / request parse / validation
|
||||
32 = Setting
|
||||
33 = AI
|
||||
34 = Job
|
||||
35 = Auth
|
||||
36 = Member
|
||||
37 = Permission
|
||||
```
|
||||
|
||||
常用 category:
|
||||
|
||||
```text
|
||||
101 = InputInvalidFormat
|
||||
104 = InputMissingRequired
|
||||
204 = DBUnavailable
|
||||
301 = ResourceNotFound
|
||||
303 = ResourceConflict
|
||||
401 = AuthUnauthorized
|
||||
505 = AuthForbidden
|
||||
601 = SystemInternal
|
||||
802 = ServiceThirdParty
|
||||
```
|
||||
|
||||
實作規則:
|
||||
|
||||
- Handler 成功/失敗都用 `internal/response.Write`,SSE endpoint 例外。
|
||||
- Request parse / validation 錯誤用 `response.WrapRequestError`,會落在 Facade scope。
|
||||
- Model/usecase 內建立錯誤時使用 `errors.For(code.<Scope>)` builder,不要手刻數字。
|
||||
- 不要把 provider 原始錯誤完整洩漏到前端;必要時只保留可排查的摘要。
|
||||
|
||||
### 分頁標準
|
||||
|
||||
列表型 API 的 query 使用 `page` / `pageSize`:
|
||||
|
||||
```text
|
||||
GET /api/v1/settings/user/user_123?page=1&pageSize=10
|
||||
```
|
||||
|
||||
回應的分頁資訊放在 `data.pagination`,資料陣列放在 `data.list`:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 102000,
|
||||
"message": "SUCCESS",
|
||||
"data": {
|
||||
"pagination": {
|
||||
"total": 42,
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"totalPages": 5
|
||||
},
|
||||
"list": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
規則:
|
||||
|
||||
- `page` 從 1 開始。
|
||||
- `pageSize <= 0` 時由 server 套用預設值。
|
||||
- `pageSize` 超過 server 上限時由 server 截斷。
|
||||
- `totalPages = ceil(total / pageSize)`。
|
||||
- response 內的 `page/pageSize` 必須回傳 server 正規化後的值。
|
||||
|
||||
### logic
|
||||
|
||||
`internal/logic/*` 只負責一次 API 請求的流程編排:
|
||||
|
||||
- 轉換 HTTP types 與 usecase DTO
|
||||
- 呼叫一個或多個 model usecase
|
||||
- 不直接操作 Mongo / Redis
|
||||
- 不放 provider HTTP 細節
|
||||
|
||||
### model
|
||||
|
||||
`internal/model/*` 放可重複使用的業務能力:
|
||||
|
||||
- `domain/entity`:資料結構
|
||||
- `domain/repository`:repository interface
|
||||
- `domain/usecase`:usecase interface 與 DTO
|
||||
- `repository`:Mongo / Redis 實作
|
||||
- `usecase`:業務能力實作
|
||||
|
||||
### provider
|
||||
|
||||
`internal/model/ai/provider` 只負責外部 AI API adapter:
|
||||
|
||||
- 不讀 setting
|
||||
- 不碰 HTTP handler
|
||||
- 不存 token
|
||||
- token 每次由 request 帶入
|
||||
|
||||
## Setting Model
|
||||
|
||||
設定使用 typed setting 形式:
|
||||
|
||||
```json
|
||||
{
|
||||
"scope": "user",
|
||||
"scope_id": "user_123",
|
||||
"key": "ai.default",
|
||||
"value": {
|
||||
"provider": "opencode-go",
|
||||
"model": "deepseek-v4-pro",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2000
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
```text
|
||||
GET /api/v1/settings/:scope/:scope_id?page=1&pageSize=10
|
||||
GET /api/v1/settings/:scope/:scope_id/:key
|
||||
PUT /api/v1/settings/:scope/:scope_id/:key
|
||||
DELETE /api/v1/settings/:scope/:scope_id/:key
|
||||
```
|
||||
|
||||
`setting` model 不知道 AI、Threads、crawler 等業務含義。各業務 model 自己解讀對應 key 的 value。
|
||||
|
||||
## Auth / Member / Permission
|
||||
|
||||
這版從 `template-monorepo` 精簡搬入會員、權限與 token 的核心概念,但不搬 OAuth / OTP / MFA / Zitadel 依賴。
|
||||
|
||||
Auth 採 native email/password:
|
||||
|
||||
```text
|
||||
POST /api/v1/auth/register
|
||||
POST /api/v1/auth/login
|
||||
POST /api/v1/auth/refresh
|
||||
POST /api/v1/auth/logout
|
||||
```
|
||||
|
||||
`register` / `login` 回傳:
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "...",
|
||||
"refresh_token": "...",
|
||||
"expires_in": 900,
|
||||
"uid": "user_uid",
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
保護路由使用:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
本機開發可以開啟 `Auth.DevHeaderFallback`,用 header 模擬登入:
|
||||
|
||||
```http
|
||||
X-Tenant-ID: default
|
||||
X-UID: user_uid
|
||||
```
|
||||
|
||||
Member API:
|
||||
|
||||
```text
|
||||
GET /api/v1/members/me
|
||||
PATCH /api/v1/members/me
|
||||
```
|
||||
|
||||
Permission API:
|
||||
|
||||
```text
|
||||
GET /api/v1/permissions/catalog?tree=true
|
||||
GET /api/v1/permissions/me?include_tree=true
|
||||
```
|
||||
|
||||
資料模型:
|
||||
|
||||
- `members`:tenant-scoped profile、email、bcrypt password hash、roles。
|
||||
- `permissions`:平台 permission catalog。
|
||||
- `role_permissions`:tenant + role_key 對 permission catalog 的綁定。
|
||||
- Redis `auth:jwt:*`:access/refresh pair 與 blacklist。Redis 未配置時仍可簽發 token,但 refresh/logout revoke 不會持久化。
|
||||
|
||||
## AI Provider
|
||||
|
||||
AI token 不存在 config,呼叫時每次帶入,且**只放 HTTP header**,不要放 JSON body(避免 log / 回應洩漏):
|
||||
|
||||
```http
|
||||
Authorization: Bearer sk-...
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"provider": "opencode-go",
|
||||
"model": "deepseek-v4-pro",
|
||||
"messages": [
|
||||
{ "role": "user", "content": "請幫我寫一段文案" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
```text
|
||||
GET /api/v1/ai/providers
|
||||
POST /api/v1/ai/providers/:provider/models
|
||||
POST /api/v1/ai/chat
|
||||
POST /api/v1/ai/chat/stream
|
||||
```
|
||||
|
||||
- `GET /providers`:只回傳 catalog(id、label、streams),不含 models、不含 token。
|
||||
- `POST /providers/:provider/models`:向 provider 的 `/models` 動態拉清單,需帶 `Authorization: Bearer <token>`。
|
||||
- 回應與錯誤訊息不會 echo token;provider 原始錯誤 body 也不會直接回傳給前端。
|
||||
|
||||
串流 endpoint 使用 SSE:
|
||||
|
||||
```text
|
||||
event: delta
|
||||
data: {"type":"delta","text":"..."}
|
||||
|
||||
event: done
|
||||
data: {"type":"done","finish_reason":"stop"}
|
||||
```
|
||||
|
||||
## Job System
|
||||
|
||||
Job 系統的詳細設計在 `docs/job-system-plan.md`。目前 runtime 原則:
|
||||
|
||||
- MongoDB 的 `job_runs` 是狀態真相來源;claim、cancel、complete、fail、retry 必須使用 conditional update,避免 worker 與 API 互相覆蓋狀態。
|
||||
- Redis `jobs:lock:<jobId>` 的 value 是 `workerID`;release / refresh 必須檢查 owner,只能由持有 lock 的 worker 操作。
|
||||
- Worker 執行長任務時要定期呼叫 `RefreshRunLock(jobId, workerID, ttlSeconds)`,避免 reaper 誤判過期。
|
||||
- Runner 支援 `RegisterStepHandler(stepID, handler)` 註冊自訂 step handler;未註冊時會走 demo handler。自訂 handler 可用 `StepContext.Heartbeat` 續約 lock。
|
||||
- 取消採 cooperative cancellation:API 先寫 `cancel_requested` 與 Redis cancel signal,worker checkpoint 讀取後呼叫 `AcknowledgeCancel(jobId, workerID)`。
|
||||
|
||||
## OpenCode Go 注意事項
|
||||
|
||||
第一版 OpenCode Go 先走 OpenAI-compatible `/chat/completions`:
|
||||
|
||||
```text
|
||||
https://opencode.ai/zen/go/v1/chat/completions
|
||||
```
|
||||
|
||||
目前已處理 Kimi 模型 `temperature = 1` 的特殊規則。部分 OpenCode Go 模型官方文件標示為 Anthropic-compatible `/messages`,後續可在 `internal/model/ai/provider` 新增 messages adapter,不需要改 logic 或前端 SSE contract。
|
||||
|
||||
## 下一步建議
|
||||
|
||||
1. 用 `goctl` 重新生成 handler / logic / types,確認 `.api` 與手寫版本對齊。
|
||||
2. 補 `setting` repository 測試與 Mongo integration 測試。
|
||||
3. 補 AI provider mock,讓 `logic/ai` 不需要真的打 provider 也能測。
|
||||
4. 新增 credential service 或 Vault/KMS 整合,但不要把 token 放進 provider config。
|
||||
5. 新增 worker/job model,讓 Go worker 與 Node Playwright worker 共用同一套 job contract。
|
||||
|
||||
## 設計文件
|
||||
|
||||
- [Job 核心系統規劃](docs/job-system-plan.md):通用 job template、run、schedule、事件、取消語意與 worker contract。
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"haixun-backend/internal/bootstrap"
|
||||
"haixun-backend/internal/config"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "init":
|
||||
if err := runInit(os.Args[2:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[tool] error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "[tool] unknown command: %s\n", os.Args[1])
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runInit(args []string) error {
|
||||
fs := flag.NewFlagSet("init", flag.ExitOnError)
|
||||
configFile := fs.String("f", "etc/gateway.yaml", "config file")
|
||||
tenantID := fs.String("tenant", envOr("INIT_TENANT_ID", "default"), "tenant id for admin and role permissions")
|
||||
email := fs.String("email", envOr("INIT_ADMIN_EMAIL", "admin@haixun.local"), "bootstrap admin email")
|
||||
password := fs.String("password", envOr("INIT_ADMIN_PASSWORD", "Admin-Pass-1!"), "bootstrap admin password")
|
||||
displayName := fs.String("display-name", envOr("INIT_ADMIN_DISPLAY_NAME", "Admin"), "bootstrap admin display name")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cfg config.Config
|
||||
conf.MustLoad(*configFile, &cfg)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
report, err := bootstrap.Init(ctx, cfg, bootstrap.InitOptions{
|
||||
TenantID: strings.TrimSpace(*tenantID),
|
||||
AdminEmail: strings.TrimSpace(*email),
|
||||
AdminPass: *password,
|
||||
DisplayName: strings.TrimSpace(*displayName),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[tool] indexes ensured\n")
|
||||
fmt.Fprintf(os.Stderr, "[tool] permissions catalog seeded\n")
|
||||
fmt.Fprintf(os.Stderr, "[tool] role_permissions seeded (admin=all, user=default)\n")
|
||||
if report.AdminCreated {
|
||||
fmt.Fprintf(os.Stderr, "[tool] admin created uid=%s email=%s tenant=%s\n", report.AdminUID, *email, *tenantID)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "[tool] admin exists uid=%s email=%s tenant=%s (roles ensured admin)\n", report.AdminUID, *email, *tenantID)
|
||||
}
|
||||
fmt.Printf("export INIT_TENANT_ID=%s\n", *tenantID)
|
||||
fmt.Printf("export INIT_ADMIN_EMAIL=%s\n", *email)
|
||||
fmt.Printf("export INIT_ADMIN_PASSWORD=%s\n", *password)
|
||||
fmt.Printf("export INIT_ADMIN_UID=%s\n", report.AdminUID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprintf(os.Stderr, "usage:\n")
|
||||
fmt.Fprintf(os.Stderr, " tool init [-f etc/gateway.yaml] [-tenant default] [-email admin@haixun.local] [-password ...]\n")
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# 本機依賴(Docker Compose)
|
||||
|
||||
Gateway 啟用 **Notification** / **Member OTP** 需要:
|
||||
|
||||
| 服務 | 用途 | 預設埠 |
|
||||
|------|------|--------|
|
||||
| **MongoDB** | `notifications`、`notification_dlq` collections | 27017 |
|
||||
| **Redis** | 冪等、配額、異步重試佇列、member OTP challenge | 6379 |
|
||||
| MailHog(選用) | 本機 SMTP 測試 | 1025 / 8025 |
|
||||
| OpenLDAP(`make ldap-up` / `make k6-up`) | ZITADEL LDAP IdP 本機目錄 | 389 |
|
||||
| ZITADEL(`make k6-up`) | OIDC / Social / LDAP 登入 | 8080 |
|
||||
|
||||
Mongo **不需要**事先手動建 collection;應用程式寫入時會自動建立。索引由 init script 或 `make mongo-index` 建立。
|
||||
|
||||
## 快速開始
|
||||
|
||||
```bash
|
||||
# 1. 啟動 Mongo + Redis
|
||||
make deps-up
|
||||
|
||||
# 2.(選用)含 MailHog
|
||||
make deps-up-smtp
|
||||
|
||||
# 3. 確認索引(首次 docker volume 通常已由 init 建立;可再跑一次保險)
|
||||
make mongo-index
|
||||
|
||||
# 4. 啟動 Gateway(使用 etc/gateway.dev.yaml)
|
||||
make run-dev
|
||||
```
|
||||
|
||||
## Mongo collections
|
||||
|
||||
| Collection | 模組 | 說明 |
|
||||
|------------|------|------|
|
||||
| `notifications` | notification | 發送紀錄、冪等 |
|
||||
| `notification_dlq` | notification | 超過 MaxRetry 的死信 |
|
||||
|
||||
索引定義見 [`deploy/mongo/init/01-gateway-indexes.js`](mongo/init/01-gateway-indexes.js),與 Go 的 `Index20260520001UP` 一致。
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
make deps-up # docker compose up -d mongo redis
|
||||
make deps-up-smtp # 再加上 mailhog(profile smtp)
|
||||
make ldap-up # 只起 OpenLDAP(profile ldap)
|
||||
make k6-up # 全棧含 OpenLDAP + ZITADEL(見 deploy/zitadel、deploy/openldap README)
|
||||
make ldap-test # 確認 LDAP 測試帳號 alice/bob
|
||||
make deps-down # 停止並移除容器(保留 volume)
|
||||
make deps-down-v # 停止並刪除 volume(會清掉 Mongo 資料)
|
||||
make deps-logs # 查看 log
|
||||
make mongo-index # 手動建立/補齊索引
|
||||
```
|
||||
|
||||
LDAP 本機測試:[deploy/openldap/README.md](openldap/README.md)
|
||||
|
||||
## 連線設定
|
||||
|
||||
設定說明:[`etc/README.md`](../etc/README.md)
|
||||
|
||||
| 檔案 | 用途 |
|
||||
|------|------|
|
||||
| [`etc/gateway.yaml`](../etc/gateway.yaml) | 預設,無需 Docker |
|
||||
| [`etc/gateway.dev.example.yaml`](../etc/gateway.dev.example.yaml) | 範例(可提交) |
|
||||
| `etc/gateway.dev.yaml` | 本機專用(**勿提交**,見 `.gitignore`) |
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
services:
|
||||
mongo:
|
||||
image: mongo:7
|
||||
container_name: gateway-mongo
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
MONGO_INITDB_DATABASE: gateway
|
||||
volumes:
|
||||
- mongo_data:/data/db
|
||||
- ./mongo/init:/docker-entrypoint-initdb.d:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: gateway-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
mongo_data:
|
||||
redis_data:
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Gateway MongoDB 初始化(僅在 data volume 首次建立時執行)
|
||||
// 與 internal/model/notification/repository/* Index20260520001UP 對齊
|
||||
// 既有 volume 請執行:make mongo-index
|
||||
|
||||
db = db.getSiblingDB('gateway');
|
||||
|
||||
print('Creating indexes on notifications...');
|
||||
|
||||
db.notifications.createIndex(
|
||||
{ tenant_id: 1, kind: 1, idempotency_key: 1 },
|
||||
{ unique: true, name: 'idx_notifications_tenant_kind_idempotency' }
|
||||
);
|
||||
|
||||
db.notifications.createIndex(
|
||||
{ tenant_id: 1, uid: 1, occurred_at: -1 },
|
||||
{ name: 'idx_notifications_tenant_uid_occurred' }
|
||||
);
|
||||
|
||||
db.notifications.createIndex(
|
||||
{ status: 1, attempts: 1, occurred_at: 1 },
|
||||
{ name: 'idx_notifications_status_attempts_occurred' }
|
||||
);
|
||||
|
||||
print('Creating indexes on notification_dlq...');
|
||||
|
||||
db.notification_dlq.createIndex(
|
||||
{ tenant_id: 1, occurred_at: -1 },
|
||||
{ name: 'idx_notification_dlq_tenant_occurred' }
|
||||
);
|
||||
|
||||
print('Gateway Mongo init done.');
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
# Job 核心系統規劃
|
||||
|
||||
## 目標
|
||||
|
||||
建立一套通用 job system,讓任何長任務、流程任務、定時任務未來都能共用。Job 不只是背景任務,而是「有模板、有設定、有狀態、有進度、有取消能力、有重跑策略、有排程能力」的工作單元。
|
||||
|
||||
## 核心設計
|
||||
|
||||
採用:
|
||||
|
||||
```text
|
||||
Mongo = job/template/run/history 的真相來源
|
||||
Redis = queue、distributed lock、schedule tick、短期 lease
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Api[GoAPI] --> Template[JobTemplate]
|
||||
Api --> JobRun[JobRunMongo]
|
||||
JobRun --> RedisQueue[RedisQueue]
|
||||
Scheduler[SchedulerTick] --> RedisQueue
|
||||
Worker[Worker] --> RedisQueue
|
||||
Worker --> JobRun
|
||||
Worker --> Step[JobStep]
|
||||
```
|
||||
|
||||
## 核心概念
|
||||
|
||||
### JobTemplate
|
||||
|
||||
Template 定義「這種 job 要怎麼做」。例如:
|
||||
|
||||
```text
|
||||
demo_long_task
|
||||
external_worker_task
|
||||
scheduled_report
|
||||
multi_step_pipeline
|
||||
```
|
||||
|
||||
Template 要回答:
|
||||
|
||||
- 這個 job 的輸入 payload schema 是什麼
|
||||
- 有哪些 steps
|
||||
- 最終狀態是什麼
|
||||
- 可不可以重複執行
|
||||
- 是否允許同 account / 同 target 同時跑
|
||||
- retry policy 是什麼
|
||||
- timeout 是多少
|
||||
- 是否可被排程
|
||||
- 是否支援取消,以及取消時 worker 要如何收斂
|
||||
|
||||
### JobRun
|
||||
|
||||
JobRun 是每一次執行實例。它引用 template,保存當次 payload、狀態、進度、結果、錯誤與執行歷史。
|
||||
|
||||
### JobSchedule
|
||||
|
||||
JobSchedule 是「何時建立 JobRun」。支援:
|
||||
|
||||
- cron
|
||||
- enabled / disabled
|
||||
- timezone
|
||||
- payload template
|
||||
- target scope,例如 user/account/system
|
||||
- nextRunAt / lastRunAt
|
||||
|
||||
### JobFlow
|
||||
|
||||
Flow 是多步驟流程。第一版不用做完整 DAG,先支援線性 steps:
|
||||
|
||||
```text
|
||||
multi_step_pipeline:
|
||||
1. prepare
|
||||
2. execute
|
||||
3. finalize
|
||||
```
|
||||
|
||||
之後再擴成 DAG 或 conditional branch。
|
||||
|
||||
## Mongo Collections
|
||||
|
||||
### `job_templates`
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "...",
|
||||
"type": "demo_long_task",
|
||||
"version": 1,
|
||||
"name": "示範長任務",
|
||||
"description": "展示 job template、進度、取消、重跑與排程能力",
|
||||
"enabled": true,
|
||||
"repeatable": true,
|
||||
"concurrencyPolicy": "reject_same_scope",
|
||||
"dedupeKeys": ["scope_id", "target"],
|
||||
"timeoutSeconds": 600,
|
||||
"cancelPolicy": {
|
||||
"supported": true,
|
||||
"mode": "cooperative",
|
||||
"graceSeconds": 30
|
||||
},
|
||||
"retryPolicy": {
|
||||
"maxAttempts": 2,
|
||||
"backoffSeconds": [30, 120]
|
||||
},
|
||||
"steps": [
|
||||
{ "id": "prepare", "name": "準備資料", "workerType": "go", "timeoutSeconds": 60, "cancelable": true },
|
||||
{ "id": "execute", "name": "執行任務", "workerType": "go", "timeoutSeconds": 300, "cancelable": true },
|
||||
{ "id": "finalize", "name": "整理結果", "workerType": "go", "timeoutSeconds": 30, "cancelable": false }
|
||||
],
|
||||
"createAt": 0,
|
||||
"updateAt": 0
|
||||
}
|
||||
```
|
||||
|
||||
### `job_runs`
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "...",
|
||||
"templateType": "demo_long_task",
|
||||
"templateVersion": 1,
|
||||
"scope": "user",
|
||||
"scopeId": "user_123",
|
||||
"status": "pending",
|
||||
"phase": "prepare",
|
||||
"payload": {},
|
||||
"progress": {
|
||||
"summary": "等待 worker 執行",
|
||||
"percentage": 20,
|
||||
"steps": []
|
||||
},
|
||||
"result": null,
|
||||
"error": null,
|
||||
"attempt": 0,
|
||||
"maxAttempts": 2,
|
||||
"lockedBy": null,
|
||||
"lockedUntil": null,
|
||||
"cancelRequestedAt": null,
|
||||
"cancelReason": null,
|
||||
"scheduledAt": null,
|
||||
"startedAt": null,
|
||||
"completedAt": null,
|
||||
"createAt": 0,
|
||||
"updateAt": 0
|
||||
}
|
||||
```
|
||||
|
||||
### `job_schedules`
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "...",
|
||||
"templateType": "demo_long_task",
|
||||
"scope": "user",
|
||||
"scopeId": "user_123",
|
||||
"enabled": true,
|
||||
"cron": "0 9 * * *",
|
||||
"timezone": "Asia/Taipei",
|
||||
"payloadTemplate": {},
|
||||
"lastRunAt": null,
|
||||
"nextRunAt": 0,
|
||||
"createAt": 0,
|
||||
"updateAt": 0
|
||||
}
|
||||
```
|
||||
|
||||
### `job_events`
|
||||
|
||||
用來觀察與 audit:
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "...",
|
||||
"jobId": "...",
|
||||
"type": "status_changed",
|
||||
"from": "pending",
|
||||
"to": "running",
|
||||
"message": "worker claimed job",
|
||||
"metadata": {},
|
||||
"createAt": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Status Model
|
||||
|
||||
```text
|
||||
pending = 已建立,等待 queue
|
||||
queued = 已推進 Redis queue
|
||||
running = worker 執行中
|
||||
waiting_worker = 等外部 worker 回寫
|
||||
cancel_requested = 使用者已要求取消,等待 worker cooperative stop
|
||||
succeeded = 成功完成
|
||||
failed = 最終失敗
|
||||
cancelled = 使用者取消
|
||||
expired = lock/timeout 過期後無法恢復
|
||||
```
|
||||
|
||||
Step status:
|
||||
|
||||
```text
|
||||
pending | running | succeeded | failed | skipped | cancelled
|
||||
```
|
||||
|
||||
## 取消語意
|
||||
|
||||
取消是第一版必做能力,採 cooperative cancellation:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
User[User] --> ApiCancel[CancelAPI]
|
||||
ApiCancel --> Run[JobRun cancel_requested]
|
||||
Run --> RedisCancel[RedisCancelSignal]
|
||||
Worker[Worker] -->|"poll cancel flag"| Run
|
||||
Worker --> Stop[StopCurrentStep]
|
||||
Stop --> Final[JobRun cancelled]
|
||||
```
|
||||
|
||||
規則:
|
||||
|
||||
- `pending` / `queued`:取消後直接變 `cancelled`,並盡量從 Redis queue 移除;若無法移除,worker claim 時必須檢查狀態並跳過。
|
||||
- `running`:狀態改為 `cancel_requested`,寫入 `cancelRequestedAt` / `cancelReason`,worker 必須在 step 間或長任務 checkpoint 檢查取消旗標。
|
||||
- `waiting_worker`:狀態改為 `cancel_requested`,同時寫 Redis cancel signal;外部 worker 回寫前要檢查 job 狀態。
|
||||
- `succeeded` / `failed` / `cancelled` / `expired`:不可取消,回傳 ResourceInvalidState。
|
||||
- worker 收到取消後呼叫 `AcknowledgeCancel(jobId, workerId)`,釋放 lock,寫入 `job_events`,狀態變 `cancelled`。
|
||||
- 若 `cancel_requested` 超過 template 的 `cancelPolicy.graceSeconds`,scheduler/reaper 可標記為 `cancelled` 或 `expired`,第一版建議標記 `cancelled` 並記錄 timeout event。
|
||||
|
||||
## 狀態與 Lock 安全規則
|
||||
|
||||
第一版已把最容易出問題的 race condition 收斂在 repository / usecase:
|
||||
|
||||
- `ClaimNext` 只能從 `pending` / `queued` conditional update 成 `running`。如果 API 同時取消,Mongo update 會被拒絕。
|
||||
- `RequestCancel` 只能從 cancellable 狀態 conditional update;`pending` / `queued` 直接變 `cancelled`,`running` / `waiting_worker` 變 `cancel_requested`。
|
||||
- `CompleteRun` / `FailRun` / `UpdateProgress` 必須帶 `workerID`,並且只能更新 `lockedBy == workerID` 的 job。
|
||||
- Redis `jobs:lock:<jobId>` 的 value 是 `workerID`;`ReleaseLock` / `RefreshLock` 使用 owner check,避免舊 worker 誤刪新 worker 的 lock。
|
||||
- Worker 長任務要定期 heartbeat,呼叫 `RefreshRunLock(jobId, workerID, ttlSeconds)`。自訂 step handler 可用 `StepContext.Heartbeat`。
|
||||
|
||||
之後新增狀態轉移時,不要直接使用裸 `Update`;若是生命週期狀態,應新增明確的 guarded repository 方法或使用現有 conditional update。
|
||||
|
||||
## Redis Keys
|
||||
|
||||
```text
|
||||
jobs:queue:<workerType> # list 或 stream,worker 消費
|
||||
jobs:lock:<jobId> # lease lock
|
||||
jobs:scheduler:lock # scheduler singleton lock
|
||||
jobs:dedupe:<template>:<hash> # 防止同 scope 重複跑
|
||||
jobs:cancel:<jobId> # cancel signal,worker checkpoint 讀取
|
||||
```
|
||||
|
||||
第一版建議用 Redis List 即可,之後需要 ack/replay 再升級 Redis Streams。
|
||||
|
||||
## API 規劃
|
||||
|
||||
```text
|
||||
GET /api/v1/job/templates
|
||||
GET /api/v1/job/templates/:type
|
||||
PUT /api/v1/job/templates/:type
|
||||
|
||||
POST /api/v1/jobs
|
||||
GET /api/v1/jobs/:id
|
||||
GET /api/v1/jobs?page=1&pageSize=20
|
||||
POST /api/v1/jobs/:id/cancel
|
||||
POST /api/v1/jobs/:id/retry
|
||||
|
||||
GET /api/v1/job/schedules?page=1&pageSize=20
|
||||
POST /api/v1/job/schedules
|
||||
PUT /api/v1/job/schedules/:id
|
||||
POST /api/v1/job/schedules/:id/enable
|
||||
POST /api/v1/job/schedules/:id/disable
|
||||
```
|
||||
|
||||
所有列表回應使用目前標準:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 102000,
|
||||
"message": "SUCCESS",
|
||||
"data": {
|
||||
"pagination": {
|
||||
"total": 42,
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"totalPages": 5
|
||||
},
|
||||
"list": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 分層規劃
|
||||
|
||||
```text
|
||||
internal/model/job/
|
||||
domain/entity/template.go
|
||||
domain/entity/run.go
|
||||
domain/entity/schedule.go
|
||||
domain/entity/event.go
|
||||
domain/enum/status.go
|
||||
domain/repository/*.go
|
||||
domain/usecase/*.go
|
||||
repository/mongo_*.go
|
||||
repository/redis_queue.go
|
||||
usecase/template_usecase.go
|
||||
usecase/run_usecase.go
|
||||
usecase/schedule_usecase.go
|
||||
usecase/progress_usecase.go
|
||||
usecase/worker_usecase.go
|
||||
|
||||
internal/logic/job/
|
||||
create_job_logic.go
|
||||
get_job_logic.go
|
||||
list_jobs_logic.go
|
||||
cancel_job_logic.go
|
||||
retry_job_logic.go
|
||||
template_logic.go
|
||||
schedule_logic.go
|
||||
|
||||
internal/worker/job/
|
||||
runner.go
|
||||
scheduler.go
|
||||
dispatcher.go
|
||||
```
|
||||
|
||||
## Template 執行規則
|
||||
|
||||
### 可不可以重複做
|
||||
|
||||
由 `repeatable` 與 `concurrencyPolicy` 控制:
|
||||
|
||||
```text
|
||||
repeatable=false
|
||||
同 dedupe key 完成過就不再建立
|
||||
|
||||
repeatable=true + reject_same_scope
|
||||
已有 running/pending job 時拒絕建立
|
||||
|
||||
repeatable=true + allow_parallel
|
||||
允許平行跑
|
||||
|
||||
repeatable=true + replace_existing
|
||||
取消舊 job,建立新 job
|
||||
```
|
||||
|
||||
### 最終狀態
|
||||
|
||||
Template 可定義成功條件:
|
||||
|
||||
```text
|
||||
successWhen = all_steps_succeeded
|
||||
```
|
||||
|
||||
第一版只支援 `all_steps_succeeded`。之後再加 `any_step_succeeded` 或 conditional flow。
|
||||
|
||||
### 取消能力
|
||||
|
||||
Template 用 `cancelPolicy` 控制取消:
|
||||
|
||||
```text
|
||||
supported=false
|
||||
API 不允許取消此 job
|
||||
|
||||
mode=cooperative
|
||||
worker checkpoint 檢查取消旗標後收斂
|
||||
|
||||
graceSeconds=30
|
||||
cancel_requested 超過 grace 後由 reaper 收斂
|
||||
```
|
||||
|
||||
Step 用 `cancelable` 控制目前步驟是否可立即停止。若目前 step 不可取消,worker 需要在 step 結束後停止後續 steps,最後狀態仍為 `cancelled`。
|
||||
|
||||
## 第一版建議實作順序
|
||||
|
||||
1. 建 `model/job` 的 enum/entity/repository interface。
|
||||
2. 實作 Mongo repositories:template/run/schedule/event。
|
||||
3. 實作 Redis queue/lock repository。
|
||||
4. 實作 `CreateRun`:讀 template、檢查 repeat/concurrency、建立 run、push queue。
|
||||
5. 實作 `ClaimNext`:worker 從 Redis 取 job,Mongo 設 lock/status。
|
||||
6. 實作 `RequestCancel`:狀態轉 `cancel_requested` 或直接 `cancelled`,寫 Redis cancel signal 與 event。
|
||||
7. 實作 `AcknowledgeCancel`:worker 收斂後釋放 lock,狀態轉 `cancelled`。
|
||||
8. 實作 `UpdateProgress`、`Complete`、`Fail`、`Retry`。
|
||||
9. 實作 schedule tick:掃 `job_schedules.nextRunAt <= now`,建立 JobRun。
|
||||
10. 實作 API 與文件。
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
Name: haixun-backend
|
||||
Host: 0.0.0.0
|
||||
Port: 8890
|
||||
Timeout: 120000
|
||||
|
||||
Mongo:
|
||||
URI: mongodb://127.0.0.1:27017
|
||||
Database: haixun_dev
|
||||
TimeoutSeconds: 10
|
||||
|
||||
Redis:
|
||||
Addr: 127.0.0.1:6379
|
||||
DB: 0
|
||||
|
||||
Auth:
|
||||
AccessSecret: haixun-dev-access-secret-change-me
|
||||
RefreshSecret: haixun-dev-refresh-secret-change-me
|
||||
AccessExpireSeconds: 900
|
||||
RefreshExpireSeconds: 2592000
|
||||
DevHeaderFallback: true
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
Name: haixun-backend
|
||||
Host: 0.0.0.0
|
||||
Port: 8890
|
||||
Timeout: 120000
|
||||
|
||||
Mongo:
|
||||
URI: mongodb://127.0.0.1:27017
|
||||
Database: haixun
|
||||
TimeoutSeconds: 10
|
||||
|
||||
Redis:
|
||||
Addr: 127.0.0.1:6379
|
||||
DB: 0
|
||||
|
||||
Auth:
|
||||
AccessSecret: haixun-dev-access-secret-change-me
|
||||
RefreshSecret: haixun-dev-refresh-secret-change-me
|
||||
AccessExpireSeconds: 900
|
||||
RefreshExpireSeconds: 2592000
|
||||
DevHeaderFallback: true
|
||||
|
||||
JobWorker:
|
||||
Enabled: true
|
||||
WorkerType: go
|
||||
|
||||
JobScheduler:
|
||||
Enabled: true
|
||||
IntervalSeconds: 60
|
||||
|
||||
JobReaper:
|
||||
Enabled: true
|
||||
IntervalSeconds: 30
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"haixun-backend/internal/config"
|
||||
"haixun-backend/internal/handler"
|
||||
"haixun-backend/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
"github.com/zeromicro/go-zero/rest"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/gateway.yaml", "config file")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
|
||||
server := rest.MustNewServer(c.RestConf)
|
||||
defer server.Stop()
|
||||
|
||||
sc := svc.NewServiceContext(c)
|
||||
defer sc.Close(context.Background())
|
||||
|
||||
handler.RegisterHandlers(server, sc)
|
||||
|
||||
fmt.Printf("Starting backend backend at %s:%d...\n", c.Host, c.Port)
|
||||
server.Start()
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
syntax = "v1"
|
||||
|
||||
type (
|
||||
AIMessage {
|
||||
Role string `json:"role" validate:"required,oneof=system user assistant"` // 訊息角色
|
||||
Content string `json:"content" validate:"required"` // 訊息內容
|
||||
}
|
||||
AIChatReq {
|
||||
Provider string `json:"provider" validate:"required,oneof=opencode-go xai"` // AI provider
|
||||
Model string `json:"model" validate:"required"` // 模型 ID
|
||||
System string `json:"system,optional"` // system prompt
|
||||
Messages []AIMessage `json:"messages" validate:"required,min=1,dive"` // 對話訊息
|
||||
Temperature *float64 `json:"temperature,optional"` // 溫度
|
||||
MaxTokens *int `json:"max_tokens,optional"` // 最大輸出 token
|
||||
}
|
||||
AIProviderOption {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Streams bool `json:"streams"`
|
||||
}
|
||||
AIProvidersData {
|
||||
Providers []AIProviderOption `json:"providers"`
|
||||
}
|
||||
AIProviderPath {
|
||||
Provider string `path:"provider" validate:"required,oneof=opencode-go xai"` // 要查模型的 provider
|
||||
}
|
||||
AIProviderModelsData {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Models []string `json:"models"`
|
||||
Streams bool `json:"streams"`
|
||||
Error string `json:"error,optional"` // 拉 models 失敗時的摘要
|
||||
}
|
||||
AIChatData {
|
||||
Text string `json:"text"`
|
||||
FinishReason string `json:"finish_reason,optional"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
group: ai
|
||||
prefix: /api/v1/ai
|
||||
tags: "AI - Provider Streaming"
|
||||
summary: "Public AI provider catalog"
|
||||
)
|
||||
service gateway {
|
||||
@handler listAiProviders
|
||||
get /providers returns (AIProvidersData)
|
||||
}
|
||||
|
||||
@server (
|
||||
group: ai
|
||||
prefix: /api/v1/ai
|
||||
middleware: MemberAuth
|
||||
tags: "AI - Provider Streaming (member)"
|
||||
summary: "Chat/stream/models; member JWT via X-Member-Authorization; provider API key via Authorization"
|
||||
)
|
||||
service gateway {
|
||||
@handler listAiProviderModels
|
||||
post /providers/:provider/models (AIProviderPath) returns (AIProviderModelsData)
|
||||
|
||||
@handler chat
|
||||
post /chat (AIChatReq) returns (AIChatData)
|
||||
|
||||
@handler chatStream
|
||||
post /chat/stream (AIChatReq)
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
syntax = "v1"
|
||||
|
||||
type (
|
||||
AuthRegisterReq {
|
||||
TenantID string `json:"tenant_id" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8,max=128"`
|
||||
DisplayName string `json:"display_name,optional"`
|
||||
Language string `json:"language,optional"`
|
||||
}
|
||||
|
||||
AuthLoginReq {
|
||||
TenantID string `json:"tenant_id" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=8,max=128"`
|
||||
}
|
||||
|
||||
AuthRefreshReq {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
AuthTokenData {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
UID string `json:"uid"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
LogoutData {
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
)
|
||||
|
||||
@server(
|
||||
group: auth
|
||||
prefix: /api/v1/auth
|
||||
tags: "Auth"
|
||||
summary: "Native member auth and JWT token endpoints"
|
||||
)
|
||||
service gateway {
|
||||
@handler register
|
||||
post /register (AuthRegisterReq) returns (AuthTokenData)
|
||||
|
||||
@handler login
|
||||
post /login (AuthLoginReq) returns (AuthTokenData)
|
||||
|
||||
@handler refresh
|
||||
post /refresh (AuthRefreshReq) returns (AuthTokenData)
|
||||
}
|
||||
|
||||
@server(
|
||||
group: auth
|
||||
prefix: /api/v1/auth
|
||||
middleware: AuthJWT
|
||||
tags: "Auth"
|
||||
summary: "Logout requires member Bearer JWT"
|
||||
)
|
||||
service gateway {
|
||||
@handler logout
|
||||
post /logout returns (LogoutData)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
syntax = "v1"
|
||||
|
||||
type ErrorDetail {
|
||||
BizCode string `json:"biz_code,optional"`
|
||||
Scope int64 `json:"scope,optional"`
|
||||
Category int64 `json:"category,optional"`
|
||||
Detail int64 `json:"detail,optional"`
|
||||
}
|
||||
|
||||
type Status {
|
||||
Code int64 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,optional"`
|
||||
Error ErrorDetail `json:"error,optional"`
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "Haixun Backend"
|
||||
desc: "Haixun service-oriented backend core"
|
||||
author: "haixun"
|
||||
version: "0.1.0"
|
||||
host: "127.0.0.1:8890"
|
||||
schemes: "http,https"
|
||||
consumes: "application/json"
|
||||
produces: "application/json"
|
||||
)
|
||||
|
||||
import (
|
||||
"common.api"
|
||||
"normal.api"
|
||||
"setting.api"
|
||||
"ai.api"
|
||||
"job.api"
|
||||
"auth.api"
|
||||
"member.api"
|
||||
"permission.api"
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
syntax = "v1"
|
||||
|
||||
type (
|
||||
JobTemplatePath {
|
||||
Type string `path:"type" validate:"required"` // template type
|
||||
}
|
||||
|
||||
JobIDPath {
|
||||
ID string `path:"id" validate:"required"` // job run id
|
||||
}
|
||||
|
||||
JobScheduleIDPath {
|
||||
ID string `path:"id" validate:"required"` // schedule id
|
||||
}
|
||||
|
||||
ListJobsReq {
|
||||
Scope string `form:"scope,optional"` // filter by scope
|
||||
ScopeID string `form:"scope_id,optional"` // filter by scope id
|
||||
Page int64 `form:"page,optional"` // page number starting at 1
|
||||
PageSize int64 `form:"pageSize,optional"` // page size
|
||||
}
|
||||
|
||||
ListJobSchedulesReq {
|
||||
Scope string `form:"scope,optional"` // filter by scope
|
||||
ScopeID string `form:"scope_id,optional"` // filter by scope id
|
||||
Page int64 `form:"page,optional"` // page number starting at 1
|
||||
PageSize int64 `form:"pageSize,optional"` // page size
|
||||
}
|
||||
|
||||
ListJobEventsReq {
|
||||
ID string `path:"id" validate:"required"` // job run id
|
||||
Limit int64 `form:"limit,optional"` // max events
|
||||
}
|
||||
|
||||
CancelJobReq {
|
||||
ID string `path:"id" validate:"required"` // job run id
|
||||
Reason string `json:"reason,optional"` // cancel reason
|
||||
}
|
||||
|
||||
CreateJobReq {
|
||||
TemplateType string `json:"template_type" validate:"required"` // job template type
|
||||
Scope string `json:"scope" validate:"required,oneof=user account system"` // job scope
|
||||
ScopeID string `json:"scope_id" validate:"required"` // scope id
|
||||
Payload map[string]interface{} `json:"payload,optional"` // job payload
|
||||
}
|
||||
|
||||
UpsertJobTemplateReq {
|
||||
Type string `path:"type" validate:"required"` // template type
|
||||
Version int `json:"version,optional"` // template version
|
||||
Name string `json:"name" validate:"required"` // display name
|
||||
Description string `json:"description,optional"` // description
|
||||
Enabled bool `json:"enabled"` // enabled flag
|
||||
Repeatable bool `json:"repeatable"` // repeatable flag
|
||||
ConcurrencyPolicy string `json:"concurrency_policy,optional"` // concurrency policy
|
||||
DedupeKeys []string `json:"dedupe_keys,optional"` // dedupe keys
|
||||
TimeoutSeconds int `json:"timeout_seconds,optional"` // timeout seconds
|
||||
CancelPolicy JobCancelPolicyData `json:"cancel_policy,optional"` // cancel policy
|
||||
RetryPolicy JobRetryPolicyData `json:"retry_policy,optional"` // retry policy
|
||||
Steps []JobTemplateStepData `json:"steps" validate:"required,min=1,dive"` // steps
|
||||
}
|
||||
|
||||
CreateJobScheduleReq {
|
||||
TemplateType string `json:"template_type" validate:"required"` // template type
|
||||
Scope string `json:"scope" validate:"required,oneof=user account system"` // scope
|
||||
ScopeID string `json:"scope_id" validate:"required"` // scope id
|
||||
Cron string `json:"cron" validate:"required"` // cron expression
|
||||
Timezone string `json:"timezone,optional"` // timezone
|
||||
PayloadTemplate map[string]interface{} `json:"payload_template,optional"` // payload template
|
||||
Enabled bool `json:"enabled"` // enabled flag
|
||||
}
|
||||
|
||||
UpdateJobScheduleReq {
|
||||
ID string `path:"id" validate:"required"` // schedule id
|
||||
Cron string `json:"cron,optional"` // cron expression
|
||||
Timezone string `json:"timezone,optional"` // timezone
|
||||
PayloadTemplate map[string]interface{} `json:"payload_template,optional"` // payload template
|
||||
Enabled *bool `json:"enabled,optional"` // enabled flag
|
||||
}
|
||||
|
||||
JobCancelPolicyData {
|
||||
Supported bool `json:"supported"`
|
||||
Mode string `json:"mode"`
|
||||
GraceSeconds int `json:"grace_seconds"`
|
||||
}
|
||||
|
||||
JobRetryPolicyData {
|
||||
MaxAttempts int `json:"max_attempts"`
|
||||
BackoffSeconds []int `json:"backoff_seconds"`
|
||||
}
|
||||
|
||||
JobTemplateStepData {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
WorkerType string `json:"worker_type"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
Cancelable bool `json:"cancelable"`
|
||||
}
|
||||
|
||||
JobTemplateData {
|
||||
Type string `json:"type"`
|
||||
Version int `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Repeatable bool `json:"repeatable"`
|
||||
ConcurrencyPolicy string `json:"concurrency_policy"`
|
||||
DedupeKeys []string `json:"dedupe_keys"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
CancelPolicy JobCancelPolicyData `json:"cancel_policy"`
|
||||
RetryPolicy JobRetryPolicyData `json:"retry_policy"`
|
||||
Steps []JobTemplateStepData `json:"steps"`
|
||||
}
|
||||
|
||||
JobTemplateListData {
|
||||
List []JobTemplateData `json:"list"`
|
||||
}
|
||||
|
||||
JobStepProgressData {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
StartedAt *int64 `json:"started_at,optional"`
|
||||
EndedAt *int64 `json:"ended_at,optional"`
|
||||
Message string `json:"message,optional"`
|
||||
}
|
||||
|
||||
JobProgressData {
|
||||
Summary string `json:"summary"`
|
||||
Percentage int `json:"percentage"`
|
||||
Steps []JobStepProgressData `json:"steps"`
|
||||
}
|
||||
|
||||
JobData {
|
||||
ID string `json:"id"`
|
||||
TemplateType string `json:"template_type"`
|
||||
TemplateVersion int `json:"template_version"`
|
||||
Scope string `json:"scope"`
|
||||
ScopeID string `json:"scope_id"`
|
||||
Status string `json:"status"`
|
||||
Phase string `json:"phase"`
|
||||
WorkerType string `json:"worker_type"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
Progress JobProgressData `json:"progress"`
|
||||
Result map[string]interface{} `json:"result,optional"`
|
||||
Error string `json:"error,optional"`
|
||||
Attempt int `json:"attempt"`
|
||||
MaxAttempts int `json:"max_attempts"`
|
||||
CancelRequestedAt *int64 `json:"cancel_requested_at,optional"`
|
||||
CancelReason string `json:"cancel_reason,optional"`
|
||||
StartedAt *int64 `json:"started_at,optional"`
|
||||
CompletedAt *int64 `json:"completed_at,optional"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
}
|
||||
|
||||
JobListData {
|
||||
Pagination PaginationData `json:"pagination"`
|
||||
List []JobData `json:"list"`
|
||||
}
|
||||
|
||||
JobScheduleData {
|
||||
ID string `json:"id"`
|
||||
TemplateType string `json:"template_type"`
|
||||
Scope string `json:"scope"`
|
||||
ScopeID string `json:"scope_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Cron string `json:"cron"`
|
||||
Timezone string `json:"timezone"`
|
||||
PayloadTemplate map[string]interface{} `json:"payload_template"`
|
||||
LastRunAt *int64 `json:"last_run_at,optional"`
|
||||
NextRunAt int64 `json:"next_run_at"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
}
|
||||
|
||||
JobScheduleListData {
|
||||
Pagination PaginationData `json:"pagination"`
|
||||
List []JobScheduleData `json:"list"`
|
||||
}
|
||||
|
||||
JobEventData {
|
||||
ID string `json:"id"`
|
||||
JobID string `json:"job_id"`
|
||||
Type string `json:"type"`
|
||||
From string `json:"from,optional"`
|
||||
To string `json:"to,optional"`
|
||||
Message string `json:"message"`
|
||||
Metadata map[string]interface{} `json:"metadata,optional"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
}
|
||||
|
||||
JobEventListData {
|
||||
List []JobEventData `json:"list"`
|
||||
}
|
||||
)
|
||||
|
||||
@server(
|
||||
group: job
|
||||
prefix: /api/v1
|
||||
middleware: AuthJWT
|
||||
tags: "Job - Core"
|
||||
summary: "Generic job templates, runs, schedules, cancel, and retry. Requires Bearer JWT."
|
||||
)
|
||||
service gateway {
|
||||
@handler listJobTemplates
|
||||
get /job/templates returns (JobTemplateListData)
|
||||
|
||||
@handler getJobTemplate
|
||||
get /job/templates/:type (JobTemplatePath) returns (JobTemplateData)
|
||||
|
||||
@handler upsertJobTemplate
|
||||
put /job/templates/:type (UpsertJobTemplateReq) returns (JobTemplateData)
|
||||
|
||||
@handler createJob
|
||||
post /jobs (CreateJobReq) returns (JobData)
|
||||
|
||||
@handler getJob
|
||||
get /jobs/:id (JobIDPath) returns (JobData)
|
||||
|
||||
@handler listJobs
|
||||
get /jobs (ListJobsReq) returns (JobListData)
|
||||
|
||||
@handler listJobEvents
|
||||
get /jobs/:id/events (ListJobEventsReq) returns (JobEventListData)
|
||||
|
||||
@handler cancelJob
|
||||
post /jobs/:id/cancel (CancelJobReq) returns (JobData)
|
||||
|
||||
@handler retryJob
|
||||
post /jobs/:id/retry (JobIDPath) returns (JobData)
|
||||
|
||||
@handler listJobSchedules
|
||||
get /job/schedules (ListJobSchedulesReq) returns (JobScheduleListData)
|
||||
|
||||
@handler createJobSchedule
|
||||
post /job/schedules (CreateJobScheduleReq) returns (JobScheduleData)
|
||||
|
||||
@handler updateJobSchedule
|
||||
put /job/schedules/:id (UpdateJobScheduleReq) returns (JobScheduleData)
|
||||
|
||||
@handler enableJobSchedule
|
||||
post /job/schedules/:id/enable (JobScheduleIDPath) returns (JobScheduleData)
|
||||
|
||||
@handler disableJobSchedule
|
||||
post /job/schedules/:id/disable (JobScheduleIDPath) returns (JobScheduleData)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
syntax = "v1"
|
||||
|
||||
type (
|
||||
MemberMeData {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UID string `json:"uid"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Origin string `json:"origin"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
BusinessEmail string `json:"business_email,omitempty"`
|
||||
BusinessEmailVerified bool `json:"business_email_verified"`
|
||||
BusinessPhone string `json:"business_phone,omitempty"`
|
||||
BusinessPhoneVerified bool `json:"business_phone_verified"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
}
|
||||
|
||||
UpdateMemberMeReq {
|
||||
DisplayName string `json:"display_name,optional"`
|
||||
Avatar string `json:"avatar,optional"`
|
||||
Language string `json:"language,optional"`
|
||||
Currency string `json:"currency,optional"`
|
||||
Phone string `json:"phone,optional"`
|
||||
}
|
||||
)
|
||||
|
||||
@server(
|
||||
group: member
|
||||
prefix: /api/v1/members
|
||||
middleware: AuthJWT
|
||||
tags: "Member"
|
||||
summary: "Current member profile endpoints. Requires Bearer JWT or dev headers."
|
||||
)
|
||||
service gateway {
|
||||
@handler getMemberMe
|
||||
get /me returns (MemberMeData)
|
||||
|
||||
@handler updateMemberMe
|
||||
patch /me (UpdateMemberMeReq) returns (MemberMeData)
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
syntax = "v1"
|
||||
|
||||
type HealthData {
|
||||
Pong string `json:"pong"`
|
||||
}
|
||||
|
||||
@server(
|
||||
group: normal
|
||||
prefix: /api/v1
|
||||
tags: "Normal - Public"
|
||||
summary: "Health check"
|
||||
)
|
||||
service gateway {
|
||||
@handler health
|
||||
get /health () returns (HealthData)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
syntax = "v1"
|
||||
|
||||
type (
|
||||
PermissionCatalogQuery {
|
||||
Status string `form:"status,optional" validate:"omitempty,oneof=open close"`
|
||||
Type string `form:"type,optional" validate:"omitempty,oneof=backend_user frontend_user"`
|
||||
Tree bool `form:"tree,optional"`
|
||||
}
|
||||
|
||||
PermissionNode {
|
||||
ID string `json:"id"`
|
||||
Parent string `json:"parent,omitempty"`
|
||||
Name string `json:"name"`
|
||||
HTTPMethods string `json:"http_methods,omitempty"`
|
||||
HTTPPath string `json:"http_path,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
Children []PermissionNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
PermissionCatalogData {
|
||||
Tree []PermissionNode `json:"tree,omitempty"`
|
||||
List []PermissionNode `json:"list,omitempty"`
|
||||
}
|
||||
|
||||
MePermissionsQuery {
|
||||
IncludeTree bool `form:"include_tree,optional"`
|
||||
}
|
||||
|
||||
MePermissionsData {
|
||||
UID string `json:"uid"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Roles []string `json:"roles"`
|
||||
Permissions map[string]string `json:"permissions"`
|
||||
Tree []PermissionNode `json:"tree,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
@server(
|
||||
group: permission
|
||||
prefix: /api/v1/permissions
|
||||
middleware: AuthJWT
|
||||
tags: "Permission"
|
||||
summary: "Permission catalog and current member permissions. Requires Bearer JWT."
|
||||
)
|
||||
service gateway {
|
||||
@handler getPermissionCatalog
|
||||
get /catalog (PermissionCatalogQuery) returns (PermissionCatalogData)
|
||||
|
||||
@handler getMePermissions
|
||||
get /me (MePermissionsQuery) returns (MePermissionsData)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
syntax = "v1"
|
||||
|
||||
type (
|
||||
SettingPath {
|
||||
Scope string `path:"scope" validate:"required,oneof=user account system"` // 設定範圍,可選 user / account / system
|
||||
ScopeID string `path:"scope_id" validate:"required"` // 範圍 ID,例如 user_id、account_id 或 global
|
||||
Page int64 `form:"page,optional"` // 頁碼,從 1 開始
|
||||
PageSize int64 `form:"pageSize,optional"` // 每頁筆數,server 會限制最大值
|
||||
}
|
||||
|
||||
SettingKeyPath {
|
||||
Scope string `path:"scope" validate:"required,oneof=user account system"` // 設定範圍,可選 user / account / system
|
||||
ScopeID string `path:"scope_id" validate:"required"` // 範圍 ID,例如 user_id、account_id 或 global
|
||||
Key string `path:"key" validate:"required"` // 設定 key,例如 ai.default
|
||||
}
|
||||
|
||||
SettingUpsertReq {
|
||||
Scope string `path:"scope" validate:"required,oneof=user account system"` // 設定範圍,可選 user / account / system
|
||||
ScopeID string `path:"scope_id" validate:"required"` // 範圍 ID,例如 user_id、account_id 或 global
|
||||
Key string `path:"key" validate:"required"` // 設定 key,例如 ai.default
|
||||
Value map[string]interface{} `json:"value" validate:"required"` // 設定內容 JSON object
|
||||
Version int `json:"version,optional"` // schema version,未帶入時預設 1
|
||||
}
|
||||
|
||||
SettingData {
|
||||
ID string `json:"id"`
|
||||
Scope string `json:"scope"`
|
||||
ScopeID string `json:"scope_id"`
|
||||
Key string `json:"key"`
|
||||
Value map[string]interface{} `json:"value"`
|
||||
Version int `json:"version"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
}
|
||||
|
||||
SettingListData {
|
||||
Pagination PaginationData `json:"pagination"`
|
||||
List []SettingData `json:"list"`
|
||||
}
|
||||
|
||||
PaginationData {
|
||||
Total int64 `json:"total"`
|
||||
Page int64 `json:"page"`
|
||||
PageSize int64 `json:"pageSize"`
|
||||
TotalPages int64 `json:"totalPages"`
|
||||
}
|
||||
)
|
||||
|
||||
@server(
|
||||
group: setting
|
||||
prefix: /api/v1/settings
|
||||
middleware: AuthJWT
|
||||
tags: "Setting - General"
|
||||
summary: "Manage settings by scope, scope_id, and key. Requires Bearer JWT."
|
||||
)
|
||||
service gateway {
|
||||
@handler listSettings
|
||||
get /:scope/:scope_id (SettingPath) returns (SettingListData)
|
||||
|
||||
@handler getSetting
|
||||
get /:scope/:scope_id/:key (SettingKeyPath) returns (SettingData)
|
||||
|
||||
@handler upsertSetting
|
||||
put /:scope/:scope_id/:key (SettingUpsertReq) returns (SettingData)
|
||||
|
||||
@handler deleteSetting
|
||||
delete /:scope/:scope_id/:key (SettingKeyPath)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl {{.version}}
|
||||
|
||||
package {{.PkgName}}
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/response"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
{{.ImportPackages}}
|
||||
)
|
||||
|
||||
{{if .HasDoc}}{{.Doc}}{{end}}
|
||||
func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
{{if .HasRequest}}var req types.{{.RequestType}}
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
{{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
|
||||
{{if .HasResp}}data, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
|
||||
{{if .HasResp}}response.Write(r.Context(), w, data, err){{else}}response.Write(r.Context(), w, nil, err){{end}}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
module haixun-backend
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.14.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/zeromicro/go-zero v1.9.2
|
||||
go.mongodb.org/mongo-driver v1.17.4
|
||||
golang.org/x/crypto v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // 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.2.2 // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
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/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/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
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/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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
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/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
|
||||
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
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/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
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/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.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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/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.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeromicro/go-zero v1.9.2 h1:ZXOXBIcazZ1pWAMiHyVnDQ3Sxwy7DYPzjE89Qtj9vqM=
|
||||
github.com/zeromicro/go-zero v1.9.2/go.mod h1:k8YBMEFZKjTd4q/qO5RCW+zDgUlNyAs5vue3P4/Kmn0=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
app "haixun-backend/internal/library/errors"
|
||||
"haixun-backend/internal/library/errors/code"
|
||||
"haixun-backend/internal/model/member/domain/entity"
|
||||
domrepo "haixun-backend/internal/model/member/domain/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type AdminOptions struct {
|
||||
TenantID string
|
||||
Email string
|
||||
Password string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
func EnsureAdminMember(ctx context.Context, repo domrepo.Repository, opts AdminOptions) (*entity.Member, bool, error) {
|
||||
tenantID := strings.TrimSpace(opts.TenantID)
|
||||
email := normalizeEmail(opts.Email)
|
||||
if tenantID == "" || email == "" || opts.Password == "" {
|
||||
return nil, false, app.For(code.Member).InputMissingRequired("tenant_id, email, and password are required")
|
||||
}
|
||||
if len(opts.Password) < 8 {
|
||||
return nil, false, app.For(code.Member).InputInvalidFormat("password must be at least 8 characters")
|
||||
}
|
||||
|
||||
existing, err := repo.FindByEmail(ctx, tenantID, email)
|
||||
if err == nil {
|
||||
if err := repo.SetRoles(ctx, tenantID, existing.UID, []string{"admin"}); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
existing.Roles = []string{"admin"}
|
||||
return existing, false, nil
|
||||
}
|
||||
if e := app.FromError(err); e == nil || e.Category() != code.ResNotFound {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(opts.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, false, app.For(code.Member).SysInternal("hash password failed").WithCause(err)
|
||||
}
|
||||
displayName := strings.TrimSpace(opts.DisplayName)
|
||||
if displayName == "" {
|
||||
displayName = "Admin"
|
||||
}
|
||||
member, err := repo.Create(ctx, &entity.Member{
|
||||
TenantID: tenantID,
|
||||
UID: uuid.NewString(),
|
||||
Email: email,
|
||||
DisplayName: displayName,
|
||||
Language: "zh-TW",
|
||||
Status: entity.StatusOpen,
|
||||
Origin: entity.OriginNative,
|
||||
PasswordHash: string(hash),
|
||||
Roles: []string{"admin"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return member, true, nil
|
||||
}
|
||||
|
||||
func normalizeEmail(email string) string {
|
||||
return strings.ToLower(strings.TrimSpace(email))
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
app "haixun-backend/internal/library/errors"
|
||||
"haixun-backend/internal/library/errors/code"
|
||||
"haixun-backend/internal/model/member/domain/entity"
|
||||
domrepo "haixun-backend/internal/model/member/domain/repository"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type memoryMemberRepo struct {
|
||||
byEmail map[string]*entity.Member
|
||||
}
|
||||
|
||||
func memberKey(tenantID, email string) string {
|
||||
return tenantID + ":" + normalizeEmail(email)
|
||||
}
|
||||
|
||||
func (m *memoryMemberRepo) EnsureIndexes(context.Context) error { return nil }
|
||||
|
||||
func (m *memoryMemberRepo) Create(_ context.Context, member *entity.Member) (*entity.Member, error) {
|
||||
if m.byEmail == nil {
|
||||
m.byEmail = map[string]*entity.Member{}
|
||||
}
|
||||
key := memberKey(member.TenantID, member.Email)
|
||||
if _, ok := m.byEmail[key]; ok {
|
||||
return nil, app.For(code.Member).ResConflict("member already exists")
|
||||
}
|
||||
cp := *member
|
||||
m.byEmail[key] = &cp
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
func (m *memoryMemberRepo) FindByUID(_ context.Context, tenantID, uid string) (*entity.Member, error) {
|
||||
for _, item := range m.byEmail {
|
||||
if item.TenantID == tenantID && item.UID == uid {
|
||||
cp := *item
|
||||
return &cp, nil
|
||||
}
|
||||
}
|
||||
return nil, app.For(code.Member).ResNotFound("member not found")
|
||||
}
|
||||
|
||||
func (m *memoryMemberRepo) FindByEmail(_ context.Context, tenantID, email string) (*entity.Member, error) {
|
||||
item, ok := m.byEmail[memberKey(tenantID, email)]
|
||||
if !ok {
|
||||
return nil, app.For(code.Member).ResNotFound("member not found")
|
||||
}
|
||||
cp := *item
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
func (m *memoryMemberRepo) UpdateProfile(context.Context, string, string, domrepo.ProfileUpdate) (*entity.Member, error) {
|
||||
return nil, app.For(code.Member).SysNotImplemented("not implemented")
|
||||
}
|
||||
|
||||
func (m *memoryMemberRepo) SetRoles(_ context.Context, tenantID, uid string, roles []string) error {
|
||||
for _, item := range m.byEmail {
|
||||
if item.TenantID == tenantID && item.UID == uid {
|
||||
item.Roles = append([]string(nil), roles...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return app.For(code.Member).ResNotFound("member not found")
|
||||
}
|
||||
|
||||
func TestEnsureAdminMemberCreatesAdmin(t *testing.T) {
|
||||
repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{}}
|
||||
member, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{
|
||||
TenantID: "default",
|
||||
Email: "admin@haixun.local",
|
||||
Password: "Admin-Pass-1!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureAdminMember: %v", err)
|
||||
}
|
||||
if !created {
|
||||
t.Fatal("expected created=true")
|
||||
}
|
||||
if member.Roles[0] != "admin" {
|
||||
t.Fatalf("roles=%v", member.Roles)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte("Admin-Pass-1!")); err != nil {
|
||||
t.Fatalf("password hash mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureAdminMemberUpgradesExisting(t *testing.T) {
|
||||
repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{
|
||||
memberKey("default", "admin@haixun.local"): {
|
||||
TenantID: "default",
|
||||
UID: "uid-1",
|
||||
Email: "admin@haixun.local",
|
||||
Roles: []string{"user"},
|
||||
},
|
||||
}}
|
||||
_, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{
|
||||
TenantID: "default",
|
||||
Email: "admin@haixun.local",
|
||||
Password: "Admin-Pass-1!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureAdminMember: %v", err)
|
||||
}
|
||||
if created {
|
||||
t.Fatal("expected created=false")
|
||||
}
|
||||
member := repo.byEmail[memberKey("default", "admin@haixun.local")]
|
||||
if len(member.Roles) != 1 || member.Roles[0] != "admin" {
|
||||
t.Fatalf("roles=%v", member.Roles)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"haixun-backend/internal/config"
|
||||
libmongo "haixun-backend/internal/library/mongo"
|
||||
jobrepo "haixun-backend/internal/model/job/repository"
|
||||
memberrepo "haixun-backend/internal/model/member/repository"
|
||||
permissionrepo "haixun-backend/internal/model/permission/repository"
|
||||
permissionuc "haixun-backend/internal/model/permission/usecase"
|
||||
settingrepo "haixun-backend/internal/model/setting/repository"
|
||||
)
|
||||
|
||||
type InitOptions struct {
|
||||
TenantID string
|
||||
AdminEmail string
|
||||
AdminPass string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
type InitReport struct {
|
||||
IndexesEnsured bool
|
||||
PermissionsSeeded bool
|
||||
RolePermissionsSeeded bool
|
||||
AdminUID string
|
||||
AdminCreated bool
|
||||
}
|
||||
|
||||
func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport, error) {
|
||||
if cfg.Mongo.URI == "" || cfg.Mongo.Database == "" {
|
||||
return nil, fmt.Errorf("mongo URI and database are required")
|
||||
}
|
||||
if opts.TenantID == "" {
|
||||
return nil, fmt.Errorf("tenant_id is required")
|
||||
}
|
||||
if opts.AdminEmail == "" || opts.AdminPass == "" {
|
||||
return nil, fmt.Errorf("admin email and password are required")
|
||||
}
|
||||
|
||||
mongoClient, err := libmongo.NewClient(ctx, cfg.Mongo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect mongo: %w", err)
|
||||
}
|
||||
defer func() { _ = mongoClient.Close(ctx) }()
|
||||
|
||||
db := mongoClient.Database()
|
||||
report := &InitReport{}
|
||||
|
||||
settingRepository := settingrepo.NewMongoRepository(db)
|
||||
memberRepository := memberrepo.NewMongoRepository(db)
|
||||
permissionRepository := permissionrepo.NewMongoPermissionRepository(db)
|
||||
rolePermissionRepository := permissionrepo.NewMongoRolePermissionRepository(db)
|
||||
jobTemplateRepository := jobrepo.NewMongoTemplateRepository(db)
|
||||
jobRunRepository := jobrepo.NewMongoRunRepository(db)
|
||||
jobScheduleRepository := jobrepo.NewMongoScheduleRepository(db)
|
||||
jobEventRepository := jobrepo.NewMongoEventRepository(db)
|
||||
|
||||
repos := []struct {
|
||||
name string
|
||||
fn func(context.Context) error
|
||||
}{
|
||||
{"settings", settingRepository.EnsureIndexes},
|
||||
{"members", memberRepository.EnsureIndexes},
|
||||
{"permissions", permissionRepository.EnsureIndexes},
|
||||
{"role_permissions", rolePermissionRepository.EnsureIndexes},
|
||||
{"job_templates", jobTemplateRepository.EnsureIndexes},
|
||||
{"job_runs", jobRunRepository.EnsureIndexes},
|
||||
{"job_schedules", jobScheduleRepository.EnsureIndexes},
|
||||
{"job_events", jobEventRepository.EnsureIndexes},
|
||||
}
|
||||
for _, repo := range repos {
|
||||
if err := repo.fn(ctx); err != nil {
|
||||
return nil, fmt.Errorf("ensure %s indexes: %w", repo.name, err)
|
||||
}
|
||||
}
|
||||
report.IndexesEnsured = true
|
||||
|
||||
permissionUseCase := permissionuc.NewUseCase(permissionRepository, rolePermissionRepository)
|
||||
if err := permissionUseCase.EnsureDefaultPermissions(ctx); err != nil {
|
||||
return nil, fmt.Errorf("seed permissions catalog: %w", err)
|
||||
}
|
||||
report.PermissionsSeeded = true
|
||||
|
||||
if err := permissionUseCase.EnsureDefaultRolePermissions(ctx, opts.TenantID); err != nil {
|
||||
return nil, fmt.Errorf("seed role permissions: %w", err)
|
||||
}
|
||||
report.RolePermissionsSeeded = true
|
||||
|
||||
admin, created, err := EnsureAdminMember(ctx, memberRepository, AdminOptions{
|
||||
TenantID: opts.TenantID,
|
||||
Email: opts.AdminEmail,
|
||||
Password: opts.AdminPass,
|
||||
DisplayName: opts.DisplayName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report.AdminUID = admin.UID
|
||||
report.AdminCreated = created
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package config
|
||||
|
||||
import "github.com/zeromicro/go-zero/rest"
|
||||
|
||||
type MongoConf struct {
|
||||
URI string
|
||||
Database string
|
||||
TimeoutSeconds int `json:",default=10"`
|
||||
}
|
||||
|
||||
type RedisConf struct {
|
||||
Addr string `json:",optional"`
|
||||
DB int `json:",optional"`
|
||||
}
|
||||
|
||||
type JobWorkerConf struct {
|
||||
Enabled bool `json:",default=true"`
|
||||
WorkerType string `json:",default=go"`
|
||||
WorkerID string `json:",optional"`
|
||||
}
|
||||
|
||||
type JobSchedulerConf struct {
|
||||
Enabled bool `json:",default=true"`
|
||||
IntervalSeconds int `json:",default=60"`
|
||||
}
|
||||
|
||||
type JobReaperConf struct {
|
||||
Enabled bool `json:",default=true"`
|
||||
IntervalSeconds int `json:",default=30"`
|
||||
}
|
||||
|
||||
type AuthConf struct {
|
||||
AccessSecret string `json:",optional"`
|
||||
RefreshSecret string `json:",optional"`
|
||||
AccessExpireSeconds int64 `json:",default=900"`
|
||||
RefreshExpireSeconds int64 `json:",default=2592000"`
|
||||
DevHeaderFallback bool `json:",default=true"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
rest.RestConf
|
||||
Mongo MongoConf `json:",optional"`
|
||||
Redis RedisConf `json:",optional"`
|
||||
Auth AuthConf `json:",optional"`
|
||||
JobWorker JobWorkerConf `json:",optional"`
|
||||
JobScheduler JobSchedulerConf `json:",optional"`
|
||||
JobReaper JobReaperConf `json:",optional"`
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/ai"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ChatHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AIChatReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := ai.BearerToken(r)
|
||||
if err != nil {
|
||||
response.Write(r.Context(), w, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := ai.NewChatLogic(r.Context(), svcCtx)
|
||||
data, err := l.Chat(&req, token)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/ai"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ChatStreamHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AIChatReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := ai.BearerToken(r)
|
||||
if err != nil {
|
||||
response.Write(r.Context(), w, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := ai.NewChatStreamLogic(r.Context(), svcCtx)
|
||||
stream, err := l.ChatStream(&req, token)
|
||||
if err != nil {
|
||||
response.Write(r.Context(), w, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(fmt.Errorf("server does not support streaming")))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
for event := range stream {
|
||||
writeSSE(w, event.Type, event)
|
||||
flusher.Flush()
|
||||
if event.Type == "done" || event.Type == "error" {
|
||||
return
|
||||
}
|
||||
}
|
||||
writeSSE(w, "done", map[string]string{"finish_reason": "stop"})
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func writeSSE(w http.ResponseWriter, eventName string, data any) {
|
||||
payload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
payload = []byte(`{"type":"error","error":"failed to serialize SSE payload"}`)
|
||||
eventName = "error"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "event: %s\n", eventName)
|
||||
_, _ = fmt.Fprintf(w, "data: %s\n\n", payload)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/ai"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListAiProviderModelsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AIProviderPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := ai.BearerToken(r)
|
||||
if err != nil {
|
||||
response.Write(r.Context(), w, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := ai.NewListAIProviderModelsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListAIProviderModels(&req, token)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/ai"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
)
|
||||
|
||||
func ListAiProvidersHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := ai.NewListAIProvidersLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListAIProviders()
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/auth"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AuthLoginReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
l := auth.NewLoginLogic(r.Context(), svcCtx)
|
||||
data, err := l.Login(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/auth"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
)
|
||||
|
||||
func LogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := auth.NewLogoutLogic(r.Context(), svcCtx)
|
||||
data, err := l.Logout(r.Header.Get("Authorization"))
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/auth"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func RefreshHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AuthRefreshReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
l := auth.NewRefreshLogic(r.Context(), svcCtx)
|
||||
data, err := l.Refresh(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/auth"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AuthRegisterReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
l := auth.NewRegisterLogic(r.Context(), svcCtx)
|
||||
data, err := l.Register(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func CancelJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CancelJobReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewCancelJobLogic(r.Context(), svcCtx)
|
||||
data, err := l.CancelJob(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func CreateJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CreateJobReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewCreateJobLogic(r.Context(), svcCtx)
|
||||
data, err := l.CreateJob(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func CreateJobScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CreateJobScheduleReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewCreateJobScheduleLogic(r.Context(), svcCtx)
|
||||
data, err := l.CreateJobSchedule(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func DisableJobScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.JobScheduleIDPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewDisableJobScheduleLogic(r.Context(), svcCtx)
|
||||
data, err := l.DisableJobSchedule(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func EnableJobScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.JobScheduleIDPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewEnableJobScheduleLogic(r.Context(), svcCtx)
|
||||
data, err := l.EnableJobSchedule(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.JobIDPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewGetJobLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetJob(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetJobTemplateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.JobTemplatePath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewGetJobTemplateLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetJobTemplate(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListJobEventsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListJobEventsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewListJobEventsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListJobEvents(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListJobSchedulesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListJobSchedulesReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewListJobSchedulesLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListJobSchedules(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
)
|
||||
|
||||
func ListJobTemplatesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := job.NewListJobTemplatesLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListJobTemplates()
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListJobsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListJobsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewListJobsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListJobs(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func RetryJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.JobIDPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewRetryJobLogic(r.Context(), svcCtx)
|
||||
data, err := l.RetryJob(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpdateJobScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UpdateJobScheduleReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewUpdateJobScheduleLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpdateJobSchedule(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/job"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpsertJobTemplateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UpsertJobTemplateReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := job.NewUpsertJobTemplateLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpsertJobTemplate(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package member
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/member"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
)
|
||||
|
||||
func GetMemberMeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := member.NewGetMemberMeLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetMemberMe()
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package member
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/member"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpdateMemberMeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UpdateMemberMeReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := member.NewUpdateMemberMeLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpdateMemberMe(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package normal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/normal"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
)
|
||||
|
||||
func HealthHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := normal.NewHealthLogic(r.Context(), svcCtx)
|
||||
data, err := l.Health()
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package permission
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/permission"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetMePermissionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.MePermissionsQuery
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := permission.NewGetMePermissionsLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetMePermissions(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package permission
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/permission"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetPermissionCatalogHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.PermissionCatalogQuery
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
l := permission.NewGetPermissionCatalogLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetPermissionCatalog(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl 1.10.1
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ai "haixun-backend/internal/handler/ai"
|
||||
auth "haixun-backend/internal/handler/auth"
|
||||
job "haixun-backend/internal/handler/job"
|
||||
member "haixun-backend/internal/handler/member"
|
||||
normal "haixun-backend/internal/handler/normal"
|
||||
permission "haixun-backend/internal/handler/permission"
|
||||
setting "haixun-backend/internal/handler/setting"
|
||||
"haixun-backend/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest"
|
||||
)
|
||||
|
||||
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
server.AddRoutes(
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/providers",
|
||||
Handler: ai.ListAiProvidersHandler(serverCtx),
|
||||
},
|
||||
},
|
||||
rest.WithPrefix("/api/v1/ai"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.MemberAuth},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/chat",
|
||||
Handler: ai.ChatHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/chat/stream",
|
||||
Handler: ai.ChatStreamHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/providers/:provider/models",
|
||||
Handler: ai.ListAiProviderModelsHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/ai"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/login",
|
||||
Handler: auth.LoginHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/refresh",
|
||||
Handler: auth.RefreshHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/register",
|
||||
Handler: auth.RegisterHandler(serverCtx),
|
||||
},
|
||||
},
|
||||
rest.WithPrefix("/api/v1/auth"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/logout",
|
||||
Handler: auth.LogoutHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/auth"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/schedules",
|
||||
Handler: job.ListJobSchedulesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules",
|
||||
Handler: job.CreateJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/job/schedules/:id",
|
||||
Handler: job.UpdateJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules/:id/disable",
|
||||
Handler: job.DisableJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/job/schedules/:id/enable",
|
||||
Handler: job.EnableJobScheduleHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/templates",
|
||||
Handler: job.ListJobTemplatesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/job/templates/:type",
|
||||
Handler: job.GetJobTemplateHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/job/templates/:type",
|
||||
Handler: job.UpsertJobTemplateHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs",
|
||||
Handler: job.CreateJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs",
|
||||
Handler: job.ListJobsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs/:id",
|
||||
Handler: job.GetJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs/:id/cancel",
|
||||
Handler: job.CancelJobHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/jobs/:id/events",
|
||||
Handler: job.ListJobEventsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/jobs/:id/retry",
|
||||
Handler: job.RetryJobHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
Handler: member.GetMemberMeHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPatch,
|
||||
Path: "/me",
|
||||
Handler: member.UpdateMemberMeHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/members"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/health",
|
||||
Handler: normal.HealthHandler(serverCtx),
|
||||
},
|
||||
},
|
||||
rest.WithPrefix("/api/v1"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/catalog",
|
||||
Handler: permission.GetPermissionCatalogHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
Handler: permission.GetMePermissionsHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/permissions"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/:scope/:scope_id",
|
||||
Handler: setting.ListSettingsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/:scope/:scope_id/:key",
|
||||
Handler: setting.GetSettingHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/:scope/:scope_id/:key",
|
||||
Handler: setting.UpsertSettingHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/:scope/:scope_id/:key",
|
||||
Handler: setting.DeleteSettingHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/settings"),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/setting"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func DeleteSettingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SettingKeyPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
l := setting.NewDeleteSettingLogic(r.Context(), svcCtx)
|
||||
err := l.DeleteSetting(&req)
|
||||
response.Write(r.Context(), w, nil, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/setting"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetSettingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SettingKeyPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
l := setting.NewGetSettingLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetSetting(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/setting"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListSettingsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SettingPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
l := setting.NewListSettingsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListSettings(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/logic/setting"
|
||||
"haixun-backend/internal/response"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpsertSettingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SettingUpsertReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
l := setting.NewUpsertSettingLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpsertSetting(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package authctx
|
||||
|
||||
import "context"
|
||||
|
||||
type Actor struct {
|
||||
TenantID string
|
||||
UID string
|
||||
JTI string
|
||||
}
|
||||
|
||||
type actorKey struct{}
|
||||
|
||||
func WithActor(ctx context.Context, actor Actor) context.Context {
|
||||
return context.WithValue(ctx, actorKey{}, actor)
|
||||
}
|
||||
|
||||
func ActorFromContext(ctx context.Context) (Actor, bool) {
|
||||
actor, ok := ctx.Value(actorKey{}).(Actor)
|
||||
return actor, ok
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package clock
|
||||
|
||||
import "time"
|
||||
|
||||
// StorageTimezone is the only timezone used for persisted timestamps.
|
||||
const StorageTimezone = "UTC"
|
||||
|
||||
// Now returns the current instant in UTC.
|
||||
func Now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// NowUnixNano returns the current instant as UTC unix nanoseconds.
|
||||
func NowUnixNano() int64 {
|
||||
return Now().UnixNano()
|
||||
}
|
||||
|
||||
// UnixNano converts an instant to UTC unix nanoseconds.
|
||||
func UnixNano(t time.Time) int64 {
|
||||
return t.UTC().UnixNano()
|
||||
}
|
||||
|
||||
// FromUnixNano parses UTC unix nanoseconds.
|
||||
func FromUnixNano(nano int64) time.Time {
|
||||
return time.Unix(0, nano).UTC()
|
||||
}
|
||||
|
||||
// AddSecondsFromNow returns UTC unix nanoseconds after the given seconds.
|
||||
func AddSecondsFromNow(seconds int) int64 {
|
||||
return Now().Add(time.Duration(seconds) * time.Second).UnixNano()
|
||||
}
|
||||
|
||||
// SecondsToNanos converts whole seconds to nanoseconds.
|
||||
func SecondsToNanos(seconds int) int64 {
|
||||
return int64(seconds) * int64(time.Second)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package clock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNow_IsUTC(t *testing.T) {
|
||||
now := Now()
|
||||
if now.Location() != time.UTC {
|
||||
t.Fatalf("location = %v, want UTC", now.Location())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromUnixNano_RoundTrip(t *testing.T) {
|
||||
nano := NowUnixNano()
|
||||
parsed := FromUnixNano(nano)
|
||||
if parsed.UnixNano() != nano {
|
||||
t.Fatalf("parsed = %d, want %d", parsed.UnixNano(), nano)
|
||||
}
|
||||
if parsed.Location() != time.UTC {
|
||||
t.Fatalf("location = %v, want UTC", parsed.Location())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package code
|
||||
|
||||
type Scope uint32
|
||||
type Category uint32
|
||||
type Detail uint32
|
||||
|
||||
const (
|
||||
Unset Scope = 0
|
||||
Facade Scope = 10
|
||||
Setting Scope = 32
|
||||
AI Scope = 33
|
||||
Job Scope = 34
|
||||
Auth Scope = 35
|
||||
Member Scope = 36
|
||||
Permission Scope = 37
|
||||
CategoryMultiplier = 1000
|
||||
ScopeMultiplier = 1000000
|
||||
DefaultDetail Detail = 0
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSuccessFullCodeInt int64 = 102000
|
||||
DefaultSuccessMessage string = "SUCCESS"
|
||||
)
|
||||
|
||||
const (
|
||||
InputInvalidFormat Category = 101
|
||||
InputMissingRequired Category = 104
|
||||
DBError Category = 201
|
||||
DBUnavailable Category = 204
|
||||
ResNotFound Category = 301
|
||||
ResConflict Category = 303
|
||||
ResInvalidState Category = 309
|
||||
AuthUnauthorized Category = 401
|
||||
AuthForbidden Category = 505
|
||||
SysInternal Category = 601
|
||||
SysNotImplemented Category = 605
|
||||
SvcThirdParty Category = 802
|
||||
)
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"haixun-backend/internal/library/errors/code"
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
scope code.Scope
|
||||
category code.Category
|
||||
detail code.Detail
|
||||
message string
|
||||
cause error
|
||||
}
|
||||
|
||||
func New(scope code.Scope, category code.Category, detail code.Detail, message string) *Error {
|
||||
return &Error{scope: scope, category: category, detail: detail, message: message}
|
||||
}
|
||||
|
||||
func FromError(err error) *Error {
|
||||
var e *Error
|
||||
if errors.As(err, &e) {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e *Error) WithCause(cause error) *Error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
cp := *e
|
||||
cp.cause = cause
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (e *Error) Scope() code.Scope {
|
||||
if e == nil {
|
||||
return code.Unset
|
||||
}
|
||||
return e.scope
|
||||
}
|
||||
|
||||
func (e *Error) Category() code.Category {
|
||||
if e == nil {
|
||||
return code.SysInternal
|
||||
}
|
||||
return e.category
|
||||
}
|
||||
|
||||
func (e *Error) Detail() code.Detail {
|
||||
if e == nil {
|
||||
return code.DefaultDetail
|
||||
}
|
||||
return e.detail
|
||||
}
|
||||
|
||||
func (e *Error) Code() uint32 {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return uint32(e.scope)*code.ScopeMultiplier + uint32(e.category)*code.CategoryMultiplier + uint32(e.detail)
|
||||
}
|
||||
|
||||
func (e *Error) DisplayCode() string {
|
||||
if e == nil {
|
||||
return "00000000"
|
||||
}
|
||||
return fmt.Sprintf("%08d", e.Code())
|
||||
}
|
||||
|
||||
func (e *Error) HTTPStatus() int {
|
||||
if e == nil {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
switch e.category {
|
||||
case code.InputInvalidFormat, code.InputMissingRequired:
|
||||
return http.StatusBadRequest
|
||||
case code.DBUnavailable:
|
||||
return http.StatusServiceUnavailable
|
||||
case code.ResNotFound:
|
||||
return http.StatusNotFound
|
||||
case code.ResConflict:
|
||||
return http.StatusConflict
|
||||
case code.ResInvalidState:
|
||||
return http.StatusConflict
|
||||
case code.AuthUnauthorized:
|
||||
return http.StatusUnauthorized
|
||||
case code.AuthForbidden:
|
||||
return http.StatusForbidden
|
||||
case code.SvcThirdParty:
|
||||
return http.StatusBadGateway
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
scope code.Scope
|
||||
}
|
||||
|
||||
func For(scope code.Scope) Builder {
|
||||
return Builder{scope: scope}
|
||||
}
|
||||
|
||||
func (b Builder) InputInvalidFormat(message string) *Error {
|
||||
return New(b.scope, code.InputInvalidFormat, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) InputMissingRequired(message string) *Error {
|
||||
return New(b.scope, code.InputMissingRequired, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) DBError(message string) *Error {
|
||||
return New(b.scope, code.DBError, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) DBUnavailable(message string) *Error {
|
||||
return New(b.scope, code.DBUnavailable, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) ResNotFound(message string) *Error {
|
||||
return New(b.scope, code.ResNotFound, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) ResConflict(message string) *Error {
|
||||
return New(b.scope, code.ResConflict, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) ResInvalidState(message string) *Error {
|
||||
return New(b.scope, code.ResInvalidState, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) AuthUnauthorized(message string) *Error {
|
||||
return New(b.scope, code.AuthUnauthorized, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) AuthForbidden(message string) *Error {
|
||||
return New(b.scope, code.AuthForbidden, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) SysInternal(message string) *Error {
|
||||
return New(b.scope, code.SysInternal, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) SysNotImplemented(message string) *Error {
|
||||
return New(b.scope, code.SysNotImplemented, 0, message)
|
||||
}
|
||||
|
||||
func (b Builder) SvcThirdParty(message string) *Error {
|
||||
return New(b.scope, code.SvcThirdParty, 0, message)
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"haixun-backend/internal/config"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
raw *mongo.Client
|
||||
db *mongo.Database
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conf config.MongoConf) (*Client, error) {
|
||||
if conf.URI == "" || conf.Database == "" {
|
||||
return nil, nil
|
||||
}
|
||||
timeout := time.Duration(conf.TimeoutSeconds) * time.Second
|
||||
if timeout <= 0 {
|
||||
timeout = 10 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
raw, err := mongo.Connect(ctx, options.Client().ApplyURI(conf.URI))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := raw.Ping(ctx, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{raw: raw, db: raw.Database(conf.Database)}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Database() *mongo.Database {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.db
|
||||
}
|
||||
|
||||
func (c *Client) Close(ctx context.Context) error {
|
||||
if c == nil || c.raw == nil {
|
||||
return nil
|
||||
}
|
||||
return c.raw.Disconnect(ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package redact
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
bearerPattern = regexp.MustCompile(`(?i)Bearer\s+[A-Za-z0-9._\-]+`)
|
||||
tokenPattern = regexp.MustCompile(`(?i)(api[_-]?key|token|authorization)\s*[:=]\s*["']?[^"'\s,}]+`)
|
||||
)
|
||||
|
||||
func Message(message string) string {
|
||||
if message == "" {
|
||||
return message
|
||||
}
|
||||
message = bearerPattern.ReplaceAllString(message, "Bearer [REDACTED]")
|
||||
message = tokenPattern.ReplaceAllString(message, "$1=[REDACTED]")
|
||||
return message
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package redact
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMessage_RedactsBearerToken(t *testing.T) {
|
||||
input := `Authorization Bearer sk-abc123xyz failed`
|
||||
got := Message(input)
|
||||
want := `Authorization Bearer [REDACTED] failed`
|
||||
if got != want {
|
||||
t.Fatalf("Message() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"haixun-backend/internal/config"
|
||||
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func NewClient(conf config.RedisConf) *goredis.Client {
|
||||
if conf.Addr == "" {
|
||||
return nil
|
||||
}
|
||||
return goredis.NewClient(&goredis.Options{
|
||||
Addr: conf.Addr,
|
||||
DB: conf.DB,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package validate
|
||||
|
||||
import "github.com/go-playground/validator/v10"
|
||||
|
||||
type Validate struct {
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
func New() *Validate {
|
||||
return &Validate{validator: validator.New()}
|
||||
}
|
||||
|
||||
func (v *Validate) ValidateAll(value any) error {
|
||||
if v == nil || v.validator == nil {
|
||||
return nil
|
||||
}
|
||||
return v.validator.Struct(value)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type ChatLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewChatLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ChatLogic {
|
||||
return &ChatLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ChatLogic) Chat(req *types.AIChatReq, token string) (*types.AIChatData, error) {
|
||||
result, err := l.svcCtx.AI.GenerateText(l.ctx, toGenerateRequest(req, token))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.AIChatData{Text: result.Text, FinishReason: result.FinishReason}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
domai "haixun-backend/internal/model/ai/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type ChatStreamLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewChatStreamLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ChatStreamLogic {
|
||||
return &ChatStreamLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ChatStreamLogic) ChatStream(req *types.AIChatReq, token string) (<-chan domai.StreamEvent, error) {
|
||||
return l.svcCtx.AI.StreamText(l.ctx, toGenerateRequest(req, token))
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
app "haixun-backend/internal/library/errors"
|
||||
"haixun-backend/internal/library/errors/code"
|
||||
)
|
||||
|
||||
func BearerToken(r *http.Request) (string, error) {
|
||||
auth := strings.TrimSpace(r.Header.Get("Authorization"))
|
||||
if auth == "" {
|
||||
return "", app.For(code.AI).InputMissingRequired("missing Authorization header")
|
||||
}
|
||||
|
||||
const prefix = "Bearer "
|
||||
if !strings.HasPrefix(auth, prefix) {
|
||||
return "", app.For(code.Facade).InputInvalidFormat("Authorization must be a Bearer token")
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(strings.TrimPrefix(auth, prefix))
|
||||
if token == "" {
|
||||
return "", app.For(code.AI).InputMissingRequired("missing AI provider token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/model/ai/domain/enum"
|
||||
aiuc "haixun-backend/internal/model/ai/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type ListAIProviderModelsLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewListAIProviderModelsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListAIProviderModelsLogic {
|
||||
return &ListAIProviderModelsLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ListAIProviderModelsLogic) ListAIProviderModels(req *types.AIProviderPath, token string) (*types.AIProviderModelsData, error) {
|
||||
result := l.svcCtx.AI.ListProviderModels(l.ctx, enum.ProviderID(req.Provider), aiuc.Credential{APIKey: token})
|
||||
return &types.AIProviderModelsData{
|
||||
ID: result.ID,
|
||||
Label: result.Label,
|
||||
Models: result.Models,
|
||||
Streams: result.Streams,
|
||||
Error: result.Error,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type ListAIProvidersLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewListAIProvidersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListAIProvidersLogic {
|
||||
return &ListAIProvidersLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ListAIProvidersLogic) ListAIProviders() (*types.AIProvidersData, error) {
|
||||
options := l.svcCtx.AI.ListProviders(l.ctx)
|
||||
items := make([]types.AIProviderOption, 0, len(options))
|
||||
for _, option := range options {
|
||||
items = append(items, types.AIProviderOption{
|
||||
ID: option.ID,
|
||||
Label: option.Label,
|
||||
Streams: option.Streams,
|
||||
})
|
||||
}
|
||||
return &types.AIProvidersData{Providers: items}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"haixun-backend/internal/model/ai/domain/enum"
|
||||
domai "haixun-backend/internal/model/ai/domain/usecase"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
func toGenerateRequest(req *types.AIChatReq, token string) domai.GenerateRequest {
|
||||
messages := make([]domai.Message, 0, len(req.Messages))
|
||||
for _, msg := range req.Messages {
|
||||
messages = append(messages, domai.Message{
|
||||
Role: msg.Role,
|
||||
Content: msg.Content,
|
||||
})
|
||||
}
|
||||
return domai.GenerateRequest{
|
||||
Provider: enum.ProviderID(req.Provider),
|
||||
Model: req.Model,
|
||||
Credential: domai.Credential{
|
||||
APIKey: token,
|
||||
},
|
||||
System: req.System,
|
||||
Messages: messages,
|
||||
Temperature: req.Temperature,
|
||||
MaxTokens: req.MaxTokens,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
memberusecase "haixun-backend/internal/model/member/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type LoginLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
|
||||
return &LoginLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *LoginLogic) Login(req *types.AuthLoginReq) (*types.AuthTokenData, error) {
|
||||
_, token, err := l.svcCtx.Member.Login(l.ctx, memberusecase.LoginRequest{
|
||||
TenantID: req.TenantID,
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
})
|
||||
return toAuthTokenData(token), err
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/middleware"
|
||||
authusecase "haixun-backend/internal/model/auth/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type LogoutLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewLogoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogoutLogic {
|
||||
return &LogoutLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *LogoutLogic) Logout(authorization string) (*types.LogoutData, error) {
|
||||
err := l.svcCtx.AuthToken.Logout(l.ctx, authusecase.LogoutRequest{
|
||||
AccessToken: middleware.BearerToken(authorization),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.LogoutData{OK: true}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
memberusecase "haixun-backend/internal/model/member/domain/usecase"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
func toAuthTokenData(token *memberusecase.AuthToken) *types.AuthTokenData {
|
||||
if token == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.AuthTokenData{
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
ExpiresIn: token.ExpiresIn,
|
||||
UID: token.UID,
|
||||
TokenType: token.TokenType,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type RefreshLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewRefreshLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshLogic {
|
||||
return &RefreshLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *RefreshLogic) Refresh(req *types.AuthRefreshReq) (*types.AuthTokenData, error) {
|
||||
pair, err := l.svcCtx.AuthToken.Refresh(l.ctx, req.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.AuthTokenData{
|
||||
AccessToken: pair.AccessToken,
|
||||
RefreshToken: pair.RefreshToken,
|
||||
ExpiresIn: pair.ExpiresIn,
|
||||
UID: pair.UID,
|
||||
TokenType: pair.TokenType,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
memberusecase "haixun-backend/internal/model/member/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type RegisterLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
|
||||
return &RegisterLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *RegisterLogic) Register(req *types.AuthRegisterReq) (*types.AuthTokenData, error) {
|
||||
_, token, err := l.svcCtx.Member.Register(l.ctx, memberusecase.RegisterRequest{
|
||||
TenantID: req.TenantID,
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
DisplayName: req.DisplayName,
|
||||
Language: req.Language,
|
||||
})
|
||||
return toAuthTokenData(token), err
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
domusecase "haixun-backend/internal/model/job/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type CancelJobLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCancelJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelJobLogic {
|
||||
return &CancelJobLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *CancelJobLogic) CancelJob(req *types.CancelJobReq) (*types.JobData, error) {
|
||||
run, err := l.svcCtx.Job.RequestCancel(l.ctx, domusecase.CancelRunRequest{
|
||||
JobID: req.ID,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobData(run)
|
||||
return &data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
domusecase "haixun-backend/internal/model/job/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type CreateJobLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCreateJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateJobLogic {
|
||||
return &CreateJobLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *CreateJobLogic) CreateJob(req *types.CreateJobReq) (*types.JobData, error) {
|
||||
run, err := l.svcCtx.Job.CreateRun(l.ctx, domusecase.CreateRunRequest{
|
||||
TemplateType: req.TemplateType,
|
||||
Scope: req.Scope,
|
||||
ScopeID: req.ScopeID,
|
||||
Payload: req.Payload,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobData(run)
|
||||
return &data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
domusecase "haixun-backend/internal/model/job/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type CreateJobScheduleLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCreateJobScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateJobScheduleLogic {
|
||||
return &CreateJobScheduleLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *CreateJobScheduleLogic) CreateJobSchedule(req *types.CreateJobScheduleReq) (*types.JobScheduleData, error) {
|
||||
schedule, err := l.svcCtx.Job.CreateSchedule(l.ctx, domusecase.CreateScheduleRequest{
|
||||
TemplateType: req.TemplateType,
|
||||
Scope: req.Scope,
|
||||
ScopeID: req.ScopeID,
|
||||
Cron: req.Cron,
|
||||
Timezone: req.Timezone,
|
||||
PayloadTemplate: req.PayloadTemplate,
|
||||
Enabled: req.Enabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobScheduleData(schedule)
|
||||
return &data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type DisableJobScheduleLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewDisableJobScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DisableJobScheduleLogic {
|
||||
return &DisableJobScheduleLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *DisableJobScheduleLogic) DisableJobSchedule(req *types.JobScheduleIDPath) (*types.JobScheduleData, error) {
|
||||
schedule, err := l.svcCtx.Job.DisableSchedule(l.ctx, req.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobScheduleData(schedule)
|
||||
return &data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type EnableJobScheduleLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewEnableJobScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *EnableJobScheduleLogic {
|
||||
return &EnableJobScheduleLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *EnableJobScheduleLogic) EnableJobSchedule(req *types.JobScheduleIDPath) (*types.JobScheduleData, error) {
|
||||
schedule, err := l.svcCtx.Job.EnableSchedule(l.ctx, req.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobScheduleData(schedule)
|
||||
return &data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type GetJobLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetJobLogic {
|
||||
return &GetJobLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *GetJobLogic) GetJob(req *types.JobIDPath) (*types.JobData, error) {
|
||||
run, err := l.svcCtx.Job.GetRun(l.ctx, req.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobData(run)
|
||||
return &data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type GetJobTemplateLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetJobTemplateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetJobTemplateLogic {
|
||||
return &GetJobTemplateLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *GetJobTemplateLogic) GetJobTemplate(req *types.JobTemplatePath) (*types.JobTemplateData, error) {
|
||||
template, err := l.svcCtx.Job.GetTemplate(l.ctx, req.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobTemplateData(template)
|
||||
return &data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type ListJobEventsLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewListJobEventsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListJobEventsLogic {
|
||||
return &ListJobEventsLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ListJobEventsLogic) ListJobEvents(req *types.ListJobEventsReq) (*types.JobEventListData, error) {
|
||||
limit := req.Limit
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
events, err := l.svcCtx.Job.ListJobEvents(l.ctx, req.ID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]types.JobEventData, 0, len(events))
|
||||
for _, event := range events {
|
||||
list = append(list, toJobEventData(event))
|
||||
}
|
||||
return &types.JobEventListData{List: list}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type ListJobSchedulesLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewListJobSchedulesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListJobSchedulesLogic {
|
||||
return &ListJobSchedulesLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ListJobSchedulesLogic) ListJobSchedules(req *types.ListJobSchedulesReq) (*types.JobScheduleListData, error) {
|
||||
items, total, page, pageSize, totalPages, err := l.svcCtx.Job.ListSchedules(l.ctx, req.Scope, req.ScopeID, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]types.JobScheduleData, 0, len(items))
|
||||
for _, item := range items {
|
||||
list = append(list, toJobScheduleData(item))
|
||||
}
|
||||
return &types.JobScheduleListData{
|
||||
Pagination: types.PaginationData{
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
},
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type ListJobTemplatesLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewListJobTemplatesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListJobTemplatesLogic {
|
||||
return &ListJobTemplatesLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ListJobTemplatesLogic) ListJobTemplates() (*types.JobTemplateListData, error) {
|
||||
templates, err := l.svcCtx.Job.ListTemplates(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]types.JobTemplateData, 0, len(templates))
|
||||
for _, template := range templates {
|
||||
list = append(list, toJobTemplateData(template))
|
||||
}
|
||||
return &types.JobTemplateListData{List: list}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type ListJobsLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewListJobsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListJobsLogic {
|
||||
return &ListJobsLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ListJobsLogic) ListJobs(req *types.ListJobsReq) (*types.JobListData, error) {
|
||||
runs, total, page, pageSize, totalPages, err := l.svcCtx.Job.ListRuns(l.ctx, req.Scope, req.ScopeID, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]types.JobData, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
list = append(list, toJobData(run))
|
||||
}
|
||||
return &types.JobListData{
|
||||
Pagination: types.PaginationData{
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
},
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"haixun-backend/internal/model/job/domain/entity"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
func toJobTemplateData(template *entity.Template) types.JobTemplateData {
|
||||
steps := make([]types.JobTemplateStepData, 0, len(template.Steps))
|
||||
for _, step := range template.Steps {
|
||||
steps = append(steps, types.JobTemplateStepData{
|
||||
ID: step.ID,
|
||||
Name: step.Name,
|
||||
WorkerType: step.WorkerType,
|
||||
TimeoutSeconds: step.TimeoutSeconds,
|
||||
Cancelable: step.Cancelable,
|
||||
})
|
||||
}
|
||||
return types.JobTemplateData{
|
||||
Type: template.Type,
|
||||
Version: template.Version,
|
||||
Name: template.Name,
|
||||
Description: template.Description,
|
||||
Enabled: template.Enabled,
|
||||
Repeatable: template.Repeatable,
|
||||
ConcurrencyPolicy: template.ConcurrencyPolicy,
|
||||
DedupeKeys: template.DedupeKeys,
|
||||
TimeoutSeconds: template.TimeoutSeconds,
|
||||
CancelPolicy: types.JobCancelPolicyData{
|
||||
Supported: template.CancelPolicy.Supported,
|
||||
Mode: template.CancelPolicy.Mode,
|
||||
GraceSeconds: template.CancelPolicy.GraceSeconds,
|
||||
},
|
||||
RetryPolicy: types.JobRetryPolicyData{
|
||||
MaxAttempts: template.RetryPolicy.MaxAttempts,
|
||||
BackoffSeconds: template.RetryPolicy.BackoffSeconds,
|
||||
},
|
||||
Steps: steps,
|
||||
}
|
||||
}
|
||||
|
||||
func toJobData(run *entity.Run) types.JobData {
|
||||
steps := make([]types.JobStepProgressData, 0, len(run.Progress.Steps))
|
||||
for _, step := range run.Progress.Steps {
|
||||
steps = append(steps, types.JobStepProgressData{
|
||||
ID: step.ID,
|
||||
Status: string(step.Status),
|
||||
StartedAt: step.StartedAt,
|
||||
EndedAt: step.EndedAt,
|
||||
Message: step.Message,
|
||||
})
|
||||
}
|
||||
return types.JobData{
|
||||
ID: run.ID.Hex(),
|
||||
TemplateType: run.TemplateType,
|
||||
TemplateVersion: run.TemplateVersion,
|
||||
Scope: run.Scope,
|
||||
ScopeID: run.ScopeID,
|
||||
Status: string(run.Status),
|
||||
Phase: run.Phase,
|
||||
WorkerType: run.WorkerType,
|
||||
Payload: run.Payload,
|
||||
Progress: types.JobProgressData{
|
||||
Summary: run.Progress.Summary,
|
||||
Percentage: run.Progress.Percentage,
|
||||
Steps: steps,
|
||||
},
|
||||
Result: run.Result,
|
||||
Error: run.Error,
|
||||
Attempt: run.Attempt,
|
||||
MaxAttempts: run.MaxAttempts,
|
||||
CancelRequestedAt: run.CancelRequestedAt,
|
||||
CancelReason: run.CancelReason,
|
||||
StartedAt: run.StartedAt,
|
||||
CompletedAt: run.CompletedAt,
|
||||
CreateAt: run.CreateAt,
|
||||
UpdateAt: run.UpdateAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toJobScheduleData(schedule *entity.Schedule) types.JobScheduleData {
|
||||
return types.JobScheduleData{
|
||||
ID: schedule.ID.Hex(),
|
||||
TemplateType: schedule.TemplateType,
|
||||
Scope: schedule.Scope,
|
||||
ScopeID: schedule.ScopeID,
|
||||
Enabled: schedule.Enabled,
|
||||
Cron: schedule.Cron,
|
||||
Timezone: schedule.Timezone,
|
||||
PayloadTemplate: schedule.PayloadTemplate,
|
||||
LastRunAt: schedule.LastRunAt,
|
||||
NextRunAt: schedule.NextRunAt,
|
||||
CreateAt: schedule.CreateAt,
|
||||
UpdateAt: schedule.UpdateAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toJobEventData(event *entity.Event) types.JobEventData {
|
||||
return types.JobEventData{
|
||||
ID: event.ID.Hex(),
|
||||
JobID: event.JobID,
|
||||
Type: event.Type,
|
||||
From: event.From,
|
||||
To: event.To,
|
||||
Message: event.Message,
|
||||
Metadata: event.Metadata,
|
||||
CreateAt: event.CreateAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toTemplateSteps(steps []types.JobTemplateStepData) []entity.TemplateStep {
|
||||
out := make([]entity.TemplateStep, 0, len(steps))
|
||||
for _, step := range steps {
|
||||
out = append(out, entity.TemplateStep{
|
||||
ID: step.ID,
|
||||
Name: step.Name,
|
||||
WorkerType: step.WorkerType,
|
||||
TimeoutSeconds: step.TimeoutSeconds,
|
||||
Cancelable: step.Cancelable,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type RetryJobLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewRetryJobLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RetryJobLogic {
|
||||
return &RetryJobLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *RetryJobLogic) RetryJob(req *types.JobIDPath) (*types.JobData, error) {
|
||||
run, err := l.svcCtx.Job.RetryRun(l.ctx, req.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobData(run)
|
||||
return &data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
domusecase "haixun-backend/internal/model/job/domain/usecase"
|
||||
"haixun-backend/internal/svc"
|
||||
"haixun-backend/internal/types"
|
||||
)
|
||||
|
||||
type UpdateJobScheduleLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewUpdateJobScheduleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateJobScheduleLogic {
|
||||
return &UpdateJobScheduleLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *UpdateJobScheduleLogic) UpdateJobSchedule(req *types.UpdateJobScheduleReq) (*types.JobScheduleData, error) {
|
||||
schedule, err := l.svcCtx.Job.UpdateSchedule(l.ctx, domusecase.UpdateScheduleRequest{
|
||||
ID: req.ID,
|
||||
Cron: req.Cron,
|
||||
Timezone: req.Timezone,
|
||||
PayloadTemplate: req.PayloadTemplate,
|
||||
Enabled: req.Enabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := toJobScheduleData(schedule)
|
||||
return &data, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue