Compare commits
13 Commits
81c2767423
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7df6587fd6 | ||
|
|
94988790f8 | ||
|
|
45dcef5702 | ||
|
|
ed7232e5c0 | ||
|
|
8852ee5a3a | ||
|
|
a7b73b0928 | ||
|
|
e35f3ed684 | ||
|
|
aed09d4341 | ||
|
|
a0cf063ff9 | ||
|
|
323b712c18 | ||
| 6786d174a6 | |||
|
|
de6b5d3960 | ||
|
|
a59fbd0edd |
@@ -38,15 +38,20 @@ func SetupRouter(engine *gin.RouterGroup) {
|
||||
ossRouter.POST("/file/upload", endpoint.UploadFile)
|
||||
}
|
||||
|
||||
userRouter := v1.Group("/user", common.GetAuthMiddleware())
|
||||
{
|
||||
userEndpoint := user.NewUserEndpoint()
|
||||
|
||||
userRouter := v1.Group("/user")
|
||||
{
|
||||
userRouter.POST("/sms", userEndpoint.SendVerificationCode)
|
||||
userRouter.POST("/register", userEndpoint.RegisterByEmail)
|
||||
userRouter.POST("/login", userEndpoint.LoginByEmail)
|
||||
userRouter.GET("/info", common.MustAuthMiddleware(), userEndpoint.GetUserInfo)
|
||||
userRouter.GET("/oauth/google/url", userEndpoint.GetGoogleOAuthUrl)
|
||||
userRouter.POST("/oauth/google/callback", userEndpoint.GoogleOAuthCallback)
|
||||
}
|
||||
|
||||
userAuthRouter := v1.Group("/user", common.GetAuthMiddleware())
|
||||
{
|
||||
userAuthRouter.GET("/info", common.MustAuthMiddleware(), userEndpoint.GetUserInfo)
|
||||
}
|
||||
|
||||
// 数据埋点路由
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.com/texpixel/document_ai/config"
|
||||
model "gitea.com/texpixel/document_ai/internal/model/user"
|
||||
@@ -169,3 +171,69 @@ func (h *UserEndpoint) LoginByEmail(ctx *gin.Context) {
|
||||
ExpiresAt: tokenResult.ExpiresAt,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *UserEndpoint) GetGoogleOAuthUrl(ctx *gin.Context) {
|
||||
req := model.GoogleAuthUrlRequest{}
|
||||
if err := ctx.ShouldBindQuery(&req); err != nil {
|
||||
ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeParamError, common.CodeParamErrorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
googleConfig := config.GlobalConfig.Google
|
||||
if googleConfig.ClientID == "" {
|
||||
log.Error(ctx, "func", "GetGoogleOAuthUrl", "msg", "Google OAuth not configured")
|
||||
ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeSystemError, common.CodeSystemErrorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
authURL := fmt.Sprintf(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?client_id=%s&redirect_uri=%s&response_type=code&scope=openid%%20email%%20profile&state=%s",
|
||||
url.QueryEscape(googleConfig.ClientID),
|
||||
url.QueryEscape(req.RedirectURI),
|
||||
url.QueryEscape(req.State),
|
||||
)
|
||||
|
||||
ctx.JSON(http.StatusOK, common.SuccessResponse(ctx, model.GoogleAuthUrlResponse{
|
||||
AuthURL: authURL,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *UserEndpoint) GoogleOAuthCallback(ctx *gin.Context) {
|
||||
req := model.GoogleOAuthCallbackRequest{}
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeParamError, common.CodeParamErrorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
googleConfig := config.GlobalConfig.Google
|
||||
if googleConfig.ClientID == "" || googleConfig.ClientSecret == "" {
|
||||
log.Error(ctx, "func", "GoogleOAuthCallback", "msg", "Google OAuth not configured")
|
||||
ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeSystemError, common.CodeSystemErrorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
userInfo, err := h.userService.ExchangeGoogleCodeAndGetUserInfo(ctx, googleConfig.ClientID, googleConfig.ClientSecret, req.Code, req.RedirectURI)
|
||||
if err != nil {
|
||||
log.Error(ctx, "func", "GoogleOAuthCallback", "msg", "exchange code failed", "error", err)
|
||||
ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeSystemError, common.CodeSystemErrorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
uid, err := h.userService.FindOrCreateGoogleUser(ctx, userInfo)
|
||||
if err != nil {
|
||||
log.Error(ctx, "func", "GoogleOAuthCallback", "msg", "find or create user failed", "error", err)
|
||||
ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeSystemError, common.CodeSystemErrorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
tokenResult, err := jwt.CreateToken(jwt.User{UserId: uid})
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, common.ErrorResponse(ctx, common.CodeUnauthorized, common.CodeUnauthorizedMsg))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, common.SuccessResponse(ctx, model.GoogleOAuthCallbackResponse{
|
||||
Token: tokenResult.Token,
|
||||
ExpiresAt: tokenResult.ExpiresAt,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -15,12 +15,20 @@ type Config struct {
|
||||
Aliyun AliyunConfig `mapstructure:"aliyun"`
|
||||
Mathpix MathpixConfig `mapstructure:"mathpix"`
|
||||
BaiduOCR BaiduOCRConfig `mapstructure:"baidu_ocr"`
|
||||
Google GoogleOAuthConfig `mapstructure:"google"`
|
||||
}
|
||||
|
||||
type BaiduOCRConfig struct {
|
||||
Token string `mapstructure:"token"`
|
||||
}
|
||||
|
||||
type GoogleOAuthConfig struct {
|
||||
ClientID string `mapstructure:"client_id"`
|
||||
ClientSecret string `mapstructure:"client_secret"`
|
||||
RedirectURI string `mapstructure:"redirect_uri"`
|
||||
Proxy string `mapstructure:"proxy"`
|
||||
}
|
||||
|
||||
type MathpixConfig struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
AppKey string `mapstructure:"app_key"`
|
||||
|
||||
@@ -4,8 +4,8 @@ server:
|
||||
|
||||
database:
|
||||
driver: mysql
|
||||
host: mysql
|
||||
port: 3306
|
||||
host: localhost
|
||||
port: 3006
|
||||
username: root
|
||||
password: texpixel#pwd123!
|
||||
dbname: doc_ai
|
||||
@@ -13,7 +13,7 @@ database:
|
||||
max_open: 100
|
||||
|
||||
redis:
|
||||
addr: redis:6379
|
||||
addr: localhost:6079
|
||||
password: yoge@123321!
|
||||
db: 0
|
||||
|
||||
@@ -30,7 +30,6 @@ log:
|
||||
maxBackups: 1 # 保留的旧日志文件最大数量
|
||||
compress: false # 是否压缩旧日志
|
||||
|
||||
|
||||
aliyun:
|
||||
sms:
|
||||
access_key_id: "LTAI5tB9ur4ExCF4dYPq7hLz"
|
||||
@@ -43,12 +42,17 @@ aliyun:
|
||||
inner_endpoint: oss-cn-beijing-internal.aliyuncs.com
|
||||
access_key_id: LTAI5t8qXhow6NCdYDtu1saF
|
||||
access_key_secret: qZ2SwYsNCEBckCVSOszH31yYwXU44A
|
||||
bucket_name: texpixel-doc
|
||||
bucket_name: texpixel-doc1
|
||||
|
||||
mathpix:
|
||||
app_id: "ocr_eede6f_ea9b5c"
|
||||
app_key: "fb72d251e33ac85c929bfd4eec40d78368d08d82fb2ee1cffb04a8bb967d1db5"
|
||||
|
||||
|
||||
baidu_ocr:
|
||||
token: "e3a47bd2438f1f38840c203fc5939d17a54482d1"
|
||||
|
||||
google:
|
||||
client_id: "404402221037-nqdsk11bkpk5a7oh396mrg1ieh28u6q1.apps.googleusercontent.com"
|
||||
client_secret: "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"
|
||||
redirect_uri: "https://app.cloud.texpixel.com:10443/auth/google/callback"
|
||||
proxy: "http://localhost:7890"
|
||||
|
||||
@@ -42,8 +42,7 @@ aliyun:
|
||||
inner_endpoint: oss-cn-beijing-internal.aliyuncs.com
|
||||
access_key_id: LTAI5t8qXhow6NCdYDtu1saF
|
||||
access_key_secret: qZ2SwYsNCEBckCVSOszH31yYwXU44A
|
||||
bucket_name: texpixel-doc
|
||||
|
||||
bucket_name: texpixel-doc1
|
||||
|
||||
mathpix:
|
||||
app_id: "ocr_eede6f_ea9b5c"
|
||||
@@ -51,3 +50,9 @@ mathpix:
|
||||
|
||||
baidu_ocr:
|
||||
token: "e3a47bd2438f1f38840c203fc5939d17a54482d1"
|
||||
|
||||
google:
|
||||
client_id: "404402221037-nqdsk11bkpk5a7oh396mrg1ieh28u6q1.apps.googleusercontent.com"
|
||||
client_secret: "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"
|
||||
redirect_uri: "https://texpixel.com/auth/google/callback"
|
||||
proxy: "http://100.115.184.74:7890"
|
||||
|
||||
13
deploy_dev.sh
Executable file
13
deploy_dev.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
git push origin test
|
||||
|
||||
ssh ubuntu << 'ENDSSH'
|
||||
cd /home/yoge/Dev/doc_ai_backed
|
||||
git checkout test
|
||||
git pull origin test
|
||||
docker compose -f docker-compose.infra.yml up -d
|
||||
docker compose down
|
||||
docker image rm doc_ai_backed-doc_ai:latest
|
||||
docker compose up -d
|
||||
ENDSSH
|
||||
32
docker-compose.infra.yml
Normal file
32
docker-compose.infra.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: texpixel#pwd123!
|
||||
MYSQL_DATABASE: doc_ai
|
||||
MYSQL_USER: texpixel
|
||||
MYSQL_PASSWORD: texpixel#pwd123!
|
||||
ports:
|
||||
- "3006:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-ptexpixel#pwd123!"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
command: redis-server --requirepass "yoge@123321!"
|
||||
ports:
|
||||
- "6079:6379"
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
@@ -2,58 +2,9 @@ services:
|
||||
doc_ai:
|
||||
build: .
|
||||
container_name: doc_ai
|
||||
ports:
|
||||
- "8024:8024"
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- backend
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
command: ["-env", "dev"]
|
||||
restart: always
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: texpixel#pwd123!
|
||||
MYSQL_DATABASE: doc_ai
|
||||
MYSQL_USER: texpixel
|
||||
MYSQL_PASSWORD: texpixel#pwd123!
|
||||
ports:
|
||||
- "3006:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
networks:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-ptexpixel#pwd123!"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
command: redis-server --requirepass "yoge@123321!"
|
||||
ports:
|
||||
- "6079:6379"
|
||||
networks:
|
||||
- backend
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
# 持久化MySQL数据卷
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
backend:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
# 数据埋点 API 调用示例
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **接口路径**: `/doc_ai/v1/analytics/track`
|
||||
- **请求方法**: `POST`
|
||||
- **Content-Type**: `application/json`
|
||||
- **认证**: 可选(Bearer Token)
|
||||
|
||||
## 1. 基础埋点事件(最小参数)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"user_id": 12345,
|
||||
"event_name": "button_click"
|
||||
}'
|
||||
```
|
||||
|
||||
## 2. 完整埋点事件(包含所有字段)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"user_id": 12345,
|
||||
"event_name": "formula_recognition_start",
|
||||
"properties": {
|
||||
"file_name": "math_formula.png",
|
||||
"file_size": 102400,
|
||||
"file_type": "image/png",
|
||||
"upload_method": "drag_drop"
|
||||
},
|
||||
"device_info": {
|
||||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
||||
"screen_width": 1920,
|
||||
"screen_height": 1080,
|
||||
"language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"platform": "MacIntel"
|
||||
},
|
||||
"meta_data": {
|
||||
"task_id": "task_123456",
|
||||
"timestamp": 1706342400000
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 3. 页面浏览事件
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"user_id": 12345,
|
||||
"event_name": "page_view",
|
||||
"properties": {
|
||||
"page_url": "https://example.com/home",
|
||||
"page_title": "首页",
|
||||
"page_name": "home",
|
||||
"referrer": "https://example.com/login"
|
||||
},
|
||||
"device_info": {
|
||||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
||||
"screen_width": 1920,
|
||||
"screen_height": 1080
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 4. 任务相关事件
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"user_id": 12345,
|
||||
"event_name": "task_create",
|
||||
"properties": {
|
||||
"task_type": "formula_recognition",
|
||||
"file_name": "equation.png",
|
||||
"file_size": 204800
|
||||
},
|
||||
"meta_data": {
|
||||
"task_id": "task_789012"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 5. 任务完成事件
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"user_id": 12345,
|
||||
"event_name": "task_complete",
|
||||
"properties": {
|
||||
"duration_seconds": 5.2,
|
||||
"success": true,
|
||||
"result_type": "latex"
|
||||
},
|
||||
"meta_data": {
|
||||
"task_id": "task_789012"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 6. 表单提交事件
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"user_id": 12345,
|
||||
"event_name": "form_submit",
|
||||
"properties": {
|
||||
"form_name": "user_registration",
|
||||
"form_fields": ["email", "password", "phone"],
|
||||
"success": true,
|
||||
"validation_errors": 0
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 7. 文件上传事件
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"user_id": 12345,
|
||||
"event_name": "file_upload",
|
||||
"properties": {
|
||||
"file_name": "document.pdf",
|
||||
"file_size": 5242880,
|
||||
"file_type": "application/pdf",
|
||||
"upload_source": "drag_drop",
|
||||
"upload_duration_ms": 1200
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 8. 错误追踪事件
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"user_id": 12345,
|
||||
"event_name": "error_occurred",
|
||||
"properties": {
|
||||
"error_type": "network_error",
|
||||
"error_message": "Failed to fetch data",
|
||||
"error_code": "NET_001",
|
||||
"page_url": "https://example.com/tasks",
|
||||
"user_action": "click_submit_button"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 9. 使用环境变量(推荐)
|
||||
|
||||
```bash
|
||||
# 设置环境变量
|
||||
export API_BASE_URL="http://localhost:8080"
|
||||
export JWT_TOKEN="YOUR_JWT_TOKEN"
|
||||
export USER_ID=12345
|
||||
|
||||
# 调用接口
|
||||
curl -X POST ${API_BASE_URL}/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
||||
-d "{
|
||||
\"user_id\": ${USER_ID},
|
||||
\"event_name\": \"button_click\",
|
||||
\"properties\": {
|
||||
\"button_name\": \"submit\",
|
||||
\"button_position\": \"bottom\"
|
||||
}
|
||||
}"
|
||||
```
|
||||
|
||||
## 10. 使用 JSON 文件
|
||||
|
||||
创建 `event.json` 文件:
|
||||
```json
|
||||
{
|
||||
"user_id": 12345,
|
||||
"event_name": "custom_event",
|
||||
"properties": {
|
||||
"action": "click",
|
||||
"element": "button",
|
||||
"value": "submit"
|
||||
},
|
||||
"device_info": {
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"screen_width": 1920,
|
||||
"screen_height": 1080
|
||||
},
|
||||
"meta_data": {
|
||||
"task_id": "task_123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后执行:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @event.json
|
||||
```
|
||||
|
||||
## 11. 批量埋点接口
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track/batch \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"events": [
|
||||
{
|
||||
"user_id": 12345,
|
||||
"event_name": "page_view",
|
||||
"properties": {
|
||||
"page_name": "home"
|
||||
}
|
||||
},
|
||||
{
|
||||
"user_id": 12345,
|
||||
"event_name": "button_click",
|
||||
"properties": {
|
||||
"button_name": "start"
|
||||
}
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## 响应示例
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "invalid request",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **user_id** 和 **event_name** 是必填字段
|
||||
2. **properties**、**device_info**、**meta_data** 都是可选字段,类型为 JSON 对象
|
||||
3. 如果提供了 Authorization header,token 中的 user_id 会被设置到上下文中,但请求体中的 user_id 仍然需要提供
|
||||
4. 建议在生产环境中始终使用 HTTPS
|
||||
5. 批量接口最多支持 100 个事件
|
||||
|
||||
## 测试命令(本地开发)
|
||||
|
||||
```bash
|
||||
# 最简单的测试
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id": 1, "event_name": "test_event"}'
|
||||
|
||||
# 查看响应详情
|
||||
curl -X POST http://localhost:8080/doc_ai/v1/analytics/track \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id": 1, "event_name": "test_event"}' \
|
||||
-v
|
||||
```
|
||||
@@ -1,308 +0,0 @@
|
||||
// Analytics SDK for Frontend
|
||||
// 前端数据埋点 SDK
|
||||
|
||||
interface EventProperties {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface DeviceInfo {
|
||||
user_agent?: string;
|
||||
screen_width?: number;
|
||||
screen_height?: number;
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
interface MetaData {
|
||||
task_id?: string | number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface TrackEventParams {
|
||||
event_name: string;
|
||||
properties?: EventProperties;
|
||||
device_info?: DeviceInfo;
|
||||
meta_data?: MetaData;
|
||||
}
|
||||
|
||||
interface AnalyticsConfig {
|
||||
apiUrl: string;
|
||||
token?: string;
|
||||
userId?: number | string;
|
||||
enableAutoTrack?: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
class Analytics {
|
||||
private config: AnalyticsConfig;
|
||||
private userId: number | string | null = null;
|
||||
private eventQueue: TrackEventParams[] = [];
|
||||
private isSending: boolean = false;
|
||||
|
||||
constructor(config: AnalyticsConfig) {
|
||||
this.config = {
|
||||
enableAutoTrack: true,
|
||||
debug: false,
|
||||
...config,
|
||||
};
|
||||
|
||||
if (this.config.userId) {
|
||||
this.userId = this.config.userId;
|
||||
}
|
||||
|
||||
// 自动收集设备信息
|
||||
if (this.config.enableAutoTrack) {
|
||||
this.initAutoTrack();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户ID
|
||||
*/
|
||||
setUserId(userId: number | string) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备信息
|
||||
*/
|
||||
private getDeviceInfo(): DeviceInfo {
|
||||
return {
|
||||
user_agent: navigator.userAgent,
|
||||
screen_width: window.screen.width,
|
||||
screen_height: window.screen.height,
|
||||
language: navigator.language,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
platform: navigator.platform,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录单个事件
|
||||
*/
|
||||
async track(params: TrackEventParams): Promise<void> {
|
||||
if (!this.userId) {
|
||||
console.warn('Analytics: userId not set, event will not be tracked');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventData = {
|
||||
user_id: this.userId,
|
||||
event_name: params.event_name,
|
||||
properties: params.properties || {},
|
||||
device_info: {
|
||||
...this.getDeviceInfo(),
|
||||
...params.device_info,
|
||||
},
|
||||
meta_data: {
|
||||
timestamp: Date.now(),
|
||||
...params.meta_data,
|
||||
},
|
||||
};
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('Analytics Track:', eventData);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.config.apiUrl}/track`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.config.token && { Authorization: `Bearer ${this.config.token}` }),
|
||||
},
|
||||
body: JSON.stringify(eventData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to track event: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('Analytics: Event tracked successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Analytics: Failed to track event', error);
|
||||
// 失败时加入队列,稍后重试
|
||||
this.eventQueue.push(params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量记录事件
|
||||
*/
|
||||
async trackBatch(events: TrackEventParams[]): Promise<void> {
|
||||
if (!this.userId) {
|
||||
console.warn('Analytics: userId not set, events will not be tracked');
|
||||
return;
|
||||
}
|
||||
|
||||
const batchData = {
|
||||
events: events.map((params) => ({
|
||||
user_id: this.userId,
|
||||
event_name: params.event_name,
|
||||
properties: params.properties || {},
|
||||
device_info: {
|
||||
...this.getDeviceInfo(),
|
||||
...params.device_info,
|
||||
},
|
||||
meta_data: {
|
||||
timestamp: Date.now(),
|
||||
...params.meta_data,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('Analytics Track Batch:', batchData);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.config.apiUrl}/track/batch`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.config.token && { Authorization: `Bearer ${this.config.token}` }),
|
||||
},
|
||||
body: JSON.stringify(batchData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to track batch events: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('Analytics: Batch events tracked successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Analytics: Failed to track batch events', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面浏览事件
|
||||
*/
|
||||
trackPageView(pageName?: string) {
|
||||
this.track({
|
||||
event_name: 'page_view',
|
||||
properties: {
|
||||
page_url: window.location.href,
|
||||
page_title: document.title,
|
||||
page_name: pageName || document.title,
|
||||
referrer: document.referrer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
trackClick(elementName: string, properties?: EventProperties) {
|
||||
this.track({
|
||||
event_name: 'click',
|
||||
properties: {
|
||||
element_name: elementName,
|
||||
page_url: window.location.href,
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单提交事件
|
||||
*/
|
||||
trackFormSubmit(formName: string, properties?: EventProperties) {
|
||||
this.track({
|
||||
event_name: 'form_submit',
|
||||
properties: {
|
||||
form_name: formName,
|
||||
page_url: window.location.href,
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务相关事件
|
||||
*/
|
||||
trackTask(taskId: string | number, action: string, properties?: EventProperties) {
|
||||
this.track({
|
||||
event_name: `task_${action}`,
|
||||
properties: {
|
||||
action,
|
||||
...properties,
|
||||
},
|
||||
meta_data: {
|
||||
task_id: taskId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化自动埋点
|
||||
*/
|
||||
private initAutoTrack() {
|
||||
// 页面加载完成时记录
|
||||
if (document.readyState === 'complete') {
|
||||
this.trackPageView();
|
||||
} else {
|
||||
window.addEventListener('load', () => this.trackPageView());
|
||||
}
|
||||
|
||||
// 页面离开前发送队列中的事件
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (this.eventQueue.length > 0) {
|
||||
this.flushQueue();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面可见性变化
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden && this.eventQueue.length > 0) {
|
||||
this.flushQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新队列中的事件
|
||||
*/
|
||||
private flushQueue() {
|
||||
if (this.isSending || this.eventQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSending = true;
|
||||
const eventsToSend = [...this.eventQueue];
|
||||
this.eventQueue = [];
|
||||
|
||||
this.trackBatch(eventsToSend).finally(() => {
|
||||
this.isSending = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动刷新队列
|
||||
*/
|
||||
flush() {
|
||||
this.flushQueue();
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
let analyticsInstance: Analytics | null = null;
|
||||
|
||||
export function initAnalytics(config: AnalyticsConfig): Analytics {
|
||||
analyticsInstance = new Analytics(config);
|
||||
return analyticsInstance;
|
||||
}
|
||||
|
||||
export function getAnalytics(): Analytics {
|
||||
if (!analyticsInstance) {
|
||||
throw new Error('Analytics not initialized. Call initAnalytics first.');
|
||||
}
|
||||
return analyticsInstance;
|
||||
}
|
||||
|
||||
export default Analytics;
|
||||
@@ -1,217 +0,0 @@
|
||||
// Analytics SDK 使用示例
|
||||
|
||||
import { initAnalytics, getAnalytics } from './analytics';
|
||||
|
||||
// 1. 初始化 SDK
|
||||
const analytics = initAnalytics({
|
||||
apiUrl: 'https://your-api-domain.com/doc_ai/v1/analytics',
|
||||
token: 'your-auth-token', // 从登录后获取
|
||||
userId: 12345, // 用户ID
|
||||
enableAutoTrack: true, // 启用自动埋点(页面浏览等)
|
||||
debug: true, // 开发环境下启用调试
|
||||
});
|
||||
|
||||
// 2. 设置用户ID(登录后)
|
||||
analytics.setUserId(12345);
|
||||
|
||||
// 3. 记录页面浏览
|
||||
analytics.trackPageView('首页');
|
||||
|
||||
// 4. 记录点击事件
|
||||
const handleButtonClick = () => {
|
||||
analytics.trackClick('提交按钮', {
|
||||
button_text: '提交',
|
||||
button_position: 'bottom',
|
||||
});
|
||||
};
|
||||
|
||||
// 5. 记录表单提交
|
||||
const handleFormSubmit = (formData: any) => {
|
||||
analytics.trackFormSubmit('用户注册表单', {
|
||||
form_fields: Object.keys(formData),
|
||||
success: true,
|
||||
});
|
||||
};
|
||||
|
||||
// 6. 记录任务相关事件
|
||||
const handleTaskCreate = (taskId: string) => {
|
||||
analytics.trackTask(taskId, 'create', {
|
||||
task_type: 'formula_recognition',
|
||||
file_type: 'image/png',
|
||||
});
|
||||
};
|
||||
|
||||
const handleTaskComplete = (taskId: string) => {
|
||||
analytics.trackTask(taskId, 'complete', {
|
||||
duration_seconds: 5.2,
|
||||
success: true,
|
||||
});
|
||||
};
|
||||
|
||||
// 7. 记录自定义事件
|
||||
const handleFileUpload = (file: File) => {
|
||||
analytics.track({
|
||||
event_name: 'file_upload',
|
||||
properties: {
|
||||
file_name: file.name,
|
||||
file_size: file.size,
|
||||
file_type: file.type,
|
||||
},
|
||||
meta_data: {
|
||||
upload_source: 'drag_drop',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 8. 批量记录事件
|
||||
const handleBatchActions = () => {
|
||||
analytics.trackBatch([
|
||||
{
|
||||
event_name: 'button_click',
|
||||
properties: { button_name: 'save' },
|
||||
},
|
||||
{
|
||||
event_name: 'data_export',
|
||||
properties: { format: 'pdf' },
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// 9. React 组件中使用
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
function HomePage() {
|
||||
useEffect(() => {
|
||||
// 页面加载时记录
|
||||
getAnalytics().trackPageView('首页');
|
||||
}, []);
|
||||
|
||||
const handleClick = () => {
|
||||
getAnalytics().trackClick('首页-开始按钮');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>首页</h1>
|
||||
<button onClick={handleClick}>开始</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 10. Vue 组件中使用
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
mounted() {
|
||||
getAnalytics().trackPageView('首页');
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
getAnalytics().trackClick('首页-开始按钮');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 11. 记录用户行为流程
|
||||
class FormulaRecognitionFlow {
|
||||
private analytics = getAnalytics();
|
||||
private taskId: string | null = null;
|
||||
|
||||
// 开始识别流程
|
||||
startRecognition(file: File) {
|
||||
this.analytics.track({
|
||||
event_name: 'formula_recognition_start',
|
||||
properties: {
|
||||
file_name: file.name,
|
||||
file_size: file.size,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
uploadSuccess(taskId: string) {
|
||||
this.taskId = taskId;
|
||||
this.analytics.trackTask(taskId, 'upload_success', {
|
||||
step: 'upload',
|
||||
});
|
||||
}
|
||||
|
||||
// 识别进行中
|
||||
recognitionProcessing() {
|
||||
if (this.taskId) {
|
||||
this.analytics.trackTask(this.taskId, 'processing', {
|
||||
step: 'recognition',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 识别完成
|
||||
recognitionComplete(result: any) {
|
||||
if (this.taskId) {
|
||||
this.analytics.trackTask(this.taskId, 'complete', {
|
||||
step: 'complete',
|
||||
has_result: !!result,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 识别失败
|
||||
recognitionFailed(error: string) {
|
||||
if (this.taskId) {
|
||||
this.analytics.trackTask(this.taskId, 'failed', {
|
||||
step: 'error',
|
||||
error_message: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 查看结果
|
||||
viewResult() {
|
||||
if (this.taskId) {
|
||||
this.analytics.trackTask(this.taskId, 'view_result', {
|
||||
step: 'view',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出结果
|
||||
exportResult(format: string) {
|
||||
if (this.taskId) {
|
||||
this.analytics.trackTask(this.taskId, 'export', {
|
||||
step: 'export',
|
||||
export_format: format,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 12. 错误追踪
|
||||
window.addEventListener('error', (event) => {
|
||||
getAnalytics().track({
|
||||
event_name: 'javascript_error',
|
||||
properties: {
|
||||
error_message: event.message,
|
||||
error_filename: event.filename,
|
||||
error_line: event.lineno,
|
||||
error_column: event.colno,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 13. 性能追踪
|
||||
window.addEventListener('load', () => {
|
||||
const perfData = performance.timing;
|
||||
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
|
||||
|
||||
getAnalytics().track({
|
||||
event_name: 'page_performance',
|
||||
properties: {
|
||||
page_load_time: pageLoadTime,
|
||||
dns_time: perfData.domainLookupEnd - perfData.domainLookupStart,
|
||||
tcp_time: perfData.connectEnd - perfData.connectStart,
|
||||
request_time: perfData.responseEnd - perfData.requestStart,
|
||||
dom_parse_time: perfData.domComplete - perfData.domLoading,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -43,3 +43,31 @@ type EmailLoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
type GoogleAuthUrlRequest struct {
|
||||
RedirectURI string `form:"redirect_uri" binding:"required"`
|
||||
State string `form:"state" binding:"required"`
|
||||
}
|
||||
|
||||
type GoogleAuthUrlResponse struct {
|
||||
AuthURL string `json:"auth_url"`
|
||||
}
|
||||
|
||||
type GoogleOAuthCallbackRequest struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
State string `json:"state" binding:"required"`
|
||||
RedirectURI string `json:"redirect_uri" binding:"required"`
|
||||
}
|
||||
|
||||
type GoogleOAuthCallbackResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
type GoogleUserInfo struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Picture string `json:"picture"`
|
||||
VerifiedEmail bool `json:"verified_email"`
|
||||
}
|
||||
|
||||
@@ -2,10 +2,15 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.com/texpixel/document_ai/config"
|
||||
model "gitea.com/texpixel/document_ai/internal/model/user"
|
||||
"gitea.com/texpixel/document_ai/internal/storage/cache"
|
||||
"gitea.com/texpixel/document_ai/internal/storage/dao"
|
||||
"gitea.com/texpixel/document_ai/pkg/common"
|
||||
@@ -159,3 +164,126 @@ func (svc *UserService) LoginByEmail(ctx context.Context, email, password string
|
||||
|
||||
return user.ID, nil
|
||||
}
|
||||
|
||||
type googleTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
func (svc *UserService) googleHTTPClient() *http.Client {
|
||||
if config.GlobalConfig.Google.Proxy == "" {
|
||||
return &http.Client{}
|
||||
}
|
||||
proxyURL, err := url.Parse(config.GlobalConfig.Google.Proxy)
|
||||
if err != nil {
|
||||
return &http.Client{}
|
||||
}
|
||||
return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
|
||||
}
|
||||
|
||||
func (svc *UserService) ExchangeGoogleCodeAndGetUserInfo(ctx context.Context, clientID, clientSecret, code, redirectURI string) (*model.GoogleUserInfo, error) {
|
||||
tokenURL := "https://oauth2.googleapis.com/token"
|
||||
formData := url.Values{
|
||||
"client_id": {clientID},
|
||||
"client_secret": {clientSecret},
|
||||
"code": {code},
|
||||
"grant_type": {"authorization_code"},
|
||||
"redirect_uri": {redirectURI},
|
||||
}
|
||||
|
||||
client := svc.googleHTTPClient()
|
||||
resp, err := client.PostForm(tokenURL, formData)
|
||||
if err != nil {
|
||||
log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "exchange code failed", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var tokenResp googleTokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "decode token response failed", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenResp.AccessToken == "" {
|
||||
log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "no access token in response")
|
||||
return nil, errors.New("no access token in response")
|
||||
}
|
||||
|
||||
userInfo, err := svc.getGoogleUserInfo(ctx, tokenResp.AccessToken)
|
||||
if err != nil {
|
||||
log.Error(ctx, "func", "ExchangeGoogleCodeAndGetUserInfo", "msg", "get user info failed", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.GoogleUserInfo{
|
||||
ID: userInfo.ID,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *UserService) getGoogleUserInfo(ctx context.Context, accessToken string) (*model.GoogleUserInfo, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/oauth2/v2/userinfo", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
client := svc.googleHTTPClient()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var userInfo model.GoogleUserInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
func (svc *UserService) FindOrCreateGoogleUser(ctx context.Context, userInfo *model.GoogleUserInfo) (uid int64, err error) {
|
||||
existingUser, err := svc.userDao.GetByGoogleID(dao.DB.WithContext(ctx), userInfo.ID)
|
||||
if err != nil {
|
||||
log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "get user by google id error", "error", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if existingUser != nil {
|
||||
return existingUser.ID, nil
|
||||
}
|
||||
|
||||
existingUser, err = svc.userDao.GetByEmail(dao.DB.WithContext(ctx), userInfo.Email)
|
||||
if err != nil {
|
||||
log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "get user by email error", "error", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if existingUser != nil {
|
||||
existingUser.GoogleID = userInfo.ID
|
||||
err = svc.userDao.Update(dao.DB.WithContext(ctx), existingUser)
|
||||
if err != nil {
|
||||
log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "update user google id error", "error", err)
|
||||
return 0, err
|
||||
}
|
||||
return existingUser.ID, nil
|
||||
}
|
||||
|
||||
user := &dao.User{
|
||||
Email: userInfo.Email,
|
||||
GoogleID: userInfo.ID,
|
||||
Username: userInfo.Name,
|
||||
}
|
||||
err = svc.userDao.Create(dao.DB.WithContext(ctx), user)
|
||||
if err != nil {
|
||||
log.Error(ctx, "func", "FindOrCreateGoogleUser", "msg", "create user error", "error", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return user.ID, nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type User struct {
|
||||
Password string `gorm:"column:password" json:"password"`
|
||||
WechatOpenID string `gorm:"column:wechat_open_id" json:"wechat_open_id"`
|
||||
WechatUnionID string `gorm:"column:wechat_union_id" json:"wechat_union_id"`
|
||||
GoogleID string `gorm:"column:google_id" json:"google_id"`
|
||||
}
|
||||
|
||||
func (u *User) TableName() string {
|
||||
@@ -63,3 +64,18 @@ func (dao *UserDao) GetByEmail(tx *gorm.DB, email string) (*User, error) {
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (dao *UserDao) GetByGoogleID(tx *gorm.DB, googleID string) (*User, error) {
|
||||
var user User
|
||||
if err := tx.Where("google_id = ?", googleID).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (dao *UserDao) Update(tx *gorm.DB, user *User) error {
|
||||
return tx.Save(user).Error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user