Compare commits
12 Commits
latest-ext
...
extension-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50bd2925c1 | ||
|
|
dc808628d3 | ||
|
|
d2b330c0c9 | ||
|
|
4e5147fb13 | ||
|
|
dba30d0ba3 | ||
|
|
911117f240 | ||
|
|
dfa5ba1c50 | ||
|
|
f13f6a4d4b | ||
|
|
1ca810ada8 | ||
|
|
c0edfb629b | ||
|
|
9c875afc95 | ||
|
|
b98077813a |
@@ -5,6 +5,7 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- 'server/**'
|
||||
- '.gitea/workflows/backend-deploy.yml'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -23,7 +24,7 @@ jobs:
|
||||
- name: 编译后端可执行文件
|
||||
run: |
|
||||
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: 准备部署文件
|
||||
run: |
|
||||
@@ -31,6 +32,7 @@ jobs:
|
||||
cp server/server_bin deploy/
|
||||
cp server/Dockerfile deploy/
|
||||
cp server/docker-compose.yml deploy/
|
||||
cp server/.env.example deploy/
|
||||
|
||||
- name: 部署文件到服务器
|
||||
uses: up9cloud/action-rsync@master
|
||||
@@ -38,8 +40,8 @@ jobs:
|
||||
USER: root
|
||||
HOST: 144.24.60.0
|
||||
KEY: ${{secrets.USAARMLOGIN_SSH_KEY}}
|
||||
ARGS: -avz --delete
|
||||
SSH_ARGS: "-p 22 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
ARGS: -avz --delete --exclude '.env'
|
||||
SSH_ARGS: "-p 22 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ServerAliveCountMax=3"
|
||||
SOURCE: ./deploy/
|
||||
TARGET: /var/admin/InsightReply/server/
|
||||
VERBOSE: true
|
||||
@@ -52,5 +54,22 @@ jobs:
|
||||
key: ${{secrets.USAARMLOGIN_SSH_KEY}}
|
||||
script: |
|
||||
cd /var/admin/InsightReply/server
|
||||
# 停止并重新构建启动容器 (利用刚同步过来的新 server_bin 和 Dockerfile)
|
||||
docker-compose up -d --build
|
||||
# 首次部署时,从模板创建 .env
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
paths:
|
||||
- 'extension/**'
|
||||
- '.gitea/workflows/extension-build.yml'
|
||||
- 'extension/.env'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -42,7 +43,7 @@ jobs:
|
||||
with:
|
||||
files: insight-reply-extension.zip
|
||||
token: '${{secrets.GITEA_TOKEN}}'
|
||||
name: 'Extension "Latest"'
|
||||
tag_name: 'latest-extension'
|
||||
name: 'Extension Release #${{ gitea.run_number }}'
|
||||
tag_name: 'extension-v${{ gitea.run_number }}'
|
||||
draft: false
|
||||
prerelease: true
|
||||
|
||||
@@ -25,6 +25,7 @@ InsightReply 采用现代化解耦的三端架构:
|
||||
## 🛠️ 本地开发指南 (Local Development)
|
||||
|
||||
### 1. 启动 PostgreSQL 数据库
|
||||
> **注意**:本项目已禁用 Go 服务自动迁移。请使用本项目支持的 MCP 服务 (`InsightReply_PostgreSQL`) 或手动执行 `docs/schema.sql` 来同步数据库结构。
|
||||
|
||||
### 2. 配置与启动后端 (Go Server)
|
||||
```bash
|
||||
@@ -82,9 +83,10 @@ VITE_API_BASE_URL=https://insight.buildapp.eu.org/api/v1
|
||||
|
||||
```bash
|
||||
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 自动签发
|
||||
在宿主机中编辑 `/etc/caddy/Caddyfile`,配置以下动静分离策略:
|
||||
|
||||
@@ -55,9 +55,9 @@ CREATE TABLE IF NOT EXISTS tweets (
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tweets_x_tweet_id ON tweets(x_tweet_id);
|
||||
CREATE INDEX 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_x_tweet_id ON tweets(x_tweet_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tweets_heat_score ON tweets(heat_score DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_tweets_crawl_queue ON tweets(crawl_queue, last_crawled_at);
|
||||
|
||||
-- generated_replies 表:生成的 AI 评论记录
|
||||
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
|
||||
);
|
||||
|
||||
CREATE INDEX 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_user_id ON generated_replies(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_generated_replies_tweet_id ON generated_replies(tweet_id);
|
||||
|
||||
-- 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
|
||||
);
|
||||
|
||||
CREATE INDEX 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_reply_id ON reply_performance(reply_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reply_performance_user_id ON reply_performance(user_id);
|
||||
|
||||
-- ====================================================
|
||||
-- 新增表 (v1.1)
|
||||
@@ -106,8 +106,8 @@ CREATE TABLE IF NOT EXISTS api_usage_logs (
|
||||
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 idx_api_usage_logs_created_at ON api_usage_logs(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_usage_logs_user_id ON api_usage_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_usage_logs_created_at ON api_usage_logs(created_at DESC);
|
||||
|
||||
-- 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
|
||||
);
|
||||
|
||||
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id);
|
||||
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
|
||||
|
||||
-- user_style_profiles 表:用户风格画像(用于个性化 Prompt)
|
||||
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
|
||||
);
|
||||
|
||||
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) — 用户可配置系统
|
||||
@@ -191,7 +191,7 @@ CREATE TABLE IF NOT EXISTS user_custom_strategies (
|
||||
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 表:竞品品牌监控(复用后端雷达,按品牌词自动抓取)
|
||||
CREATE TABLE IF NOT EXISTS competitor_monitors (
|
||||
@@ -204,7 +204,7 @@ CREATE TABLE IF NOT EXISTS competitor_monitors (
|
||||
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
|
||||
@@ -219,12 +219,20 @@ END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 为所有需要追踪更新时间的表添加触发器
|
||||
CREATE TRIGGER update_users_modtime
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_users_modtime') THEN
|
||||
CREATE TRIGGER update_users_modtime
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TRIGGER update_user_style_profiles_modtime
|
||||
BEFORE UPDATE ON user_style_profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
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
|
||||
BEFORE UPDATE ON user_style_profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
@@ -1 +1 @@
|
||||
VITE_API_BASE_URL=http://localhost:8080/api/v1
|
||||
VITE_API_BASE_URL=https://insight.buildapp.eu.org/api/v1
|
||||
|
||||
@@ -9,7 +9,19 @@ const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api
|
||||
|
||||
chrome.runtime.onMessage.addListener((message: { type: string; payload?: any }, _sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => {
|
||||
if (message.type === 'SHOW_INSIGHT') {
|
||||
console.log('Received tweet data in background:', message.payload);
|
||||
console.log('[InsightReply Background] Received SHOW_INSIGHT data:', message.payload);
|
||||
if (_sender.tab?.id) {
|
||||
console.log(`[InsightReply Background] Forwarding SHOW_INSIGHT to tab ${_sender.tab.id}`);
|
||||
chrome.tabs.sendMessage(_sender.tab.id, message, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('[InsightReply Background] Error forwarding message to tab:', chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log('[InsightReply Background] Successfully forwarded to tab, response:', response);
|
||||
}
|
||||
});
|
||||
}
|
||||
sendResponse({ success: true, forwarded: !!_sender.tab?.id });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'FETCH_CUSTOM_STRATEGIES') {
|
||||
|
||||
@@ -52,7 +52,14 @@ const extractTweetData = (tweetElement: HTMLElement): TweetData | null => {
|
||||
const authorElement = tweetElement.querySelector('[data-testid="User-Name"]');
|
||||
const linkElement = tweetElement.querySelector('time')?.parentElement as HTMLAnchorElement;
|
||||
|
||||
if (!textElement || !authorElement || !linkElement) return null;
|
||||
if (!textElement || !authorElement || !linkElement) {
|
||||
console.debug('[InsightReply] Missing elements for tweet extraction:', {
|
||||
hasText: !!textElement,
|
||||
hasAuthor: !!authorElement,
|
||||
hasLink: !!linkElement
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const getStat = (testid: string) => {
|
||||
const el = tweetElement.querySelector(`[data-testid="${testid}"]`);
|
||||
@@ -146,19 +153,40 @@ const injectInsightButton = (tweetElement: HTMLElement) => {
|
||||
btnContainer.style.marginLeft = '12px';
|
||||
btnContainer.style.cursor = 'pointer';
|
||||
|
||||
btnContainer.innerHTML = `
|
||||
<div style="padding: 4px; border-radius: 9999px; transition: background 0.2s;" onmouseover="this.style.background='rgba(139, 92, 246, 0.1)'" onmouseout="this.style.background='transparent'">
|
||||
const innerDiv = document.createElement('div');
|
||||
innerDiv.style.cssText = 'padding: 4px; border-radius: 9999px; transition: background 0.2s;';
|
||||
|
||||
// Use event listeners instead of inline handlers to comply with Content Security Policy
|
||||
innerDiv.addEventListener('mouseover', () => {
|
||||
innerDiv.style.background = 'rgba(139, 92, 246, 0.1)';
|
||||
});
|
||||
innerDiv.addEventListener('mouseout', () => {
|
||||
innerDiv.style.background = 'transparent';
|
||||
});
|
||||
|
||||
innerDiv.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style="color: #8B5CF6;">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
btnContainer.appendChild(innerDiv);
|
||||
|
||||
btnContainer.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
console.log('[InsightReply] Insight button clicked');
|
||||
|
||||
const data = extractTweetData(tweetElement);
|
||||
if (data) {
|
||||
chrome.runtime.sendMessage({ type: 'SHOW_INSIGHT', payload: data });
|
||||
console.log('[InsightReply] Extracted Tweet Data:', data);
|
||||
chrome.runtime.sendMessage({ type: 'SHOW_INSIGHT', payload: data }, (response) => {
|
||||
console.log('[InsightReply] Background script responsed SHOW_INSIGHT with:', response);
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('[InsightReply] Error sending SHOW_INSIGHT message:', chrome.runtime.lastError);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('[InsightReply] Failed to extract tweet data on click');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -44,8 +44,11 @@ function initSidebar(tweetData?: any) {
|
||||
}
|
||||
|
||||
// Listen for messages to show/hide or update data
|
||||
chrome.runtime.onMessage.addListener((message) => {
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message.type === 'SHOW_INSIGHT') {
|
||||
initSidebar(message.payload)
|
||||
console.log('[InsightReply Sidebar Mount] Received SHOW_INSIGHT message:', message.payload);
|
||||
initSidebar(message.payload);
|
||||
sendResponse({ received: true });
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -57,3 +57,7 @@ globs: *.go, *.sql
|
||||
* 对于本项目的初步开发,推荐使用如 **`gorm`** 或 **`sqlx`** 进行快速的数据交互操作。
|
||||
* 所有表名、字段名在 Go 结构体 (`struct`) 的 tag 中必须显式定义为下划线 (snake_case)。
|
||||
* UUID 作为主键,禁止前端或外部服务自行生成传入,一律由 PostgreSQL `gen_random_uuid()` 或者服务端生成。
|
||||
* **数据库迁移 (Migration)**:
|
||||
* **禁用自动迁移**: 后端程序不再自动执行 `Up()` 迁移,所有变更需手动通过 MCP 或 DBA 工具执行。
|
||||
* **幂等性**: 所有 SQL 脚本(如 `CREATE INDEX`, `CREATE TABLE`)必须包含 `IF NOT EXISTS` 保护。
|
||||
* **触发器**: 创建触发器时必须先检查是否存在,避免重复定义导致部署中断。
|
||||
|
||||
@@ -38,7 +38,7 @@ GEMINI_AVAILABLE_MODELS=gemini-2.5-flash,gemini-2.5-pro
|
||||
JWT_SECRET=your-random-32-char-secret-key-here
|
||||
|
||||
# ---- 服务器 ----
|
||||
SERVER_PORT=8080
|
||||
SERVER_PORT=8009
|
||||
LOG_LEVEL=info
|
||||
|
||||
# ---- CORS ----
|
||||
|
||||
@@ -10,9 +10,8 @@ WORKDIR /app
|
||||
COPY server_bin .
|
||||
RUN chmod +x server_bin
|
||||
|
||||
# 拷贝数据库迁移文件 (服务启动时自动执行)
|
||||
COPY migrations ./migrations
|
||||
# 数据库迁移现已通过 MCP 手动管理,不再打包进镜像
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8009
|
||||
|
||||
CMD ["./server_bin"]
|
||||
|
||||
@@ -21,9 +21,6 @@ import (
|
||||
"github.com/zs/InsightReply/internal/service"
|
||||
"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/gorm"
|
||||
)
|
||||
@@ -58,24 +55,14 @@ func main() {
|
||||
}
|
||||
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
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
userSvc := service.NewUserService(userRepo)
|
||||
userHandler := handler.NewUserHandler(userSvc)
|
||||
|
||||
authSvc := service.NewAuthService(userRepo)
|
||||
authHandler := handler.NewAuthHandler(authSvc)
|
||||
|
||||
profileRepo := repository.NewProductProfileRepository(db)
|
||||
profileSvc := service.NewProductProfileService(profileRepo)
|
||||
profileHandler := handler.NewProductProfileHandler(profileSvc)
|
||||
@@ -136,6 +123,7 @@ func main() {
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
// Public routes
|
||||
r.Post("/users/register", userHandler.Register)
|
||||
r.Post("/auth/login", authHandler.Login)
|
||||
|
||||
// Protected routes
|
||||
r.Group(func(r chi.Router) {
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
insight-reply-server:
|
||||
build: .
|
||||
container_name: insight-reply-server
|
||||
restart: always
|
||||
ports:
|
||||
- "8009:8080"
|
||||
network_mode: host
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- LOG_FILE_PATH=/app/logs/InsightReply.log
|
||||
- SERVER_PORT=8009
|
||||
volumes:
|
||||
# Map the host's /root/logs to the container's /app/logs directory
|
||||
- /root/logs:/app/logs
|
||||
networks:
|
||||
- insight_network
|
||||
|
||||
networks:
|
||||
insight_network:
|
||||
driver: bridge
|
||||
|
||||
44
server/internal/handler/auth_handler.go
Normal file
44
server/internal/handler/auth_handler.go
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -18,6 +18,7 @@ func NewUserHandler(svc *service.UserService) *UserHandler {
|
||||
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Identity string `json:"identity"`
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.svc.Register(body.Email, body.Identity)
|
||||
user, err := h.svc.Register(body.Email, body.Password, body.Identity)
|
||||
if err != nil {
|
||||
SendError(w, http.StatusInternalServerError, 5001, "Failed to register user")
|
||||
return
|
||||
|
||||
53
server/internal/service/auth_service.go
Normal file
53
server/internal/service/auth_service.go
Normal 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
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"github.com/zs/InsightReply/internal/model"
|
||||
"github.com/zs/InsightReply/internal/repository"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
@@ -13,12 +14,18 @@ func NewUserService(repo *repository.UserRepository) *UserService {
|
||||
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{
|
||||
Email: email,
|
||||
PasswordHash: string(hashedPassword),
|
||||
IdentityLabel: identity,
|
||||
}
|
||||
err := s.repo.Create(user)
|
||||
err = s.repo.Create(user)
|
||||
return user, err
|
||||
}
|
||||
|
||||
|
||||
@@ -55,9 +55,9 @@ CREATE TABLE IF NOT EXISTS tweets (
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tweets_x_tweet_id ON tweets(x_tweet_id);
|
||||
CREATE INDEX 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_x_tweet_id ON tweets(x_tweet_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tweets_heat_score ON tweets(heat_score DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_tweets_crawl_queue ON tweets(crawl_queue, last_crawled_at);
|
||||
|
||||
-- generated_replies 表:生成的 AI 评论记录
|
||||
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
|
||||
);
|
||||
|
||||
CREATE INDEX 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_user_id ON generated_replies(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_generated_replies_tweet_id ON generated_replies(tweet_id);
|
||||
|
||||
-- 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
|
||||
);
|
||||
|
||||
CREATE INDEX 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_reply_id ON reply_performance(reply_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reply_performance_user_id ON reply_performance(user_id);
|
||||
|
||||
-- ====================================================
|
||||
-- 新增表 (v1.1)
|
||||
@@ -106,8 +106,8 @@ CREATE TABLE IF NOT EXISTS api_usage_logs (
|
||||
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 idx_api_usage_logs_created_at ON api_usage_logs(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_usage_logs_user_id ON api_usage_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_usage_logs_created_at ON api_usage_logs(created_at DESC);
|
||||
|
||||
-- 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
|
||||
);
|
||||
|
||||
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id);
|
||||
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
|
||||
|
||||
-- user_style_profiles 表:用户风格画像(用于个性化 Prompt)
|
||||
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
|
||||
);
|
||||
|
||||
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) — 用户可配置系统
|
||||
@@ -191,7 +191,7 @@ CREATE TABLE IF NOT EXISTS user_custom_strategies (
|
||||
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 表:竞品品牌监控(复用后端雷达,按品牌词自动抓取)
|
||||
CREATE TABLE IF NOT EXISTS competitor_monitors (
|
||||
@@ -204,7 +204,7 @@ CREATE TABLE IF NOT EXISTS competitor_monitors (
|
||||
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
|
||||
@@ -219,12 +219,20 @@ END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 为所有需要追踪更新时间的表添加触发器
|
||||
CREATE TRIGGER update_users_modtime
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_users_modtime') THEN
|
||||
CREATE TRIGGER update_users_modtime
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TRIGGER update_user_style_profiles_modtime
|
||||
BEFORE UPDATE ON user_style_profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
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
|
||||
BEFORE UPDATE ON user_style_profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
Reference in New Issue
Block a user