feat/add_search #1
|
|
@ -0,0 +1,15 @@
|
||||||
|
# --- secrets / env(不要 commit 實值)---
|
||||||
|
infra/.env
|
||||||
|
infra/etc/haixun.env
|
||||||
|
/opt/haixun/
|
||||||
|
|
||||||
|
# --- build 產物 ---
|
||||||
|
backend/bin/
|
||||||
|
frontend/dist/
|
||||||
|
|
||||||
|
# --- 依賴 ---
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# --- 其他 ---
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
287
Makefile
287
Makefile
|
|
@ -1,98 +1,229 @@
|
||||||
GO ?= go
|
# 巡樓 monorepo Makefile
|
||||||
GOFMT ?= gofmt
|
# 兩種模式:
|
||||||
GOCTL ?= goctl
|
# dev - 本機開發(docker 起 Mongo/Redis,go run / vite dev)
|
||||||
GO_ZERO_STYLE := go_zero
|
# prod - 建置產物(前端 dist + linux Go binary),並可部署成 systemd 服務 + nginx
|
||||||
API_ENTRY := ./generate/api/gateway.api
|
#
|
||||||
GOFILES := $(shell find . -name '*.go')
|
# 常用:
|
||||||
|
# make dev-infra # 起本機 Mongo/Redis
|
||||||
|
# make dev-backend # 跑 gateway (:8890)
|
||||||
|
# make dev-frontend # 跑前端 (:5173,proxy 到 :8890)
|
||||||
|
# make build # 產出 frontend/dist + backend/bin/{gateway,worker}
|
||||||
|
# sudo make install # 部署到目標主機(見 infra/README.md)
|
||||||
|
|
||||||
|
SHELL := /bin/bash
|
||||||
|
|
||||||
|
# --- 路徑 ---
|
||||||
|
BACKEND_DIR := backend
|
||||||
|
FRONTEND_DIR := frontend
|
||||||
|
INFRA_DIR := infra
|
||||||
|
BIN_DIR := $(BACKEND_DIR)/bin
|
||||||
|
|
||||||
|
# --- 部署目標(install 用)---
|
||||||
|
DEPLOY_ROOT := /opt/haixun
|
||||||
|
WEB_ROOT := /var/www/haixun
|
||||||
|
|
||||||
|
# --- 交叉編譯目標(在 mac 上 build 給 linux 主機)---
|
||||||
|
GOOS ?= linux
|
||||||
|
GOARCH ?= amd64
|
||||||
|
|
||||||
|
# --- docker compose ---
|
||||||
|
COMPOSE := docker compose -f $(INFRA_DIR)/docker-compose.yml --env-file $(INFRA_DIR)/.env
|
||||||
|
|
||||||
|
# --- dev 用 worker secret(對應 etc/gateway.yaml 的 InternalWorker.Secret)---
|
||||||
|
DEV_WORKER_SECRET := haixun-dev-worker-secret
|
||||||
|
DEV_BACKEND_URL := http://127.0.0.1:8890
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
help: ## 顯示可用指令
|
help: ## 顯示可用指令
|
||||||
@echo "Haixun Backend"
|
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
||||||
|
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DEV
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
$(INFRA_DIR)/.env:
|
||||||
|
@test -f $(INFRA_DIR)/.env || (cp $(INFRA_DIR)/.env.example $(INFRA_DIR)/.env && echo "已從 .env.example 建立 $(INFRA_DIR)/.env,請視需要修改密碼")
|
||||||
|
|
||||||
|
.PHONY: dev-infra
|
||||||
|
dev-infra: $(INFRA_DIR)/.env ## [dev] 起本機 Mongo + Redis (docker)
|
||||||
|
$(COMPOSE) up -d
|
||||||
|
$(COMPOSE) ps
|
||||||
|
|
||||||
|
.PHONY: dev-infra-down
|
||||||
|
dev-infra-down: ## [dev] 停掉本機 Mongo + Redis
|
||||||
|
$(COMPOSE) down
|
||||||
|
|
||||||
|
.PHONY: dev-backend
|
||||||
|
dev-backend: ## [dev] 跑 gateway API (:8890)
|
||||||
|
cd $(BACKEND_DIR) && go run . -f etc/gateway.yaml
|
||||||
|
|
||||||
|
.PHONY: dev-worker
|
||||||
|
dev-worker: ## [dev] 跑 Go job worker (:8891)
|
||||||
|
cd $(BACKEND_DIR) && go run ./cmd/worker -f etc/gateway.worker.yaml
|
||||||
|
|
||||||
|
.PHONY: dev-node-worker
|
||||||
|
dev-node-worker: ## [dev] 跑 Node playwright worker (style-8d)
|
||||||
|
cd $(BACKEND_DIR)/worker && npm install && \
|
||||||
|
HAIXUN_WORKER_SECRET=$(DEV_WORKER_SECRET) HAIXUN_BACKEND_URL=$(DEV_BACKEND_URL) npm run style-8d
|
||||||
|
|
||||||
|
.PHONY: dev-frontend
|
||||||
|
dev-frontend: ## [dev] 跑前端 dev server (:5173)
|
||||||
|
cd $(FRONTEND_DIR) && npm install && npm run dev
|
||||||
|
|
||||||
|
.PHONY: dev-init
|
||||||
|
dev-init: ## [dev] 初始化 DB(索引/權限)並建立 admin 帳號(可用 INIT_ADMIN_EMAIL / INIT_ADMIN_PASSWORD 覆寫)
|
||||||
|
cd $(BACKEND_DIR) && go run ./cmd/tool init -f etc/gateway.yaml
|
||||||
|
|
||||||
|
.PHONY: dev
|
||||||
|
dev: ## [dev] 顯示本機開發要開的終端
|
||||||
|
@echo "本機開發請分開幾個終端執行:"
|
||||||
|
@echo " 1) make dev-infra # Mongo/Redis"
|
||||||
|
@echo " 2) make dev-backend # gateway :8890"
|
||||||
|
@echo " 3) make dev-worker # go worker(可選)"
|
||||||
|
@echo " 4) make dev-node-worker # node worker(需 style-8d 時)"
|
||||||
|
@echo " 5) make dev-frontend # 前端 :5173"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 安裝依賴(新機器 clone 後執行一次)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
.PHONY: bootstrap-sys
|
||||||
|
bootstrap-sys: ## [需 sudo] 安裝系統依賴(Go, Node.js, Docker, Nginx)— Ubuntu 專用
|
||||||
|
@echo "=== 安裝系統套件: nginx, curl, gnupg ==="
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq nginx curl gnupg ca-certificates
|
||||||
|
@echo ""
|
||||||
|
@echo "=== 安裝 Go 1.22+ ==="
|
||||||
|
@GO_VER=$$(curl -sL https://go.dev/VERSION?m=text 2>/dev/null | head -1 || echo "go1.22"); \
|
||||||
|
echo "下載 $$GO_VER.linux-amd64.tar.gz"; \
|
||||||
|
curl -sL "https://go.dev/dl/$$GO_VER.linux-amd64.tar.gz" -o /tmp/go.tar.gz && \
|
||||||
|
sudo rm -rf /usr/local/go && \
|
||||||
|
sudo tar -C /usr/local -xzf /tmp/go.tar.gz && \
|
||||||
|
rm /tmp/go.tar.gz; \
|
||||||
|
echo 'export PATH=/usr/local/go/bin:$$PATH' | sudo tee /etc/profile.d/go.sh
|
||||||
|
@echo ""
|
||||||
|
@echo "=== 安裝 Node.js LTS ==="
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y -qq nodejs
|
||||||
|
@echo ""
|
||||||
|
@echo "=== 安裝 Docker Engine + Compose ==="
|
||||||
|
curl -fsSL https://get.docker.com | sudo sh
|
||||||
|
sudo usermod -aG docker $(USER) 2>/dev/null || true
|
||||||
|
@echo ""
|
||||||
|
@echo "=== 安裝 Nginx ==="
|
||||||
|
sudo apt-get install -y -qq nginx
|
||||||
|
@echo ""
|
||||||
|
@echo "✓ 系統依賴安裝完成。"
|
||||||
|
@echo " 登出再登入後,Go/Node/Docker 即可使用(group 變更生效)。"
|
||||||
|
@echo " 接著執行 make bootstrap 安裝專案層依賴。"
|
||||||
@echo ""
|
@echo ""
|
||||||
@grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf " make %-12s %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
tools: ## 安裝 goctl / goimports
|
.PHONY: bootstrap
|
||||||
@command -v $(GOCTL) >/dev/null 2>&1 || (echo ">> installing goctl" && $(GO) install github.com/zeromicro/go-zero/tools/goctl@latest)
|
bootstrap: ## 安裝專案層依賴(Go modules + npm + Playwright),需先執行 make bootstrap-sys
|
||||||
@command -v goimports >/dev/null 2>&1 || (echo ">> installing goimports" && $(GO) install golang.org/x/tools/cmd/goimports@latest)
|
@echo "=== [1/4] Go modules ==="
|
||||||
|
cd $(BACKEND_DIR) && go mod tidy
|
||||||
|
@echo ""
|
||||||
|
@echo "=== [2/4] 前端 npm ==="
|
||||||
|
cd $(FRONTEND_DIR) && npm install
|
||||||
|
@echo ""
|
||||||
|
@echo "=== [3/4] Node worker npm ==="
|
||||||
|
cd $(BACKEND_DIR)/worker && npm install
|
||||||
|
@echo ""
|
||||||
|
@echo "=== [4/4] Playwright 瀏覽器(chromium)+ 系統依賴 ==="
|
||||||
|
cd $(BACKEND_DIR)/worker && sudo npx playwright install --with-deps chromium
|
||||||
|
@echo ""
|
||||||
|
@echo "✓ 全裝完成。後續步驟:"
|
||||||
|
@echo " 1) make dev-infra # 起 Mongo/Redis (Docker)"
|
||||||
|
@echo " 2) make dev-init # 初始化 DB + 建立 admin"
|
||||||
|
@echo " 3) make dev-backend # 啟動 gateway (:8890)"
|
||||||
|
@echo " 4) sudo make install # 正式部署(含 systemd + nginx)"
|
||||||
|
|
||||||
gen-api: tools ## 由 .api 生成 handler / logic / types
|
# ============================================================
|
||||||
$(GOCTL) api go -api $(API_ENTRY) -dir . -style $(GO_ZERO_STYLE) -home generate/goctl
|
# BUILD (prod 產物)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
fmt: ## gofmt + goimports
|
.PHONY: build
|
||||||
$(GOFMT) -s -w $(GOFILES)
|
build: build-frontend build-backend ## [prod] 建置前端 dist 與 Go binary
|
||||||
@command -v goimports >/dev/null 2>&1 && goimports -w . || true
|
|
||||||
|
|
||||||
test: ## 執行測試
|
.PHONY: build-frontend
|
||||||
$(GO) test ./...
|
build-frontend: ## [prod] 前端靜態建置 (tsc + vite) -> frontend/dist
|
||||||
|
cd $(FRONTEND_DIR) && npm ci && npm run build
|
||||||
|
|
||||||
run: ## 啟動 API(前景)
|
.PHONY: build-backend
|
||||||
$(GO) run ./gateway.go -f etc/gateway.yaml
|
build-backend: ## [prod] 交叉編譯 gateway + worker -> backend/bin (linux)
|
||||||
|
cd $(BACKEND_DIR) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
||||||
|
go build -trimpath -ldflags "-s -w" -o bin/gateway .
|
||||||
|
cd $(BACKEND_DIR) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
||||||
|
go build -trimpath -ldflags "-s -w" -o bin/worker ./cmd/worker
|
||||||
|
cd $(BACKEND_DIR) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
||||||
|
go build -trimpath -ldflags "-s -w" -o bin/tool ./cmd/tool
|
||||||
|
@echo "binary 已輸出到 $(BIN_DIR)/(gateway / worker / tool)"
|
||||||
|
|
||||||
dev-all: ## 一鍵啟動 Mongo/Redis + API + 前端 + 8D worker(背景)
|
# ============================================================
|
||||||
bash scripts/start-all.sh
|
# PROD (部署)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
stop-all: ## 一鍵停止全部開發服務
|
.PHONY: prod-infra
|
||||||
bash scripts/stop-all.sh
|
prod-infra: $(INFRA_DIR)/.env ## [prod] 起 Mongo + Redis (docker,背景)
|
||||||
|
$(COMPOSE) up -d
|
||||||
|
|
||||||
restart-all: ## 一鍵重啟全部開發服務
|
.PHONY: prod-infra-down
|
||||||
bash scripts/restart-all.sh
|
prod-infra-down: ## [prod] 停掉 Mongo + Redis
|
||||||
|
$(COMPOSE) down
|
||||||
|
|
||||||
status-all: ## 查看全部開發服務狀態
|
.PHONY: prod-init
|
||||||
bash scripts/status-all.sh
|
prod-init: ## [prod] 目標主機初始化 DB + 建立 admin(讀 /opt/haixun/etc/haixun.env;可加 INIT_ADMIN_EMAIL/PASSWORD)
|
||||||
|
@test -x $(DEPLOY_ROOT)/bin/tool || (echo "缺少 $(DEPLOY_ROOT)/bin/tool,請先 make install" && exit 1)
|
||||||
|
set -a; . $(DEPLOY_ROOT)/etc/haixun.env; set +a; \
|
||||||
|
$(DEPLOY_ROOT)/bin/tool init -f $(DEPLOY_ROOT)/etc/gateway.prod.yaml
|
||||||
|
|
||||||
stop: stop-all ## 同 stop-all
|
.PHONY: install
|
||||||
|
install: ## [prod] 安裝 binary/前端/設定/systemd/nginx(需 root,在目標主機執行)
|
||||||
|
@test -f $(BIN_DIR)/gateway || (echo "缺少 $(BIN_DIR)/gateway,請先在能 build 的機器執行 make build" && exit 1)
|
||||||
|
@test -d $(FRONTEND_DIR)/dist || (echo "缺少 $(FRONTEND_DIR)/dist,請先 make build-frontend" && exit 1)
|
||||||
|
id haixun >/dev/null 2>&1 || useradd --system --home $(DEPLOY_ROOT) --shell /usr/sbin/nologin haixun
|
||||||
|
install -d $(DEPLOY_ROOT)/bin $(DEPLOY_ROOT)/etc $(DEPLOY_ROOT)/node-worker $(WEB_ROOT)
|
||||||
|
install -m 0755 $(BIN_DIR)/gateway $(DEPLOY_ROOT)/bin/gateway
|
||||||
|
install -m 0755 $(BIN_DIR)/worker $(DEPLOY_ROOT)/bin/worker
|
||||||
|
install -m 0755 $(BIN_DIR)/tool $(DEPLOY_ROOT)/bin/tool
|
||||||
|
install -m 0644 $(BACKEND_DIR)/etc/gateway.prod.yaml $(DEPLOY_ROOT)/etc/gateway.prod.yaml
|
||||||
|
install -m 0644 $(BACKEND_DIR)/etc/gateway.worker.prod.yaml $(DEPLOY_ROOT)/etc/gateway.worker.prod.yaml
|
||||||
|
rm -rf $(WEB_ROOT)/* && cp -r $(FRONTEND_DIR)/dist/* $(WEB_ROOT)/
|
||||||
|
cp -r $(BACKEND_DIR)/worker/* $(DEPLOY_ROOT)/node-worker/
|
||||||
|
cd $(DEPLOY_ROOT)/node-worker && npm ci && npx playwright install --with-deps chromium
|
||||||
|
install -m 0644 $(INFRA_DIR)/systemd/haixun-gateway.service /etc/systemd/system/haixun-gateway.service
|
||||||
|
install -m 0644 $(INFRA_DIR)/systemd/haixun-worker.service /etc/systemd/system/haixun-worker.service
|
||||||
|
install -m 0644 $(INFRA_DIR)/systemd/haixun-node-worker.service /etc/systemd/system/haixun-node-worker.service
|
||||||
|
install -m 0644 $(INFRA_DIR)/nginx/haixun.conf /etc/nginx/conf.d/haixun.conf
|
||||||
|
chown -R haixun:haixun $(DEPLOY_ROOT) $(WEB_ROOT)
|
||||||
|
@echo "----"
|
||||||
|
@echo "接著(只做一次)建立 secret 檔:"
|
||||||
|
@echo " cp $(INFRA_DIR)/etc/haixun.env.example $(DEPLOY_ROOT)/etc/haixun.env && chmod 600 $(DEPLOY_ROOT)/etc/haixun.env && sudoedit $(DEPLOY_ROOT)/etc/haixun.env"
|
||||||
|
@echo "再啟用服務:"
|
||||||
|
@echo " systemctl daemon-reload && systemctl enable --now haixun-gateway haixun-worker haixun-node-worker"
|
||||||
|
@echo " nginx -t && systemctl reload nginx"
|
||||||
|
|
||||||
restart: restart-all ## 同 restart-all
|
# ============================================================
|
||||||
|
# 驗證 / 維護
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
dev-8d: ## 一鍵啟動 API + Node 8D worker(前景,Ctrl+C 結束)
|
.PHONY: tidy
|
||||||
bash scripts/dev-with-style-8d.sh
|
tidy: ## go mod tidy
|
||||||
|
cd $(BACKEND_DIR) && go mod tidy
|
||||||
|
|
||||||
CONFIG ?= etc/gateway.yaml
|
.PHONY: fmt
|
||||||
INIT_TENANT ?= default
|
fmt: ## gofmt 後端
|
||||||
INIT_EMAIL ?= admin@30cm.net
|
cd $(BACKEND_DIR) && gofmt -w .
|
||||||
INIT_PASSWORD ?= Fafafa54088
|
|
||||||
|
|
||||||
tool-init: ## 初始化 Mongo indexes、預設權限與 admin 帳號
|
.PHONY: test
|
||||||
$(GO) run ./cmd/tool init -f $(CONFIG) -tenant $(INIT_TENANT) -email $(INIT_EMAIL) -password '$(INIT_PASSWORD)'
|
test: ## 跑後端測試
|
||||||
|
cd $(BACKEND_DIR) && go test ./...
|
||||||
|
|
||||||
tool: ## 執行 cmd/tool(例:make tool ARGS="init -f etc/gateway.yaml")
|
.PHONY: verify
|
||||||
$(GO) run ./cmd/tool $(ARGS)
|
verify: ## 後端 build/test + 前端 build + compose 語法
|
||||||
|
cd $(BACKEND_DIR) && go build ./... && go test ./...
|
||||||
web-install: ## 安裝前端依賴
|
cd $(FRONTEND_DIR) && npm ci && npm run build
|
||||||
cd web && npm install
|
$(COMPOSE) config >/dev/null && echo "docker compose config OK"
|
||||||
|
|
||||||
web-dev: web-install ## 啟動前端 dev server(proxy 到 :8890)
|
|
||||||
cd web && npm run dev
|
|
||||||
|
|
||||||
extension-pack: ## 打包 Chrome 擴充為 web/public/downloads/*.zip
|
|
||||||
bash scripts/package-extension.sh
|
|
||||||
|
|
||||||
web-build: web-install extension-pack ## 建置前端靜態檔
|
|
||||||
cd web && npm run build
|
|
||||||
|
|
||||||
node-worker-style-8d: ## 啟動 Node 8D 爬蟲 worker
|
|
||||||
cd .. && npm run worker:style-8d
|
|
||||||
|
|
||||||
check: fmt test ## 格式化並測試
|
|
||||||
|
|
||||||
prod: ## 一鍵啟動 production Docker(API + Web + workers,分身數見 deploy/.env)
|
|
||||||
bash scripts/prod-up.sh
|
|
||||||
|
|
||||||
prod-update: ## 只重建/重啟 API+Web+Workers;mongo/redis 不重啟,資料留在 volume
|
|
||||||
bash scripts/prod-update.sh
|
|
||||||
|
|
||||||
prod-deps: ## 只啟動 mongo+redis(named volume 持久化)
|
|
||||||
bash scripts/prod-deps.sh
|
|
||||||
|
|
||||||
prod-down: ## 停止 stack(不刪 volume;Mongo/Redis 資料保留)
|
|
||||||
bash scripts/prod-down.sh
|
|
||||||
|
|
||||||
prod-wipe-data: ## 停止並刪除 mongo/redis volume(危險,需輸入 yes)
|
|
||||||
bash scripts/prod-wipe-data.sh
|
|
||||||
|
|
||||||
prod-logs: ## 追蹤 production logs(可傳 service 名,例:make prod-logs ARGS=api)
|
|
||||||
bash scripts/prod-logs.sh $(ARGS)
|
|
||||||
|
|
||||||
prod-build: web-build ## 建置靜態前端 + production images(不啟動)
|
|
||||||
cd deploy && docker compose -f docker-compose.prod.yml build
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -36,15 +36,15 @@ func runInit(args []string) error {
|
||||||
fs := flag.NewFlagSet("init", flag.ExitOnError)
|
fs := flag.NewFlagSet("init", flag.ExitOnError)
|
||||||
configFile := fs.String("f", "etc/gateway.yaml", "config file")
|
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")
|
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")
|
email := fs.String("email", envOr("INIT_ADMIN_EMAIL", "admin@30cm.net"), "bootstrap admin email")
|
||||||
password := fs.String("password", envOr("INIT_ADMIN_PASSWORD", "Admin-Pass-1!"), "bootstrap admin password")
|
password := fs.String("password", envOr("INIT_ADMIN_PASSWORD", "Fafafa54088"), "bootstrap admin password")
|
||||||
displayName := fs.String("display-name", envOr("INIT_ADMIN_DISPLAY_NAME", "Admin"), "bootstrap admin display name")
|
displayName := fs.String("display-name", envOr("INIT_ADMIN_DISPLAY_NAME", "Admin"), "bootstrap admin display name")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg config.Config
|
var cfg config.Config
|
||||||
conf.MustLoad(*configFile, &cfg)
|
conf.MustLoad(*configFile, &cfg, conf.UseEnv())
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
@ -20,7 +20,7 @@ func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
var c config.Config
|
var c config.Config
|
||||||
conf.MustLoad(*configFile, &c)
|
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
||||||
if !c.JobWorker.Enabled {
|
if !c.JobWorker.Enabled {
|
||||||
fmt.Fprintln(os.Stderr, "[worker] JobWorker.Enabled must be true")
|
fmt.Fprintln(os.Stderr, "[worker] JobWorker.Enabled must be true")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -40,4 +40,4 @@ func main() {
|
||||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-ch
|
<-ch
|
||||||
fmt.Println("[worker] shutting down")
|
fmt.Println("[worker] shutting down")
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
# 全 Phase 新增功能說明
|
||||||
|
|
||||||
|
## Phase 1 — TA 精準度
|
||||||
|
|
||||||
|
### 1. Semantic Embedding Reranker
|
||||||
|
|
||||||
|
**ScanPost entity 新增欄位**(`internal/model/scan_post/domain/entity/post.go`):
|
||||||
|
|
||||||
|
| 欄位 | 型別 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `Embedding` | `[]float32` | OpenAI text-embedding-3-small 向量 |
|
||||||
|
| `SemanticScore` | `int` | 語意相似度分數(0–100) |
|
||||||
|
| `EngagementPredicted` | `int` | 預測互動率 |
|
||||||
|
| `AudienceQualityScore` | `int` | 受眾品質分數(0–100) |
|
||||||
|
| `AuthorID` | `string` | 作者 ID(Threads user ID) |
|
||||||
|
| `AuthorAvatar` | `string` | 作者頭像 URL |
|
||||||
|
| `AuthorFollowers` | `int` | 作者粉絲數 |
|
||||||
|
|
||||||
|
`ScanCandidate` struct 同步新增對應欄位(`internal/library/placement/dual_track.go`)。
|
||||||
|
|
||||||
|
### 2. Embedding Client
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/library/embedding/client.go
|
||||||
|
func NewClient(apiKey, model string) *Client
|
||||||
|
func (c *Client) Generate(ctx context.Context, text string) ([]float32, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 預設使用 OpenAI `text-embedding-3-small`(可降級 local BGE-M3)
|
||||||
|
- Scorer: `CosineSimilarity(a, b []float32) float64`
|
||||||
|
|
||||||
|
### 3. Placement Score V2
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/library/placement/dual_track.go
|
||||||
|
func computePlacementScore(post *ScanCandidate, member productContext, brand productMeta,
|
||||||
|
semanticScore, predictedEngagement, audienceQuality *int) int
|
||||||
|
```
|
||||||
|
|
||||||
|
整合三項 pointer 參數(nil 表示欄位不存在,跳過該項):
|
||||||
|
|
||||||
|
- **語意相似度**(`SemanticScore`):Embedding cosine → 0–100
|
||||||
|
- **預測互動率**(`EngagementPredicted`):like+reply → 對數正規化 0–100
|
||||||
|
- **受眾品質**(`AudienceQualityScore`):`computeAudienceQuality()` heuristic
|
||||||
|
|
||||||
|
權重:語意 20% / 互動 30% / 品質 10% / 舊有 productFit + solvedBy + recency 40%。
|
||||||
|
|
||||||
|
```go
|
||||||
|
func computeAudienceQuality(followers int) int
|
||||||
|
```
|
||||||
|
|
||||||
|
Heuristic:followers 在 500–50k 之間品質最佳,過低或過高遞減。
|
||||||
|
|
||||||
|
### 4. Creator Lookalike
|
||||||
|
|
||||||
|
**端點**: `POST /api/v1/scan-post/lookalike`(需 JWT)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "source_post_id": "uuid", "brand_id": "...", "limit": 10 }
|
||||||
|
|
||||||
|
→ { "list": [{ "post_id", "author_id", "author_name", "similarity", ... }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
**演算法**: cosine similarity(門檻 ≥0.3),同 brand 內找 embedding 最接近的貼文回傳。
|
||||||
|
|
||||||
|
| 檔案 | |
|
||||||
|
|------|--|
|
||||||
|
| Handler | `internal/handler/scan_post/lookalike_handler.go` |
|
||||||
|
| Logic | `internal/logic/scan_post/lookalike_logic.go` |
|
||||||
|
| `cosineSimilarity` | `internal/logic/scan_post/lookalike_logic.go` — 全 Go 實作 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 — 營運流程
|
||||||
|
|
||||||
|
### 2.1 Creator CRM
|
||||||
|
|
||||||
|
**完整四層 model** `crm_contact`:
|
||||||
|
|
||||||
|
```
|
||||||
|
internal/model/crm_contact/
|
||||||
|
domain/entity/entity.go
|
||||||
|
domain/repository/repository.go
|
||||||
|
domain/usecase/usecase.go
|
||||||
|
usecase/usecase.go
|
||||||
|
repository/mongo.go
|
||||||
|
```
|
||||||
|
|
||||||
|
**端點**: `/api/v1/crm/contacts`(需 JWT)
|
||||||
|
|
||||||
|
| Method | Path | 用途 |
|
||||||
|
|--------|------|------|
|
||||||
|
| GET | `/` | List(`page`/`pageSize`/`brand_id`/`status`/`tag`) |
|
||||||
|
| POST | `/` | 新增聯絡人(`author_id` + `author_name`) |
|
||||||
|
| GET | `/:id` | 取得單筆 |
|
||||||
|
| PATCH | `/:id` | 更新(`notes`/`tags`/`status`) |
|
||||||
|
| DELETE | `/:id` | 刪除 |
|
||||||
|
|
||||||
|
**CRMContact 狀態機**: `lead` → `contacted` → `responded` → `converted` / `archived`
|
||||||
|
|
||||||
|
### 2.2 Publish Analytics
|
||||||
|
|
||||||
|
**完整四層 model** `publish_analytics`:
|
||||||
|
|
||||||
|
**Checkpoint 常數**(`internal/model/publish_analytics/usecase/usecase.go`): `+1h`、`+24h`、`+7d`
|
||||||
|
|
||||||
|
**方法**:
|
||||||
|
```go
|
||||||
|
RecordCheckpoint(ctx, tenantID, ownerUID, summary) (*PublishAnalyticsSummary, error)
|
||||||
|
ListByMedia(ctx, tenantID, ownerUID, mediaID) ([]PublishAnalyticsSummary, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Worker handler** `publish-analytics`(step `check`):
|
||||||
|
|
||||||
|
呼叫 Threads API `GET /{media-id}?fields=like_count,reply_count,repost_count,quote_count` → 存入 `publish_analytics`。
|
||||||
|
|
||||||
|
### 2.3 Threads API Token 刷新
|
||||||
|
|
||||||
|
**底層 API**(`internal/library/threadsapi/media.go`):
|
||||||
|
```go
|
||||||
|
RefreshAccessToken(ctx, currentToken) (*TokenRefreshResult, error)
|
||||||
|
// GET /v1.0/refresh_access_token?grant_type=th_refresh_token
|
||||||
|
```
|
||||||
|
|
||||||
|
**儲存 Token**:
|
||||||
|
```go
|
||||||
|
// Repository
|
||||||
|
SaveAPIAccessToken(ctx, accountID, accessToken, expiresAt) (*entity.Secrets, error)
|
||||||
|
// Usecase
|
||||||
|
SaveAPIAccessToken(ctx, accountID, accessToken, expiresAt) (*SecretsData, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Worker handler** `refresh-threads-token`(step `refresh`):讀取 secrets → refresh → 儲存。
|
||||||
|
|
||||||
|
### 2.4 Rate Limiter
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/library/ratelimit/token_bucket.go
|
||||||
|
func NewTokenBucket(rate float64, burst int) *TokenBucket
|
||||||
|
func (tb *TokenBucket) Allow() bool
|
||||||
|
func (tb *TokenBucket) AllowN(n int) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
- In-memory token bucket(非 Redis),thread-safe
|
||||||
|
- `rate` = tokens/sec,`burst` = 最大 burst
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
### Job Templates
|
||||||
|
|
||||||
|
`internal/model/job/usecase/usecase.go` 新增兩個 template constants + 對應 `Ensure*` methods + `template()` functions:
|
||||||
|
|
||||||
|
| Template Type | Steps | Timeout | Retry |
|
||||||
|
|---------------|-------|---------|-------|
|
||||||
|
| `refresh-threads-token` | `refresh`(go) | 120s | 3 次(30/120/300s) |
|
||||||
|
| `publish-analytics` | `check`(go) | 120s | 2 次(60s) |
|
||||||
|
|
||||||
|
### Worker Handlers 註冊
|
||||||
|
|
||||||
|
`internal/svc/service_context.go` 中註冊:
|
||||||
|
|
||||||
|
```go
|
||||||
|
jobworker.RegisterRefreshThreadsTokenHandler(runner, RefreshThreadsTokenDeps{...})
|
||||||
|
jobworker.RegisterPublishAnalyticsHandler(runner, PublishAnalyticsDeps{...})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Worker Helper
|
||||||
|
|
||||||
|
```go
|
||||||
|
// internal/worker/job/expand_graph.go
|
||||||
|
func int64Field(payload map[string]any, key string) int64
|
||||||
|
```
|
||||||
|
|
||||||
|
從 job payload 讀 `int64`,支援 `float64` / `int64` / `int` / `json.Number`。
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
Name: haixun-backend
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
# 連線字串與所有 secret 都從環境變數注入(systemd EnvironmentFile=/opt/haixun/etc/haixun.env)。
|
||||||
|
# go-zero 以 conf.UseEnv() + os.ExpandEnv 展開 ${VAR};未設定的變數會展開為空字串並讓服務 fail fast。
|
||||||
|
Mongo:
|
||||||
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
|
Database: ${HAIXUN_MONGO_DB}
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: ${HAIXUN_REDIS_ADDR}
|
||||||
|
Password: ${HAIXUN_REDIS_PASSWORD}
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: ${HAIXUN_JWT_ACCESS_SECRET}
|
||||||
|
RefreshSecret: ${HAIXUN_JWT_REFRESH_SECRET}
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
Secrets:
|
||||||
|
EncryptionKey: ${HAIXUN_SECRETS_KEY}
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: false
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -5,20 +5,24 @@ Timeout: 120000
|
||||||
|
|
||||||
Mongo:
|
Mongo:
|
||||||
URI: ${HAIXUN_MONGO_URI}
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
Database: ${HAIXUN_MONGO_DATABASE}
|
Database: ${HAIXUN_MONGO_DB}
|
||||||
TimeoutSeconds: 10
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
Redis:
|
Redis:
|
||||||
Addr: ${HAIXUN_REDIS_ADDR}
|
Addr: ${HAIXUN_REDIS_ADDR}
|
||||||
|
Password: ${HAIXUN_REDIS_PASSWORD}
|
||||||
DB: 0
|
DB: 0
|
||||||
|
|
||||||
Auth:
|
Auth:
|
||||||
AccessSecret: ${HAIXUN_AUTH_ACCESS_SECRET}
|
AccessSecret: ${HAIXUN_JWT_ACCESS_SECRET}
|
||||||
RefreshSecret: ${HAIXUN_AUTH_REFRESH_SECRET}
|
RefreshSecret: ${HAIXUN_JWT_REFRESH_SECRET}
|
||||||
AccessExpireSeconds: 900
|
AccessExpireSeconds: 900
|
||||||
RefreshExpireSeconds: 2592000
|
RefreshExpireSeconds: 2592000
|
||||||
DevHeaderFallback: false
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
Secrets:
|
||||||
|
EncryptionKey: ${HAIXUN_SECRETS_KEY}
|
||||||
|
|
||||||
InternalWorker:
|
InternalWorker:
|
||||||
Secret: ${HAIXUN_WORKER_SECRET}
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
|
|
@ -32,4 +36,4 @@ JobScheduler:
|
||||||
|
|
||||||
JobReaper:
|
JobReaper:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
IntervalSeconds: 30
|
IntervalSeconds: 30
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
Name: haixun-backend
|
Name: haixun-worker
|
||||||
Host: 0.0.0.0
|
Host: 0.0.0.0
|
||||||
Port: 8890
|
Port: 8891
|
||||||
Timeout: 120000
|
Timeout: 120000
|
||||||
|
|
||||||
|
# 本機開發 worker 設定(go worker)。搭配 `make dev-infra` 的 Mongo/Redis。
|
||||||
Mongo:
|
Mongo:
|
||||||
URI: mongodb://127.0.0.1:27017
|
URI: mongodb://haixun:change-me-mongo-pass@127.0.0.1:27017/?authSource=admin
|
||||||
Database: haixun
|
Database: haixun
|
||||||
TimeoutSeconds: 10
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
Redis:
|
Redis:
|
||||||
Addr: 127.0.0.1:6379
|
Addr: 127.0.0.1:6379
|
||||||
|
Password: change-me-redis-pass
|
||||||
DB: 0
|
DB: 0
|
||||||
|
|
||||||
Auth:
|
Auth:
|
||||||
|
|
@ -17,16 +19,22 @@ Auth:
|
||||||
RefreshSecret: haixun-dev-refresh-secret-change-me
|
RefreshSecret: haixun-dev-refresh-secret-change-me
|
||||||
AccessExpireSeconds: 900
|
AccessExpireSeconds: 900
|
||||||
RefreshExpireSeconds: 2592000
|
RefreshExpireSeconds: 2592000
|
||||||
DevHeaderFallback: true
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
Secrets:
|
||||||
|
EncryptionKey: ""
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: haixun-dev-worker-secret
|
||||||
|
|
||||||
JobWorker:
|
JobWorker:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
WorkerType: go
|
WorkerType: go
|
||||||
|
|
||||||
JobScheduler:
|
JobScheduler:
|
||||||
Enabled: true
|
Enabled: false
|
||||||
IntervalSeconds: 60
|
IntervalSeconds: 60
|
||||||
|
|
||||||
JobReaper:
|
JobReaper:
|
||||||
Enabled: true
|
Enabled: false
|
||||||
IntervalSeconds: 30
|
IntervalSeconds: 30
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
Name: haixun-backend
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
# 本機開發設定。預設搭配 `make dev-infra`(infra/docker-compose.yml)跑的 Mongo/Redis,
|
||||||
|
# 帳密對應 infra/.env.example 的預設值。若你改了 .env 密碼,這裡也要同步。
|
||||||
|
Mongo:
|
||||||
|
URI: mongodb://haixun:change-me-mongo-pass@127.0.0.1:27017/?authSource=admin
|
||||||
|
Database: haixun
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: 127.0.0.1:6379
|
||||||
|
Password: change-me-redis-pass
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: haixun-dev-access-secret-change-me
|
||||||
|
RefreshSecret: haixun-dev-refresh-secret-change-me
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
# 僅本機開發開啟:允許用 X-Tenant-ID / X-UID header 模擬登入。正式環境必須為 false。
|
||||||
|
DevHeaderFallback: true
|
||||||
|
|
||||||
|
Secrets:
|
||||||
|
# 留空 = 不加密(本機開發方便)。正式環境用 ${HAIXUN_SECRETS_KEY}。
|
||||||
|
EncryptionKey: ""
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: haixun-dev-worker-secret
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: true
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -19,7 +19,7 @@ func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
var c config.Config
|
var c config.Config
|
||||||
conf.MustLoad(*configFile, &c)
|
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
||||||
|
|
||||||
server := rest.MustNewServer(c.RestConf)
|
server := rest.MustNewServer(c.RestConf)
|
||||||
defer server.Stop()
|
defer server.Stop()
|
||||||
|
|
@ -115,6 +115,28 @@ type (
|
||||||
ThreadsAccountPath
|
ThreadsAccountPath
|
||||||
UpdateThreadsAccountAiSettingsReq
|
UpdateThreadsAccountAiSettingsReq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThreadsAccountAiProviderPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
Provider string `path:"provider" validate:"required,oneof=opencode-go xai"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListThreadsAccountAiProviderModelsReq {
|
||||||
|
ApiKey string `json:"api_key,optional"` // 選填;未帶則用已儲存的 key
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAccountAiProviderModelsHandlerReq {
|
||||||
|
ThreadsAccountAiProviderPath
|
||||||
|
ListThreadsAccountAiProviderModelsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAccountAiProviderModelsData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Models []string `json:"models"`
|
||||||
|
Streams bool `json:"streams"`
|
||||||
|
Error string `json:"error,optional"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server(
|
@server(
|
||||||
|
|
@ -154,4 +176,7 @@ service gateway {
|
||||||
|
|
||||||
@handler updateThreadsAccountAiSettings
|
@handler updateThreadsAccountAiSettings
|
||||||
put /:id/ai-settings (UpdateThreadsAccountAiSettingsHandlerReq) returns (ThreadsAccountAiSettingsData)
|
put /:id/ai-settings (UpdateThreadsAccountAiSettingsHandlerReq) returns (ThreadsAccountAiSettingsData)
|
||||||
|
|
||||||
|
@handler listThreadsAccountAiProviderModels
|
||||||
|
post /:id/ai-settings/providers/:provider/models (ThreadsAccountAiProviderModelsHandlerReq) returns (ThreadsAccountAiProviderModelsData)
|
||||||
}
|
}
|
||||||
|
|
@ -5,12 +5,23 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"haixun-backend/internal/config"
|
"haixun-backend/internal/config"
|
||||||
|
libcrypto "haixun-backend/internal/library/crypto"
|
||||||
libmongo "haixun-backend/internal/library/mongo"
|
libmongo "haixun-backend/internal/library/mongo"
|
||||||
|
brandrepo "haixun-backend/internal/model/brand/repository"
|
||||||
|
cmatrixrepo "haixun-backend/internal/model/content_matrix/repository"
|
||||||
|
copydraftrepo "haixun-backend/internal/model/copy_draft/repository"
|
||||||
|
copymissionrepo "haixun-backend/internal/model/copy_mission/repository"
|
||||||
jobrepo "haixun-backend/internal/model/job/repository"
|
jobrepo "haixun-backend/internal/model/job/repository"
|
||||||
|
kgrepo "haixun-backend/internal/model/knowledge_graph/repository"
|
||||||
memberrepo "haixun-backend/internal/model/member/repository"
|
memberrepo "haixun-backend/internal/model/member/repository"
|
||||||
|
outreachdraftrepo "haixun-backend/internal/model/outreach_draft/repository"
|
||||||
permissionrepo "haixun-backend/internal/model/permission/repository"
|
permissionrepo "haixun-backend/internal/model/permission/repository"
|
||||||
permissionuc "haixun-backend/internal/model/permission/usecase"
|
permissionuc "haixun-backend/internal/model/permission/usecase"
|
||||||
|
personarepo "haixun-backend/internal/model/persona/repository"
|
||||||
|
placementtopicrepo "haixun-backend/internal/model/placement_topic/repository"
|
||||||
|
scanpostrepo "haixun-backend/internal/model/scan_post/repository"
|
||||||
settingrepo "haixun-backend/internal/model/setting/repository"
|
settingrepo "haixun-backend/internal/model/setting/repository"
|
||||||
|
threadsaccountrepo "haixun-backend/internal/model/threads_account/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InitOptions struct {
|
type InitOptions struct {
|
||||||
|
|
@ -48,6 +59,12 @@ func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport
|
||||||
db := mongoClient.Database()
|
db := mongoClient.Database()
|
||||||
report := &InitReport{}
|
report := &InitReport{}
|
||||||
|
|
||||||
|
// cipher 只用於資料加解密;EnsureIndexes 不會用到,但 secrets repo 建構子需要它。
|
||||||
|
secretsCipher, err := libcrypto.New(cfg.Secrets.EncryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init secrets cipher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
settingRepository := settingrepo.NewMongoRepository(db)
|
settingRepository := settingrepo.NewMongoRepository(db)
|
||||||
memberRepository := memberrepo.NewMongoRepository(db)
|
memberRepository := memberrepo.NewMongoRepository(db)
|
||||||
permissionRepository := permissionrepo.NewMongoPermissionRepository(db)
|
permissionRepository := permissionrepo.NewMongoPermissionRepository(db)
|
||||||
|
|
@ -69,6 +86,17 @@ func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport
|
||||||
{"job_runs", jobRunRepository.EnsureIndexes},
|
{"job_runs", jobRunRepository.EnsureIndexes},
|
||||||
{"job_schedules", jobScheduleRepository.EnsureIndexes},
|
{"job_schedules", jobScheduleRepository.EnsureIndexes},
|
||||||
{"job_events", jobEventRepository.EnsureIndexes},
|
{"job_events", jobEventRepository.EnsureIndexes},
|
||||||
|
{"copy_missions", copymissionrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"copy_drafts", copydraftrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"scan_posts", scanpostrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"outreach_drafts", outreachdraftrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"content_matrix", cmatrixrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"knowledge_graph", kgrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"personas", personarepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"brands", brandrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"placement_topics", placementtopicrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"threads_accounts", threadsaccountrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"threads_account_secrets", threadsaccountrepo.NewSecretsMongoRepository(db, secretsCipher).EnsureIndexes},
|
||||||
}
|
}
|
||||||
for _, repo := range repos {
|
for _, repo := range repos {
|
||||||
if err := repo.fn(ctx); err != nil {
|
if err := repo.fn(ctx); err != nil {
|
||||||
|
|
@ -9,8 +9,9 @@ type MongoConf struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedisConf struct {
|
type RedisConf struct {
|
||||||
Addr string `json:",optional"`
|
Addr string `json:",optional"`
|
||||||
DB int `json:",optional"`
|
Password string `json:",optional"`
|
||||||
|
DB int `json:",optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobWorkerConf struct {
|
type JobWorkerConf struct {
|
||||||
|
|
@ -34,13 +35,19 @@ type AuthConf struct {
|
||||||
RefreshSecret string `json:",optional"`
|
RefreshSecret string `json:",optional"`
|
||||||
AccessExpireSeconds int64 `json:",default=900"`
|
AccessExpireSeconds int64 `json:",default=900"`
|
||||||
RefreshExpireSeconds int64 `json:",default=2592000"`
|
RefreshExpireSeconds int64 `json:",default=2592000"`
|
||||||
DevHeaderFallback bool `json:",default=true"`
|
DevHeaderFallback bool `json:",default=false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InternalWorkerConf struct {
|
type InternalWorkerConf struct {
|
||||||
Secret string `json:",optional"`
|
Secret string `json:",optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecretsConf holds the application-layer encryption key (base64 of 32 bytes)
|
||||||
|
// used to encrypt sensitive data at rest (browser session, third-party API keys).
|
||||||
|
type SecretsConf struct {
|
||||||
|
EncryptionKey string `json:",optional"`
|
||||||
|
}
|
||||||
|
|
||||||
type BraveConf struct {
|
type BraveConf struct {
|
||||||
APIKey string `json:",optional"`
|
APIKey string `json:",optional"`
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +57,7 @@ type Config struct {
|
||||||
Mongo MongoConf `json:",optional"`
|
Mongo MongoConf `json:",optional"`
|
||||||
Redis RedisConf `json:",optional"`
|
Redis RedisConf `json:",optional"`
|
||||||
Auth AuthConf `json:",optional"`
|
Auth AuthConf `json:",optional"`
|
||||||
|
Secrets SecretsConf `json:",optional"`
|
||||||
InternalWorker InternalWorkerConf `json:",optional"`
|
InternalWorker InternalWorkerConf `json:",optional"`
|
||||||
JobWorker JobWorkerConf `json:",optional"`
|
JobWorker JobWorkerConf `json:",optional"`
|
||||||
JobScheduler JobSchedulerConf `json:",optional"`
|
JobScheduler JobSchedulerConf `json:",optional"`
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package crm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/crm"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateCrmContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreateCrmContactReq
|
||||||
|
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 := crm.NewCreateCrmContactLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreateCrmContact(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package crm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/crm"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteCrmContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CrmContactIDPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := crm.NewDeleteCrmContactLogic(r.Context(), svcCtx)
|
||||||
|
err := l.DeleteCrmContact(&req)
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package crm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/crm"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCrmContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CrmContactIDPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := crm.NewGetCrmContactLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GetCrmContact(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package crm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/crm"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListCrmContactsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.ListCrmContactsReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := crm.NewListCrmContactsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListCrmContacts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package crm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/crm"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateCrmContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpdateCrmContactReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := crm.NewUpdateCrmContactLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.UpdateCrmContact(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue