11 Commits

Author SHA1 Message Date
zs
dc808628d3 feat: 扩展Api
All checks were successful
Extension Build & Release / build (push) Successful in 1m14s
2026-03-03 00:04:44 +08:00
zs
d2b330c0c9 feat: 管理后台登录
Some checks failed
Backend Deploy (Go + Docker) / deploy (push) Successful in 1m4s
Extension Build & Release / build (push) Failing after 46s
2026-03-02 23:54:59 +08:00
zs
4e5147fb13 feat: 后台打包测试部署
All checks were successful
Backend Deploy (Go + Docker) / deploy (push) Successful in 1m29s
2026-03-02 23:37:50 +08:00
zs
dba30d0ba3 feat: 后台打包测试部署
All checks were successful
Backend Deploy (Go + Docker) / deploy (push) Successful in 1m31s
2026-03-02 23:23:05 +08:00
zs
911117f240 feat: 后台打包测试部署
All checks were successful
Backend Deploy (Go + Docker) / deploy (push) Successful in 1m29s
2026-03-02 22:31:12 +08:00
zs
dfa5ba1c50 feat: 后台打包测试部署
All checks were successful
Backend Deploy (Go + Docker) / deploy (push) Successful in 54s
2026-03-02 22:07:57 +08:00
zs
f13f6a4d4b feat: 后台打包测试部署
All checks were successful
Backend Deploy (Go + Docker) / deploy (push) Successful in 1m29s
2026-03-02 22:01:25 +08:00
zs
1ca810ada8 feat: 后台打包测试部署
Some checks failed
Backend Deploy (Go + Docker) / deploy (push) Failing after 53s
2026-03-02 21:58:27 +08:00
zs
c0edfb629b feat: 后台打包测试部署
Some checks failed
Backend Deploy (Go + Docker) / deploy (push) Failing after 57s
2026-03-02 21:54:24 +08:00
zs
9c875afc95 feat: 后台打包测试部署
Some checks failed
Backend Deploy (Go + Docker) / deploy (push) Failing after 48s
2026-03-02 21:51:45 +08:00
zs
b98077813a feat: 后台打包测试部署
Some checks failed
Backend Deploy (Go + Docker) / deploy (push) Failing after 55s
2026-03-02 21:49:08 +08:00
15 changed files with 214 additions and 89 deletions

View File

@@ -5,6 +5,7 @@ on:
- main - main
paths: paths:
- 'server/**' - 'server/**'
- '.gitea/workflows/backend-deploy.yml'
jobs: jobs:
deploy: deploy:
@@ -23,7 +24,7 @@ jobs:
- name: 编译后端可执行文件 - name: 编译后端可执行文件
run: | run: |
cd server cd server
env GOOS=linux GOARCH=arm64 go build -o server_bin ./cmd/server CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o server_bin ./cmd/server
- name: 准备部署文件 - name: 准备部署文件
run: | run: |
@@ -31,6 +32,7 @@ jobs:
cp server/server_bin deploy/ cp server/server_bin deploy/
cp server/Dockerfile deploy/ cp server/Dockerfile deploy/
cp server/docker-compose.yml deploy/ cp server/docker-compose.yml deploy/
cp server/.env.example deploy/
- name: 部署文件到服务器 - name: 部署文件到服务器
uses: up9cloud/action-rsync@master uses: up9cloud/action-rsync@master
@@ -38,8 +40,8 @@ jobs:
USER: root USER: root
HOST: 144.24.60.0 HOST: 144.24.60.0
KEY: ${{secrets.USAARMLOGIN_SSH_KEY}} KEY: ${{secrets.USAARMLOGIN_SSH_KEY}}
ARGS: -avz --delete ARGS: -avz --delete --exclude '.env'
SSH_ARGS: "-p 22 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" SSH_ARGS: "-p 22 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ServerAliveCountMax=3"
SOURCE: ./deploy/ SOURCE: ./deploy/
TARGET: /var/admin/InsightReply/server/ TARGET: /var/admin/InsightReply/server/
VERBOSE: true VERBOSE: true
@@ -52,5 +54,22 @@ jobs:
key: ${{secrets.USAARMLOGIN_SSH_KEY}} key: ${{secrets.USAARMLOGIN_SSH_KEY}}
script: | script: |
cd /var/admin/InsightReply/server cd /var/admin/InsightReply/server
# 停止并重新构建启动容器 (利用刚同步过来的新 server_bin 和 Dockerfile) # 首次部署时,从模板创建 .env
docker-compose up -d --build if [ ! -f .env ]; then
echo "⚠️ .env not found, creating from .env.example"
cp .env.example .env
echo "📝 请在服务器上编辑 /var/admin/InsightReply/server/.env 填入真实密钥"
fi
sync
sleep 3
# 检查是否有正在移除的容器,等待其完成
for i in $(seq 1 10); do
if docker ps -a 2>/dev/null | grep -q 'Removal'; then
echo "Waiting for container removal... ($i/10)"
sleep 3
else
break
fi
done
# 使用 --remove-orphans 更彻底地清理,并直接 build up
docker compose up -d --build --remove-orphans

View File

@@ -6,6 +6,7 @@ on:
paths: paths:
- 'extension/**' - 'extension/**'
- '.gitea/workflows/extension-build.yml' - '.gitea/workflows/extension-build.yml'
- 'extension/.env'
jobs: jobs:
build: build:
@@ -42,7 +43,7 @@ jobs:
with: with:
files: insight-reply-extension.zip files: insight-reply-extension.zip
token: '${{secrets.GITEA_TOKEN}}' token: '${{secrets.GITEA_TOKEN}}'
name: 'Extension "Latest"' name: 'Extension Release #${{ gitea.run_number }}'
tag_name: 'latest-extension' tag_name: 'extension-v${{ gitea.run_number }}'
draft: false draft: false
prerelease: true prerelease: true

View File

@@ -25,6 +25,7 @@ InsightReply 采用现代化解耦的三端架构:
## 🛠️ 本地开发指南 (Local Development) ## 🛠️ 本地开发指南 (Local Development)
### 1. 启动 PostgreSQL 数据库 ### 1. 启动 PostgreSQL 数据库
> **注意**:本项目已禁用 Go 服务自动迁移。请使用本项目支持的 MCP 服务 (`InsightReply_PostgreSQL`) 或手动执行 `docs/schema.sql` 来同步数据库结构。
### 2. 配置与启动后端 (Go Server) ### 2. 配置与启动后端 (Go Server)
```bash ```bash
@@ -82,9 +83,10 @@ VITE_API_BASE_URL=https://insight.buildapp.eu.org/api/v1
```bash ```bash
cd /var/admin/InsightReply/server cd /var/admin/InsightReply/server
docker-compose up -d --build # 生产环境使用 docker compose (V2)
docker compose up -d --build --remove-orphans
``` ```
> 此时应用全局运行日志将自动映射并写入由于宿主机的 `/root/logs/InsightReply.log` 内以供探查。 > 此时应用将以 `network_mode: host` 模式运行,全局运行日志将自动映射并写入宿主机的 `/app/logs/InsightReply.log` 内以供探查。
### 3. Caddyfile 反向代理与 SSL 自动签发 ### 3. Caddyfile 反向代理与 SSL 自动签发
在宿主机中编辑 `/etc/caddy/Caddyfile`,配置以下动静分离策略: 在宿主机中编辑 `/etc/caddy/Caddyfile`,配置以下动静分离策略:

View File

@@ -55,9 +55,9 @@ CREATE TABLE IF NOT EXISTS tweets (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_tweets_x_tweet_id ON tweets(x_tweet_id); CREATE INDEX IF NOT EXISTS idx_tweets_x_tweet_id ON tweets(x_tweet_id);
CREATE INDEX idx_tweets_heat_score ON tweets(heat_score DESC); CREATE INDEX IF NOT EXISTS idx_tweets_heat_score ON tweets(heat_score DESC);
CREATE INDEX idx_tweets_crawl_queue ON tweets(crawl_queue, last_crawled_at); CREATE INDEX IF NOT EXISTS idx_tweets_crawl_queue ON tweets(crawl_queue, last_crawled_at);
-- generated_replies 表:生成的 AI 评论记录 -- generated_replies 表:生成的 AI 评论记录
CREATE TABLE IF NOT EXISTS generated_replies ( CREATE TABLE IF NOT EXISTS generated_replies (
@@ -71,8 +71,8 @@ CREATE TABLE IF NOT EXISTS generated_replies (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_generated_replies_user_id ON generated_replies(user_id); CREATE INDEX IF NOT EXISTS idx_generated_replies_user_id ON generated_replies(user_id);
CREATE INDEX idx_generated_replies_tweet_id ON generated_replies(tweet_id); CREATE INDEX IF NOT EXISTS idx_generated_replies_tweet_id ON generated_replies(tweet_id);
-- reply_performance 表:针对已发布评论的效果数据回拨 -- reply_performance 表:针对已发布评论的效果数据回拨
CREATE TABLE IF NOT EXISTS reply_performance ( CREATE TABLE IF NOT EXISTS reply_performance (
@@ -85,8 +85,8 @@ CREATE TABLE IF NOT EXISTS reply_performance (
check_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP check_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_reply_performance_reply_id ON reply_performance(reply_id); CREATE INDEX IF NOT EXISTS idx_reply_performance_reply_id ON reply_performance(reply_id);
CREATE INDEX idx_reply_performance_user_id ON reply_performance(user_id); CREATE INDEX IF NOT EXISTS idx_reply_performance_user_id ON reply_performance(user_id);
-- ==================================================== -- ====================================================
-- 新增表 (v1.1) -- 新增表 (v1.1)
@@ -106,8 +106,8 @@ CREATE TABLE IF NOT EXISTS api_usage_logs (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_api_usage_logs_user_id ON api_usage_logs(user_id); CREATE INDEX IF NOT EXISTS idx_api_usage_logs_user_id ON api_usage_logs(user_id);
CREATE INDEX idx_api_usage_logs_created_at ON api_usage_logs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_api_usage_logs_created_at ON api_usage_logs(created_at DESC);
-- subscriptions 表:用户订阅记录(支付历史) -- subscriptions 表:用户订阅记录(支付历史)
CREATE TABLE IF NOT EXISTS subscriptions ( CREATE TABLE IF NOT EXISTS subscriptions (
@@ -122,8 +122,8 @@ CREATE TABLE IF NOT EXISTS subscriptions (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status); CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
-- user_style_profiles 表:用户风格画像(用于个性化 Prompt -- user_style_profiles 表:用户风格画像(用于个性化 Prompt
CREATE TABLE IF NOT EXISTS user_style_profiles ( CREATE TABLE IF NOT EXISTS user_style_profiles (
@@ -147,7 +147,7 @@ CREATE TABLE IF NOT EXISTS crawl_snapshots (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_crawl_snapshots_created_at ON crawl_snapshots(created_at DESC); CREATE INDEX IF NOT EXISTS idx_crawl_snapshots_created_at ON crawl_snapshots(created_at DESC);
-- ==================================================== -- ====================================================
-- 新增表 (v1.2) — 用户可配置系统 -- 新增表 (v1.2) — 用户可配置系统
@@ -191,7 +191,7 @@ CREATE TABLE IF NOT EXISTS user_custom_strategies (
UNIQUE (user_id, strategy_key) UNIQUE (user_id, strategy_key)
); );
CREATE INDEX idx_user_custom_strategies_user_id ON user_custom_strategies(user_id); CREATE INDEX IF NOT EXISTS idx_user_custom_strategies_user_id ON user_custom_strategies(user_id);
-- competitor_monitors 表:竞品品牌监控(复用后端雷达,按品牌词自动抓取) -- competitor_monitors 表:竞品品牌监控(复用后端雷达,按品牌词自动抓取)
CREATE TABLE IF NOT EXISTS competitor_monitors ( CREATE TABLE IF NOT EXISTS competitor_monitors (
@@ -204,7 +204,7 @@ CREATE TABLE IF NOT EXISTS competitor_monitors (
UNIQUE (user_id, brand_name) UNIQUE (user_id, brand_name)
); );
CREATE INDEX idx_competitor_monitors_user_id ON competitor_monitors(user_id); CREATE INDEX IF NOT EXISTS idx_competitor_monitors_user_id ON competitor_monitors(user_id);
-- ==================================================== -- ====================================================
-- 触发器:自动更新 updated_at -- 触发器:自动更新 updated_at
@@ -219,12 +219,20 @@ END;
$$ language 'plpgsql'; $$ language 'plpgsql';
-- 为所有需要追踪更新时间的表添加触发器 -- 为所有需要追踪更新时间的表添加触发器
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_users_modtime') THEN
CREATE TRIGGER update_users_modtime CREATE TRIGGER update_users_modtime
BEFORE UPDATE ON users BEFORE UPDATE ON users
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_modified_column(); EXECUTE FUNCTION update_modified_column();
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_user_style_profiles_modtime') THEN
CREATE TRIGGER update_user_style_profiles_modtime CREATE TRIGGER update_user_style_profiles_modtime
BEFORE UPDATE ON user_style_profiles BEFORE UPDATE ON user_style_profiles
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_modified_column(); EXECUTE FUNCTION update_modified_column();
END IF;
END $$;

View File

@@ -1 +1 @@
VITE_API_BASE_URL=http://localhost:8080/api/v1 VITE_API_BASE_URL=https://insight.buildapp.eu.org/api/v1

View File

@@ -57,3 +57,7 @@ globs: *.go, *.sql
* 对于本项目的初步开发,推荐使用如 **`gorm`** 或 **`sqlx`** 进行快速的数据交互操作。 * 对于本项目的初步开发,推荐使用如 **`gorm`** 或 **`sqlx`** 进行快速的数据交互操作。
* 所有表名、字段名在 Go 结构体 (`struct`) 的 tag 中必须显式定义为下划线 (snake_case)。 * 所有表名、字段名在 Go 结构体 (`struct`) 的 tag 中必须显式定义为下划线 (snake_case)。
* UUID 作为主键,禁止前端或外部服务自行生成传入,一律由 PostgreSQL `gen_random_uuid()` 或者服务端生成。 * UUID 作为主键,禁止前端或外部服务自行生成传入,一律由 PostgreSQL `gen_random_uuid()` 或者服务端生成。
* **数据库迁移 (Migration)**:
* **禁用自动迁移**: 后端程序不再自动执行 `Up()` 迁移,所有变更需手动通过 MCP 或 DBA 工具执行。
* **幂等性**: 所有 SQL 脚本(如 `CREATE INDEX`, `CREATE TABLE`)必须包含 `IF NOT EXISTS` 保护。
* **触发器**: 创建触发器时必须先检查是否存在,避免重复定义导致部署中断。

View File

@@ -38,7 +38,7 @@ GEMINI_AVAILABLE_MODELS=gemini-2.5-flash,gemini-2.5-pro
JWT_SECRET=your-random-32-char-secret-key-here JWT_SECRET=your-random-32-char-secret-key-here
# ---- 服务器 ---- # ---- 服务器 ----
SERVER_PORT=8080 SERVER_PORT=8009
LOG_LEVEL=info LOG_LEVEL=info
# ---- CORS ---- # ---- CORS ----

View File

@@ -10,9 +10,8 @@ WORKDIR /app
COPY server_bin . COPY server_bin .
RUN chmod +x server_bin RUN chmod +x server_bin
# 拷贝数据库迁移文件 (服务启动时自动执行) # 数据库迁移现已通过 MCP 手动管理,不再打包进镜像
COPY migrations ./migrations
EXPOSE 8080 EXPOSE 8009
CMD ["./server_bin"] CMD ["./server_bin"]

View File

@@ -21,9 +21,6 @@ import (
"github.com/zs/InsightReply/internal/service" "github.com/zs/InsightReply/internal/service"
"github.com/zs/InsightReply/internal/worker" "github.com/zs/InsightReply/internal/worker"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -58,24 +55,14 @@ func main() {
} }
fmt.Println("Database connection established") fmt.Println("Database connection established")
// 2.1 Run Database Migrations
log.Println("Running database migrations...")
m, err := migrate.New("file://migrations", dsn)
if err != nil {
log.Printf("Failed to initialize migrate, skipping: %v", err)
} else {
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Printf("Failed to run migrate (maybe tables already exist), continuing: %v", err)
} else {
log.Println("Database migrations applied successfully")
}
}
// 3. Initialize Layers // 3. Initialize Layers
userRepo := repository.NewUserRepository(db) userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo) userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc) userHandler := handler.NewUserHandler(userSvc)
authSvc := service.NewAuthService(userRepo)
authHandler := handler.NewAuthHandler(authSvc)
profileRepo := repository.NewProductProfileRepository(db) profileRepo := repository.NewProductProfileRepository(db)
profileSvc := service.NewProductProfileService(profileRepo) profileSvc := service.NewProductProfileService(profileRepo)
profileHandler := handler.NewProductProfileHandler(profileSvc) profileHandler := handler.NewProductProfileHandler(profileSvc)
@@ -136,6 +123,7 @@ func main() {
r.Route("/api/v1", func(r chi.Router) { r.Route("/api/v1", func(r chi.Router) {
// Public routes // Public routes
r.Post("/users/register", userHandler.Register) r.Post("/users/register", userHandler.Register)
r.Post("/auth/login", authHandler.Login)
// Protected routes // Protected routes
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {

View File

@@ -1,22 +1,13 @@
version: '3.8'
services: services:
insight-reply-server: insight-reply-server:
build: . build: .
container_name: insight-reply-server container_name: insight-reply-server
restart: always restart: always
ports: network_mode: host
- "8009:8080"
env_file: env_file:
- .env - .env
environment: environment:
- LOG_FILE_PATH=/app/logs/InsightReply.log - LOG_FILE_PATH=/app/logs/InsightReply.log
- SERVER_PORT=8009
volumes: volumes:
# Map the host's /root/logs to the container's /app/logs directory
- /root/logs:/app/logs - /root/logs:/app/logs
networks:
- insight_network
networks:
insight_network:
driver: bridge

View File

@@ -0,0 +1,44 @@
package handler
import (
"encoding/json"
"net/http"
"github.com/zs/InsightReply/internal/service"
)
type AuthHandler struct {
svc *service.AuthService
}
func NewAuthHandler(svc *service.AuthService) *AuthHandler {
return &AuthHandler{svc: svc}
}
// Login handles user authentication and returns a JWT token
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var body struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
SendError(w, http.StatusBadRequest, 4001, "Invalid request body")
return
}
if body.Email == "" || body.Password == "" {
SendError(w, http.StatusBadRequest, 4001, "Email and Password are required")
return
}
token, err := h.svc.Login(body.Email, body.Password)
if err != nil {
SendError(w, http.StatusUnauthorized, 4001, err.Error())
return
}
SendSuccess(w, map[string]string{
"token": token,
})
}

View File

@@ -18,6 +18,7 @@ func NewUserHandler(svc *service.UserService) *UserHandler {
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
var body struct { var body struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"`
Identity string `json:"identity"` Identity string `json:"identity"`
} }
@@ -26,7 +27,7 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := h.svc.Register(body.Email, body.Identity) user, err := h.svc.Register(body.Email, body.Password, body.Identity)
if err != nil { if err != nil {
SendError(w, http.StatusInternalServerError, 5001, "Failed to register user") SendError(w, http.StatusInternalServerError, 5001, "Failed to register user")
return return

View File

@@ -0,0 +1,53 @@
package service
import (
"errors"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/zs/InsightReply/internal/repository"
"golang.org/x/crypto/bcrypt"
)
type AuthService struct {
userRepo *repository.UserRepository
}
func NewAuthService(userRepo *repository.UserRepository) *AuthService {
return &AuthService{userRepo: userRepo}
}
func (s *AuthService) Login(email, password string) (string, error) {
// 1. Fetch user by email
user, err := s.userRepo.GetByEmail(email)
if err != nil {
return "", errors.New("invalid email or password")
}
// 2. Compare password hash
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
return "", errors.New("invalid email or password")
}
// 3. Generate JWT Token
secret := os.Getenv("JWT_SECRET")
if secret == "" {
secret = "fallback_secret_key_change_in_production"
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.ID.String(),
"email": user.Email,
"exp": time.Now().Add(time.Hour * 72).Unix(), // 3 days expiration
"iat": time.Now().Unix(),
})
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", err
}
return tokenString, nil
}

View File

@@ -3,6 +3,7 @@ package service
import ( import (
"github.com/zs/InsightReply/internal/model" "github.com/zs/InsightReply/internal/model"
"github.com/zs/InsightReply/internal/repository" "github.com/zs/InsightReply/internal/repository"
"golang.org/x/crypto/bcrypt"
) )
type UserService struct { type UserService struct {
@@ -13,12 +14,18 @@ func NewUserService(repo *repository.UserRepository) *UserService {
return &UserService{repo: repo} return &UserService{repo: repo}
} }
func (s *UserService) Register(email string, identity string) (*model.User, error) { func (s *UserService) Register(email string, password string, identity string) (*model.User, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &model.User{ user := &model.User{
Email: email, Email: email,
PasswordHash: string(hashedPassword),
IdentityLabel: identity, IdentityLabel: identity,
} }
err := s.repo.Create(user) err = s.repo.Create(user)
return user, err return user, err
} }

View File

@@ -55,9 +55,9 @@ CREATE TABLE IF NOT EXISTS tweets (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_tweets_x_tweet_id ON tweets(x_tweet_id); CREATE INDEX IF NOT EXISTS idx_tweets_x_tweet_id ON tweets(x_tweet_id);
CREATE INDEX idx_tweets_heat_score ON tweets(heat_score DESC); CREATE INDEX IF NOT EXISTS idx_tweets_heat_score ON tweets(heat_score DESC);
CREATE INDEX idx_tweets_crawl_queue ON tweets(crawl_queue, last_crawled_at); CREATE INDEX IF NOT EXISTS idx_tweets_crawl_queue ON tweets(crawl_queue, last_crawled_at);
-- generated_replies 表:生成的 AI 评论记录 -- generated_replies 表:生成的 AI 评论记录
CREATE TABLE IF NOT EXISTS generated_replies ( CREATE TABLE IF NOT EXISTS generated_replies (
@@ -71,8 +71,8 @@ CREATE TABLE IF NOT EXISTS generated_replies (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_generated_replies_user_id ON generated_replies(user_id); CREATE INDEX IF NOT EXISTS idx_generated_replies_user_id ON generated_replies(user_id);
CREATE INDEX idx_generated_replies_tweet_id ON generated_replies(tweet_id); CREATE INDEX IF NOT EXISTS idx_generated_replies_tweet_id ON generated_replies(tweet_id);
-- reply_performance 表:针对已发布评论的效果数据回拨 -- reply_performance 表:针对已发布评论的效果数据回拨
CREATE TABLE IF NOT EXISTS reply_performance ( CREATE TABLE IF NOT EXISTS reply_performance (
@@ -85,8 +85,8 @@ CREATE TABLE IF NOT EXISTS reply_performance (
check_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP check_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_reply_performance_reply_id ON reply_performance(reply_id); CREATE INDEX IF NOT EXISTS idx_reply_performance_reply_id ON reply_performance(reply_id);
CREATE INDEX idx_reply_performance_user_id ON reply_performance(user_id); CREATE INDEX IF NOT EXISTS idx_reply_performance_user_id ON reply_performance(user_id);
-- ==================================================== -- ====================================================
-- 新增表 (v1.1) -- 新增表 (v1.1)
@@ -106,8 +106,8 @@ CREATE TABLE IF NOT EXISTS api_usage_logs (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_api_usage_logs_user_id ON api_usage_logs(user_id); CREATE INDEX IF NOT EXISTS idx_api_usage_logs_user_id ON api_usage_logs(user_id);
CREATE INDEX idx_api_usage_logs_created_at ON api_usage_logs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_api_usage_logs_created_at ON api_usage_logs(created_at DESC);
-- subscriptions 表:用户订阅记录(支付历史) -- subscriptions 表:用户订阅记录(支付历史)
CREATE TABLE IF NOT EXISTS subscriptions ( CREATE TABLE IF NOT EXISTS subscriptions (
@@ -122,8 +122,8 @@ CREATE TABLE IF NOT EXISTS subscriptions (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status); CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
-- user_style_profiles 表:用户风格画像(用于个性化 Prompt -- user_style_profiles 表:用户风格画像(用于个性化 Prompt
CREATE TABLE IF NOT EXISTS user_style_profiles ( CREATE TABLE IF NOT EXISTS user_style_profiles (
@@ -147,7 +147,7 @@ CREATE TABLE IF NOT EXISTS crawl_snapshots (
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_crawl_snapshots_created_at ON crawl_snapshots(created_at DESC); CREATE INDEX IF NOT EXISTS idx_crawl_snapshots_created_at ON crawl_snapshots(created_at DESC);
-- ==================================================== -- ====================================================
-- 新增表 (v1.2) — 用户可配置系统 -- 新增表 (v1.2) — 用户可配置系统
@@ -191,7 +191,7 @@ CREATE TABLE IF NOT EXISTS user_custom_strategies (
UNIQUE (user_id, strategy_key) UNIQUE (user_id, strategy_key)
); );
CREATE INDEX idx_user_custom_strategies_user_id ON user_custom_strategies(user_id); CREATE INDEX IF NOT EXISTS idx_user_custom_strategies_user_id ON user_custom_strategies(user_id);
-- competitor_monitors 表:竞品品牌监控(复用后端雷达,按品牌词自动抓取) -- competitor_monitors 表:竞品品牌监控(复用后端雷达,按品牌词自动抓取)
CREATE TABLE IF NOT EXISTS competitor_monitors ( CREATE TABLE IF NOT EXISTS competitor_monitors (
@@ -204,7 +204,7 @@ CREATE TABLE IF NOT EXISTS competitor_monitors (
UNIQUE (user_id, brand_name) UNIQUE (user_id, brand_name)
); );
CREATE INDEX idx_competitor_monitors_user_id ON competitor_monitors(user_id); CREATE INDEX IF NOT EXISTS idx_competitor_monitors_user_id ON competitor_monitors(user_id);
-- ==================================================== -- ====================================================
-- 触发器:自动更新 updated_at -- 触发器:自动更新 updated_at
@@ -219,12 +219,20 @@ END;
$$ language 'plpgsql'; $$ language 'plpgsql';
-- 为所有需要追踪更新时间的表添加触发器 -- 为所有需要追踪更新时间的表添加触发器
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_users_modtime') THEN
CREATE TRIGGER update_users_modtime CREATE TRIGGER update_users_modtime
BEFORE UPDATE ON users BEFORE UPDATE ON users
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_modified_column(); EXECUTE FUNCTION update_modified_column();
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_user_style_profiles_modtime') THEN
CREATE TRIGGER update_user_style_profiles_modtime CREATE TRIGGER update_user_style_profiles_modtime
BEFORE UPDATE ON user_style_profiles BEFORE UPDATE ON user_style_profiles
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_modified_column(); EXECUTE FUNCTION update_modified_column();
END IF;
END $$;