Compare commits

...

5 Commits

Author SHA1 Message Date
王性驊 ef6281ef83 fix 2026-06-26 16:35:47 +00:00
王性驊 29d0aca295 fix2 2026-06-27 00:15:29 +08:00
王性驊 ffd6a31d6f fix2 2026-06-27 00:02:06 +08:00
王性驊 3342f7a344 fix floder 2026-06-26 20:09:36 +08:00
王性驊 8b0985f604 first commit 2026-06-26 16:56:07 +08:00
837 changed files with 1836 additions and 1683 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

15
.gitignore vendored Normal file
View File

@ -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
View File

@ -1,98 +1,229 @@
GO ?= go # 巡樓 monorepo Makefile
GOFMT ?= gofmt # 兩種模式:
GOCTL ?= goctl # dev - 本機開發docker 起 Mongo/Redisgo 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 # 跑前端 (:5173proxy 到 :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/toolmake 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 serverproxy 到 :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 DockerAPI + Web + workers分身數見 deploy/.env
bash scripts/prod-up.sh
prod-update: ## 只重建/重啟 API+Web+Workersmongo/redis 不重啟,資料留在 volume
bash scripts/prod-update.sh
prod-deps: ## 只啟動 mongo+redisnamed volume 持久化)
bash scripts/prod-deps.sh
prod-down: ## 停止 stack不刪 volumeMongo/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

BIN
backend/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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()

View File

@ -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")
} }

View File

@ -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

View File

@ -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

View File

@ -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

43
backend/etc/gateway.yaml Normal file
View File

@ -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

View File

@ -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()

View File

@ -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 {

View File

@ -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"`

Some files were not shown because too many files have changed in this diff Show More