commit
This commit is contained in:
@@ -1,264 +0,0 @@
|
||||
# 环境变量配置指南
|
||||
|
||||
## 概述
|
||||
|
||||
Go 服务支持通过**环境变量**来确定配置,优先级为:
|
||||
|
||||
```
|
||||
环境变量 > 配置文件 > 默认值
|
||||
```
|
||||
|
||||
## 1. 环境选择
|
||||
|
||||
### 方式一:环境变量 `APP_ENV`(推荐)
|
||||
```bash
|
||||
# Windows PowerShell
|
||||
$env:APP_ENV="prod"
|
||||
.\start.bat
|
||||
|
||||
# Linux/Mac
|
||||
export APP_ENV=prod
|
||||
./start.sh
|
||||
|
||||
# Docker
|
||||
docker run -e APP_ENV=prod ...
|
||||
```
|
||||
|
||||
### 方式二:命令行参数
|
||||
```bash
|
||||
go run main.go -env=prod
|
||||
```
|
||||
|
||||
### 方式三:默认值
|
||||
不设置任何参数时,默认使用 `dev` 环境
|
||||
|
||||
---
|
||||
|
||||
## 2. 支持的环境变量
|
||||
|
||||
### 服务器配置
|
||||
| 环境变量 | 配置项 | 说明 | 示例 |
|
||||
|---------|--------|------|------|
|
||||
| `SERVER_PORT` | server.port | 服务端口 | 8080 |
|
||||
| `SERVER_MODE` | server.mode | 运行模式 | release |
|
||||
|
||||
### 数据库配置
|
||||
| 环境变量 | 配置项 | 说明 | 示例 |
|
||||
|---------|--------|------|------|
|
||||
| `DB_HOST` | database.host | 数据库地址 | localhost |
|
||||
| `DB_PORT` | database.port | 数据库端口 | 3306 |
|
||||
| `DB_USERNAME` | database.username | 用户名 | root |
|
||||
| `DB_PASSWORD` | database.password | 密码 | your_password |
|
||||
| `DB_NAME` | database.dbname | 数据库名 | ai_wht |
|
||||
| `DB_CHARSET` | database.charset | 字符集 | utf8mb4 |
|
||||
|
||||
### JWT 配置
|
||||
| 环境变量 | 配置项 | 说明 | 示例 |
|
||||
|---------|--------|------|------|
|
||||
| `JWT_SECRET` | jwt.secret | JWT 密钥 | your_secret_key |
|
||||
| `JWT_EXPIRE_HOURS` | jwt.expire_hours | 过期时间(小时) | 168 |
|
||||
|
||||
### 微信配置
|
||||
| 环境变量 | 配置项 | 说明 | 示例 |
|
||||
|---------|--------|------|------|
|
||||
| `WECHAT_APP_ID` | wechat.app_id | 微信 AppID | wx1234567890 |
|
||||
| `WECHAT_APP_SECRET` | wechat.app_secret | 微信 AppSecret | your_secret |
|
||||
|
||||
### 小红书配置
|
||||
| 环境变量 | 配置项 | 说明 | 示例 |
|
||||
|---------|--------|------|------|
|
||||
| `XHS_PYTHON_SERVICE_URL` | xhs.python_service_url | Python服务地址 | http://localhost:8000 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 使用场景
|
||||
|
||||
### 场景一:本地开发(覆盖数据库密码)
|
||||
```bash
|
||||
# Windows
|
||||
$env:DB_PASSWORD="local_password"
|
||||
go run main.go
|
||||
|
||||
# Linux/Mac
|
||||
export DB_PASSWORD=local_password
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### 场景二:生产部署
|
||||
```bash
|
||||
# 设置生产环境
|
||||
$env:APP_ENV="prod"
|
||||
$env:DB_HOST="prod-db.example.com"
|
||||
$env:DB_PASSWORD="prod_secure_password"
|
||||
$env:JWT_SECRET="prod_jwt_secret_key"
|
||||
$env:WECHAT_APP_ID="wx_prod_appid"
|
||||
$env:WECHAT_APP_SECRET="wx_prod_secret"
|
||||
|
||||
.\start.bat
|
||||
```
|
||||
|
||||
### 场景三:Docker 部署
|
||||
```dockerfile
|
||||
# Dockerfile
|
||||
FROM golang:1.21-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
RUN go build -o main .
|
||||
|
||||
# 设置环境变量
|
||||
ENV APP_ENV=prod
|
||||
ENV SERVER_PORT=8080
|
||||
ENV DB_HOST=mysql-server
|
||||
|
||||
CMD ["./main"]
|
||||
```
|
||||
|
||||
```bash
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
go-backend:
|
||||
build: .
|
||||
environment:
|
||||
- APP_ENV=prod
|
||||
- DB_HOST=mysql
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- WECHAT_APP_ID=${WECHAT_APP_ID}
|
||||
- WECHAT_APP_SECRET=${WECHAT_APP_SECRET}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
```
|
||||
|
||||
### 场景四:CI/CD 流水线
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
env:
|
||||
APP_ENV: prod
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
WECHAT_APP_ID: ${{ secrets.WECHAT_APP_ID }}
|
||||
WECHAT_APP_SECRET: ${{ secrets.WECHAT_APP_SECRET }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置优先级示例
|
||||
|
||||
假设 `config.prod.yaml` 中配置:
|
||||
```yaml
|
||||
database:
|
||||
host: localhost
|
||||
port: 3306
|
||||
password: file_password
|
||||
```
|
||||
|
||||
运行时设置环境变量:
|
||||
```bash
|
||||
$env:DB_HOST="prod-server.com"
|
||||
$env:DB_PASSWORD="env_password"
|
||||
```
|
||||
|
||||
**最终生效的配置:**
|
||||
```
|
||||
host: prod-server.com # 来自环境变量 DB_HOST
|
||||
port: 3306 # 来自配置文件
|
||||
password: env_password # 来自环境变量 DB_PASSWORD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 查看当前配置
|
||||
|
||||
启动服务时,会在日志中输出:
|
||||
```
|
||||
2024/12/15 14:45:00 从环境变量 APP_ENV 读取环境: prod
|
||||
2024/12/15 14:45:00 配置加载成功: prod 环境
|
||||
2024/12/15 14:45:00 数据库配置: root@prod-server.com:3306/ai_wht
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 安全建议
|
||||
|
||||
1. **敏感信息不要写入配置文件**,使用环境变量:
|
||||
- `DB_PASSWORD`
|
||||
- `JWT_SECRET`
|
||||
- `WECHAT_APP_SECRET`
|
||||
|
||||
2. **生产环境必须覆盖的环境变量**:
|
||||
```bash
|
||||
APP_ENV=prod
|
||||
DB_PASSWORD=<secure_password>
|
||||
JWT_SECRET=<random_secret_key>
|
||||
WECHAT_APP_SECRET=<wechat_secret>
|
||||
```
|
||||
|
||||
3. **使用密钥管理工具**(可选):
|
||||
- Azure Key Vault
|
||||
- AWS Secrets Manager
|
||||
- HashiCorp Vault
|
||||
|
||||
---
|
||||
|
||||
## 7. 快速启动脚本
|
||||
|
||||
### Windows (start_with_env.bat)
|
||||
```bat
|
||||
@echo off
|
||||
set APP_ENV=prod
|
||||
set DB_HOST=localhost
|
||||
set DB_PASSWORD=your_password
|
||||
set JWT_SECRET=your_jwt_secret
|
||||
set WECHAT_APP_ID=your_appid
|
||||
set WECHAT_APP_SECRET=your_secret
|
||||
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### Linux/Mac (start_with_env.sh)
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export APP_ENV=prod
|
||||
export DB_HOST=localhost
|
||||
export DB_PASSWORD=your_password
|
||||
export JWT_SECRET=your_jwt_secret
|
||||
export WECHAT_APP_ID=your_appid
|
||||
export WECHAT_APP_SECRET=your_secret
|
||||
|
||||
go run main.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
### Q: 环境变量没生效?
|
||||
**A:** 检查环境变量名称是否正确,区分大小写。
|
||||
|
||||
### Q: 如何查看所有环境变量?
|
||||
**A:**
|
||||
```bash
|
||||
# Windows
|
||||
Get-ChildItem Env:
|
||||
|
||||
# Linux/Mac
|
||||
printenv
|
||||
```
|
||||
|
||||
### Q: 如何临时设置环境变量?
|
||||
**A:**
|
||||
```bash
|
||||
# Windows - 当前会话有效
|
||||
$env:DB_PASSWORD="temp_password"
|
||||
|
||||
# Linux/Mac - 当前会话有效
|
||||
export DB_PASSWORD=temp_password
|
||||
```
|
||||
|
||||
### Q: 如何永久设置环境变量?
|
||||
**A:**
|
||||
- Windows: 系统设置 → 高级系统设置 → 环境变量
|
||||
- Linux/Mac: 添加到 `~/.bashrc` 或 `~/.zshrc`
|
||||
@@ -1,166 +0,0 @@
|
||||
# Python虚拟环境跨平台配置说明
|
||||
|
||||
## 📋 概述
|
||||
|
||||
Go服务已支持跨平台调用Python脚本,可以在Windows和Ubuntu/Linux环境下正常运行。
|
||||
|
||||
## 🔄 Windows vs Linux 路径对比
|
||||
|
||||
### Windows环境
|
||||
```
|
||||
backend/
|
||||
├── venv/
|
||||
│ ├── Scripts/ ← Windows使用Scripts目录
|
||||
│ │ ├── python.exe
|
||||
│ │ ├── activate.bat
|
||||
│ │ └── ...
|
||||
│ └── Lib/
|
||||
```
|
||||
|
||||
**Python解释器**: `backend/venv/Scripts/python.exe`
|
||||
|
||||
### Ubuntu/Linux环境
|
||||
```
|
||||
backend/
|
||||
├── venv/
|
||||
│ ├── bin/ ← Linux使用bin目录
|
||||
│ │ ├── python
|
||||
│ │ ├── activate
|
||||
│ │ └── ...
|
||||
│ └── lib/
|
||||
```
|
||||
|
||||
**Python解释器**: `backend/venv/bin/python`
|
||||
|
||||
## 🚀 部署步骤
|
||||
|
||||
### 在Ubuntu服务器上部署
|
||||
|
||||
1. **创建Python虚拟环境**:
|
||||
```bash
|
||||
cd /path/to/backend
|
||||
python3 -m venv venv
|
||||
```
|
||||
|
||||
2. **激活虚拟环境**:
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
3. **安装依赖**:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
4. **启动Go服务**:
|
||||
```bash
|
||||
cd /path/to/go_backend
|
||||
go run main.go
|
||||
```
|
||||
|
||||
### 在Windows上开发
|
||||
|
||||
1. **创建Python虚拟环境**:
|
||||
```cmd
|
||||
cd backend
|
||||
python -m venv venv
|
||||
```
|
||||
|
||||
2. **激活虚拟环境**:
|
||||
```cmd
|
||||
venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. **安装依赖**:
|
||||
```cmd
|
||||
pip install -r requirements.txt
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
4. **启动Go服务**:
|
||||
```cmd
|
||||
cd go_backend
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 跨平台路径检测
|
||||
|
||||
Go代码中使用`runtime.GOOS`自动检测操作系统:
|
||||
|
||||
```go
|
||||
func getPythonPath(backendDir string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows: venv\Scripts\python.exe
|
||||
return filepath.Join(backendDir, "venv", "Scripts", "python.exe")
|
||||
}
|
||||
// Linux/Mac: venv/bin/python
|
||||
return filepath.Join(backendDir, "venv", "bin", "python")
|
||||
}
|
||||
```
|
||||
|
||||
### 使用位置
|
||||
|
||||
该函数在以下服务中被调用:
|
||||
- `service/xhs_service.go` - 小红书登录服务
|
||||
- `service/employee_service.go` - 员工服务(绑定小红书账号)
|
||||
|
||||
## ✅ 验证部署
|
||||
|
||||
### 测试Python环境
|
||||
```bash
|
||||
# Ubuntu
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
python xhs_cli.py --help
|
||||
|
||||
# Windows
|
||||
cd backend
|
||||
venv\Scripts\activate
|
||||
python xhs_cli.py --help
|
||||
```
|
||||
|
||||
### 测试Go调用
|
||||
```bash
|
||||
# 启动Go服务后,测试发送验证码接口
|
||||
curl -X POST http://localhost:8080/api/employee/xhs/send-code \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone":"13800138000"}'
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **不要提交venv目录到Git**:已在`.gitignore`中配置忽略
|
||||
2. **环境隔离**:Windows和Ubuntu各自维护独立的venv环境
|
||||
3. **依赖一致性**:确保requirements.txt在两个平台上一致
|
||||
4. **Playwright浏览器**:在Ubuntu上需要安装chromium依赖库
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: Ubuntu上提示找不到Python
|
||||
**A**: 确保已安装Python3:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install python3 python3-venv python3-pip
|
||||
```
|
||||
|
||||
### Q: Playwright启动失败
|
||||
**A**: 安装系统依赖:
|
||||
```bash
|
||||
playwright install-deps chromium
|
||||
```
|
||||
|
||||
### Q: Go服务找不到Python脚本
|
||||
**A**: 检查`backend`目录与`go_backend`目录的相对位置,确保为:
|
||||
```
|
||||
project/
|
||||
├── backend/ # Python脚本
|
||||
└── go_backend/ # Go服务
|
||||
```
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [Go服务环境变量配置](ENV_CONFIG_GUIDE.md)
|
||||
- [Python CLI工具文档](../backend/XHS_CLI_README.md)
|
||||
@@ -1,412 +0,0 @@
|
||||
# Ubuntu 启动脚本使用指南
|
||||
|
||||
## 📁 脚本文件列表
|
||||
|
||||
| 脚本文件 | 用途 | 推荐场景 |
|
||||
|---------|------|----------|
|
||||
| `restart.sh` | 智能重启脚本(支持 dev/prod) | **推荐使用** - 开发和生产环境通用 |
|
||||
| `start_prod.sh` | 生产环境快速启动 | 仅生产环境快速部署 |
|
||||
| `stop.sh` | 停止服务脚本 | 停止所有运行的服务 |
|
||||
| `start.sh` | 开发环境启动(原有) | 开发环境直接运行 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 赋予执行权限
|
||||
```bash
|
||||
cd go_backend
|
||||
|
||||
# 一次性赋予所有脚本执行权限
|
||||
chmod +x restart.sh start_prod.sh stop.sh start.sh
|
||||
```
|
||||
|
||||
### 2. 启动开发环境
|
||||
```bash
|
||||
# 方式1: 使用 restart.sh (推荐)
|
||||
./restart.sh dev
|
||||
|
||||
# 方式2: 默认启动开发环境
|
||||
./restart.sh
|
||||
|
||||
# 方式3: 使用原有脚本
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### 3. 启动生产环境
|
||||
```bash
|
||||
# 方式1: 使用 restart.sh (推荐)
|
||||
./restart.sh prod
|
||||
|
||||
# 方式2: 使用专用脚本
|
||||
./start_prod.sh
|
||||
```
|
||||
|
||||
### 4. 停止服务
|
||||
```bash
|
||||
./stop.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 详细说明
|
||||
|
||||
### restart.sh - 智能重启脚本 ⭐推荐
|
||||
|
||||
**功能特点:**
|
||||
- ✅ 自动停止旧服务
|
||||
- ✅ 支持开发/生产环境切换
|
||||
- ✅ 完整的环境检查
|
||||
- ✅ 多重端口清理机制
|
||||
- ✅ 启动验证和错误检测
|
||||
- ✅ 彩色输出,易于阅读
|
||||
|
||||
**使用方法:**
|
||||
```bash
|
||||
# 启动开发环境 (端口 8080)
|
||||
./restart.sh
|
||||
./restart.sh dev
|
||||
|
||||
# 启动生产环境 (端口 8070)
|
||||
./restart.sh prod
|
||||
|
||||
# 查看帮助
|
||||
./restart.sh help
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
========================================
|
||||
AI小红书 Go 后端服务重启脚本
|
||||
========================================
|
||||
环境: dev
|
||||
端口: 8080
|
||||
日志: ai_xhs.log
|
||||
|
||||
=== [1/4] 停止现有服务 ===
|
||||
✅ 端口 8080 已释放
|
||||
|
||||
=== [2/4] 环境检查 ===
|
||||
✅ Go 环境: go version go1.21.0 linux/amd64
|
||||
✅ 主文件: main.go
|
||||
✅ 配置文件: config/config.dev.yaml
|
||||
|
||||
=== [3/4] 下载依赖 ===
|
||||
✅ 依赖下载完成
|
||||
|
||||
=== [4/4] 启动服务 ===
|
||||
✅ 服务已启动,进程 PID: 12345
|
||||
|
||||
========================================
|
||||
🎉 服务启动成功!
|
||||
========================================
|
||||
|
||||
服务信息:
|
||||
环境: dev
|
||||
端口: 8080
|
||||
进程PID: 12345
|
||||
日志文件: ai_xhs.log
|
||||
|
||||
快捷命令:
|
||||
查看日志: tail -f ai_xhs.log
|
||||
停止服务: kill -9 12345
|
||||
|
||||
访问地址:
|
||||
本地: http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### start_prod.sh - 生产环境快速启动
|
||||
|
||||
**功能特点:**
|
||||
- ✅ 专为生产环境优化
|
||||
- ✅ 简洁快速
|
||||
- ✅ 固定端口 8070
|
||||
- ✅ 独立日志文件 `ai_xhs_prod.log`
|
||||
|
||||
**使用方法:**
|
||||
```bash
|
||||
./start_prod.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### stop.sh - 停止服务脚本
|
||||
|
||||
**功能特点:**
|
||||
- ✅ 同时停止开发和生产环境
|
||||
- ✅ 多种停止方法确保彻底清理
|
||||
- ✅ 自动验证停止结果
|
||||
- ✅ 清理所有相关进程
|
||||
|
||||
**使用方法:**
|
||||
```bash
|
||||
./stop.sh
|
||||
```
|
||||
|
||||
**清理范围:**
|
||||
- 所有 `go run main.go` 进程
|
||||
- 占用 8080 端口的进程(开发环境)
|
||||
- 占用 8070 端口的进程(生产环境)
|
||||
- 其他相关 main.go 进程
|
||||
|
||||
---
|
||||
|
||||
## 🔧 环境变量配置
|
||||
|
||||
### 通过环境变量覆盖配置
|
||||
|
||||
脚本支持通过环境变量覆盖配置文件:
|
||||
|
||||
```bash
|
||||
# 设置环境
|
||||
export APP_ENV=prod
|
||||
|
||||
# 覆盖数据库配置
|
||||
export DB_HOST=prod-server.com
|
||||
export DB_PASSWORD=secure_password
|
||||
|
||||
# 覆盖微信配置
|
||||
export WECHAT_APP_ID=wx_prod_id
|
||||
export WECHAT_APP_SECRET=wx_prod_secret
|
||||
|
||||
# 启动服务
|
||||
./restart.sh
|
||||
```
|
||||
|
||||
**支持的环境变量:** 详见 [ENV_CONFIG_GUIDE.md](ENV_CONFIG_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
## 📋 日志管理
|
||||
|
||||
### 查看实时日志
|
||||
```bash
|
||||
# 开发环境
|
||||
tail -f ai_xhs.log
|
||||
|
||||
# 生产环境
|
||||
tail -f ai_xhs_prod.log
|
||||
|
||||
# 查看最后 50 行
|
||||
tail -n 50 ai_xhs.log
|
||||
|
||||
# 搜索错误日志
|
||||
grep -i "error\|fatal\|panic" ai_xhs.log
|
||||
```
|
||||
|
||||
### 日志文件说明
|
||||
|
||||
| 文件 | 用途 | 环境 |
|
||||
|------|------|------|
|
||||
| `ai_xhs.log` | 开发环境日志 | dev |
|
||||
| `ai_xhs_prod.log` | 生产环境日志 | prod |
|
||||
|
||||
---
|
||||
|
||||
## 🛠 常用命令
|
||||
|
||||
### 检查服务状态
|
||||
```bash
|
||||
# 查看 Go 进程
|
||||
ps aux | grep "go run main.go"
|
||||
|
||||
# 查看端口占用
|
||||
lsof -i:8080 # 开发环境
|
||||
lsof -i:8070 # 生产环境
|
||||
|
||||
# 查看所有监听端口
|
||||
netstat -tunlp | grep go
|
||||
```
|
||||
|
||||
### 手动停止服务
|
||||
```bash
|
||||
# 方法1: 使用 PID (推荐)
|
||||
kill -9 <PID>
|
||||
|
||||
# 方法2: 停止所有 go run 进程
|
||||
pkill -f "go run main.go"
|
||||
|
||||
# 方法3: 通过端口停止
|
||||
sudo fuser -k 8080/tcp
|
||||
```
|
||||
|
||||
### 测试服务
|
||||
```bash
|
||||
# 测试服务是否启动
|
||||
curl http://localhost:8080/api/health
|
||||
|
||||
# 查看返回内容
|
||||
curl -i http://localhost:8080/api/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 权限问题
|
||||
某些清理操作需要 sudo 权限:
|
||||
```bash
|
||||
# 如果遇到权限问题,使用 sudo
|
||||
sudo ./stop.sh
|
||||
```
|
||||
|
||||
### 2. 端口冲突
|
||||
如果端口被其他程序占用:
|
||||
```bash
|
||||
# 查看占用端口的程序
|
||||
lsof -i:8080
|
||||
|
||||
# 修改配置文件中的端口
|
||||
vim config/config.dev.yaml
|
||||
```
|
||||
|
||||
### 3. 日志文件过大
|
||||
定期清理日志:
|
||||
```bash
|
||||
# 清空日志文件
|
||||
> ai_xhs.log
|
||||
|
||||
# 或删除旧日志
|
||||
rm ai_xhs.log
|
||||
```
|
||||
|
||||
### 4. 后台运行说明
|
||||
- 脚本使用 `nohup` 在后台运行服务
|
||||
- 关闭终端不会停止服务
|
||||
- 必须使用 `kill` 或 `stop.sh` 停止服务
|
||||
|
||||
---
|
||||
|
||||
## 🔄 系统服务化 (可选)
|
||||
|
||||
### 创建 systemd 服务
|
||||
|
||||
如果需要开机自启动,可以创建系统服务:
|
||||
|
||||
```bash
|
||||
# 创建服务文件
|
||||
sudo vim /etc/systemd/system/ai_xhs.service
|
||||
```
|
||||
|
||||
添加内容:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=AI XHS Go Backend
|
||||
After=network.target mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your_user
|
||||
WorkingDirectory=/path/to/go_backend
|
||||
Environment="APP_ENV=prod"
|
||||
ExecStart=/usr/local/go/bin/go run main.go
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启用服务:
|
||||
```bash
|
||||
# 重载配置
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# 启动服务
|
||||
sudo systemctl start ai_xhs
|
||||
|
||||
# 设置开机自启
|
||||
sudo systemctl enable ai_xhs
|
||||
|
||||
# 查看状态
|
||||
sudo systemctl status ai_xhs
|
||||
|
||||
# 查看日志
|
||||
sudo journalctl -u ai_xhs -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 故障排查
|
||||
|
||||
### 问题1: 服务启动失败
|
||||
```bash
|
||||
# 检查日志
|
||||
tail -f ai_xhs.log
|
||||
|
||||
# 检查配置文件
|
||||
cat config/config.dev.yaml
|
||||
|
||||
# 检查 Go 环境
|
||||
go version
|
||||
```
|
||||
|
||||
### 问题2: 端口无法释放
|
||||
```bash
|
||||
# 强制停止
|
||||
sudo ./stop.sh
|
||||
|
||||
# 检查是否还有进程
|
||||
lsof -i:8080
|
||||
|
||||
# 手动清理
|
||||
sudo fuser -k 8080/tcp
|
||||
```
|
||||
|
||||
### 问题3: 找不到依赖
|
||||
```bash
|
||||
# 重新下载依赖
|
||||
go mod tidy
|
||||
go mod download
|
||||
|
||||
# 清理缓存
|
||||
go clean -modcache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 快速参考
|
||||
|
||||
### 一键部署生产环境
|
||||
```bash
|
||||
# 1. 进入目录
|
||||
cd go_backend
|
||||
|
||||
# 2. 赋予权限
|
||||
chmod +x restart.sh
|
||||
|
||||
# 3. 设置环境变量(可选)
|
||||
export DB_PASSWORD=your_password
|
||||
|
||||
# 4. 启动服务
|
||||
./restart.sh prod
|
||||
|
||||
# 5. 查看日志
|
||||
tail -f ai_xhs.log
|
||||
```
|
||||
|
||||
### 一键停止所有服务
|
||||
```bash
|
||||
chmod +x stop.sh
|
||||
./stop.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
1. **使用 restart.sh** - 功能最完善,错误检查最全面
|
||||
2. **配置环境变量** - 敏感信息不要写入配置文件
|
||||
3. **定期查看日志** - 及时发现问题
|
||||
4. **使用 systemd** - 生产环境推荐系统服务化
|
||||
5. **备份配置文件** - 修改前先备份
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [环境变量配置指南](ENV_CONFIG_GUIDE.md)
|
||||
- [数据库迁移指南](DATABASE_MIGRATION_GUIDE.md)
|
||||
- [微信登录集成指南](WECHAT_LOGIN_GUIDE.md)
|
||||
16
go_backend/check_article.sql
Normal file
16
go_backend/check_article.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 检查文案ID为1的详细信息
|
||||
SELECT
|
||||
a.id AS article_id,
|
||||
a.title,
|
||||
a.status,
|
||||
a.review_comment, -- 这里会显示失败原因
|
||||
a.created_user_id,
|
||||
a.publish_user_id,
|
||||
u.phone,
|
||||
u.is_bound_xhs,
|
||||
au.xhs_cookie IS NOT NULL AS has_cookie,
|
||||
LENGTH(au.xhs_cookie) AS cookie_length
|
||||
FROM ai_articles a
|
||||
LEFT JOIN ai_users u ON u.id = COALESCE(a.publish_user_id, a.created_user_id)
|
||||
LEFT JOIN ai_authors au ON au.phone = u.phone AND au.enterprise_id = u.enterprise_id AND au.channel = 1
|
||||
WHERE a.id = 1;
|
||||
51
go_backend/cmd/check_all_locks.go
Normal file
51
go_backend/cmd/check_all_locks.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ai_xhs/config"
|
||||
"ai_xhs/database"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
if err := config.LoadConfig("dev"); err != nil {
|
||||
log.Fatalf("配置加载失败: %v", err)
|
||||
}
|
||||
|
||||
// 连接Redis
|
||||
if err := database.InitRedis(); err != nil {
|
||||
log.Fatalf("Redis连接失败: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 列出所有lock相关的键
|
||||
fmt.Println("=== 检查所有锁相关的键 ===")
|
||||
keys, err := database.RDB.Keys(ctx, "lock:*").Result()
|
||||
if err != nil {
|
||||
log.Fatalf("查询锁失败: %v", err)
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
fmt.Printf("发现 %d 个锁:\n", len(keys))
|
||||
for _, key := range keys {
|
||||
ttl, _ := database.RDB.TTL(ctx, key).Result()
|
||||
value, _ := database.RDB.Get(ctx, key).Result()
|
||||
fmt.Printf(" - %s (TTL: %v, Value: %s)\n", key, ttl, value)
|
||||
}
|
||||
|
||||
fmt.Println("\n是否要清除所有锁? (y/n)")
|
||||
var answer string
|
||||
fmt.Scanln(&answer)
|
||||
if answer == "y" || answer == "Y" {
|
||||
for _, key := range keys {
|
||||
database.RDB.Del(ctx, key)
|
||||
}
|
||||
fmt.Println("✓ 已清除所有锁")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("未发现任何锁")
|
||||
}
|
||||
}
|
||||
83
go_backend/cmd/clear_bind_lock.go
Normal file
83
go_backend/cmd/clear_bind_lock.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ai_xhs/config"
|
||||
"ai_xhs/database"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
if err := config.LoadConfig("dev"); err != nil {
|
||||
log.Fatalf("配置加载失败: %v", err)
|
||||
}
|
||||
|
||||
// 连接Redis
|
||||
if err := database.InitRedis(); err != nil {
|
||||
log.Fatalf("Redis连接失败: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 获取命令行参数
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("用法: go run cmd/clear_bind_lock.go <employee_id>")
|
||||
fmt.Println("示例: go run cmd/clear_bind_lock.go 1")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
employeeID, err := strconv.Atoi(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatalf("无效的员工ID: %v", err)
|
||||
}
|
||||
|
||||
// 构造锁的key
|
||||
lockKey := fmt.Sprintf("lock:bind_xhs:%d", employeeID)
|
||||
|
||||
// 检查锁是否存在
|
||||
exists, err := database.RDB.Exists(ctx, lockKey).Result()
|
||||
if err != nil {
|
||||
log.Fatalf("检查锁失败: %v", err)
|
||||
}
|
||||
|
||||
if exists > 0 {
|
||||
// 获取锁的TTL
|
||||
ttl, err := database.RDB.TTL(ctx, lockKey).Result()
|
||||
if err != nil {
|
||||
log.Printf("获取锁TTL失败: %v", err)
|
||||
} else {
|
||||
log.Printf("发现锁: %s, 剩余时间: %v", lockKey, ttl)
|
||||
}
|
||||
|
||||
// 删除锁
|
||||
err = database.RDB.Del(ctx, lockKey).Err()
|
||||
if err != nil {
|
||||
log.Fatalf("删除锁失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✓ 成功删除锁: %s", lockKey)
|
||||
} else {
|
||||
log.Printf("未发现锁: %s", lockKey)
|
||||
}
|
||||
|
||||
// 列出所有相关的锁
|
||||
fmt.Println("\n=== 检查所有绑定相关的锁 ===")
|
||||
keys, err := database.RDB.Keys(ctx, "lock:bind_xhs:*").Result()
|
||||
if err != nil {
|
||||
log.Printf("查询锁失败: %v", err)
|
||||
} else {
|
||||
if len(keys) > 0 {
|
||||
fmt.Printf("发现 %d 个绑定锁:\n", len(keys))
|
||||
for _, key := range keys {
|
||||
ttl, _ := database.RDB.TTL(ctx, key).Result()
|
||||
fmt.Printf(" - %s (TTL: %v)\n", key, ttl)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("未发现任何绑定锁")
|
||||
}
|
||||
}
|
||||
}
|
||||
47
go_backend/cmd/generate_password.go
Normal file
47
go_backend/cmd/generate_password.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// HashPassword 密码加密(使用SHA256,与Python版本保持一致)
|
||||
func HashPassword(password string) string {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 如果有命令行参数,加密该密码
|
||||
if len(os.Args) > 1 {
|
||||
password := os.Args[1]
|
||||
hashed := HashPassword(password)
|
||||
fmt.Printf("原始密码: %s\n", password)
|
||||
fmt.Printf("加密后: %s\n", hashed)
|
||||
return
|
||||
}
|
||||
|
||||
// 为测试数据生成加密密码
|
||||
passwords := []string{
|
||||
"admin123", // 企业管理员密码
|
||||
"user123", // 普通用户密码
|
||||
"123456", // 默认密码
|
||||
}
|
||||
|
||||
fmt.Println("生成加密密码(SHA256):")
|
||||
fmt.Println("=====================================")
|
||||
|
||||
for i, pwd := range passwords {
|
||||
hashed := HashPassword(pwd)
|
||||
fmt.Printf("%d. 原始密码: %s\n", i+1, pwd)
|
||||
fmt.Printf(" 加密后: %s\n\n", hashed)
|
||||
}
|
||||
|
||||
fmt.Println("=====================================")
|
||||
fmt.Println("使用说明:")
|
||||
fmt.Println("方式1:直接运行此程序,查看常用密码的加密结果")
|
||||
fmt.Println("方式2:传入密码参数,如: go run generate_password.go mypassword")
|
||||
fmt.Println("注意:请将加密后的密码保存到数据库的 password 字段")
|
||||
}
|
||||
24
go_backend/cmd/test_password_hash.go
Normal file
24
go_backend/cmd/test_password_hash.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 测试密码
|
||||
passwords := []string{
|
||||
"123456",
|
||||
"password",
|
||||
"admin123",
|
||||
}
|
||||
|
||||
fmt.Println("=== Go SHA256 密码加密测试 ===")
|
||||
for _, pwd := range passwords {
|
||||
hash := sha256.Sum256([]byte(pwd))
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
fmt.Printf("密码: %s\n", pwd)
|
||||
fmt.Printf("SHA256: %s\n\n", hashStr)
|
||||
}
|
||||
}
|
||||
168
go_backend/cmd/test_redis.go
Normal file
168
go_backend/cmd/test_redis.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ai_xhs/config"
|
||||
"ai_xhs/database"
|
||||
"ai_xhs/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestData 测试数据结构
|
||||
type TestData struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
if err := config.LoadConfig("dev"); err != nil {
|
||||
log.Fatalf("配置加载失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始化Redis
|
||||
if err := database.InitRedis(); err != nil {
|
||||
log.Fatalf("Redis初始化失败: %v", err)
|
||||
}
|
||||
defer database.CloseRedis()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("\n=== Redis缓存功能测试 ===\n")
|
||||
|
||||
// 1. 测试基本的Set/Get
|
||||
fmt.Println("1. 测试基本Set/Get操作:")
|
||||
testData := TestData{
|
||||
Name: "张三",
|
||||
Age: 25,
|
||||
Email: "zhangsan@example.com",
|
||||
}
|
||||
|
||||
if err := utils.SetCache(ctx, "user:1001", testData, 5*time.Minute); err != nil {
|
||||
log.Fatalf("设置缓存失败: %v", err)
|
||||
}
|
||||
fmt.Println("✓ 缓存设置成功")
|
||||
|
||||
var result TestData
|
||||
if err := utils.GetCache(ctx, "user:1001", &result); err != nil {
|
||||
log.Fatalf("获取缓存失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 缓存获取成功: %+v\n", result)
|
||||
|
||||
// 2. 测试Exists
|
||||
fmt.Println("\n2. 测试缓存是否存在:")
|
||||
exists, err := utils.ExistsCache(ctx, "user:1001")
|
||||
if err != nil {
|
||||
log.Fatalf("检查缓存失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 缓存存在性检查: %v\n", exists)
|
||||
|
||||
// 3. 测试TTL
|
||||
fmt.Println("\n3. 测试获取TTL:")
|
||||
ttl, err := utils.GetTTL(ctx, "user:1001")
|
||||
if err != nil {
|
||||
log.Fatalf("获取TTL失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 缓存TTL: %v\n", ttl)
|
||||
|
||||
// 4. 测试计数器
|
||||
fmt.Println("\n4. 测试计数器操作:")
|
||||
count, err := utils.IncrCache(ctx, "counter:test")
|
||||
if err != nil {
|
||||
log.Fatalf("递增失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 递增后计数: %d\n", count)
|
||||
|
||||
count, err = utils.IncrCache(ctx, "counter:test")
|
||||
if err != nil {
|
||||
log.Fatalf("递增失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 再次递增后计数: %d\n", count)
|
||||
|
||||
// 5. 测试Hash操作
|
||||
fmt.Println("\n5. 测试Hash操作:")
|
||||
if err := utils.HSetCache(ctx, "user:hash:1001", "name", "李四"); err != nil {
|
||||
log.Fatalf("HSet失败: %v", err)
|
||||
}
|
||||
if err := utils.HSetCache(ctx, "user:hash:1001", "age", "30"); err != nil {
|
||||
log.Fatalf("HSet失败: %v", err)
|
||||
}
|
||||
fmt.Println("✓ Hash字段设置成功")
|
||||
|
||||
name, err := utils.HGetCache(ctx, "user:hash:1001", "name")
|
||||
if err != nil {
|
||||
log.Fatalf("HGet失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 获取Hash字段 name: %s\n", name)
|
||||
|
||||
allFields, err := utils.HGetAllCache(ctx, "user:hash:1001")
|
||||
if err != nil {
|
||||
log.Fatalf("HGetAll失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 获取所有Hash字段: %+v\n", allFields)
|
||||
|
||||
// 6. 测试Set操作
|
||||
fmt.Println("\n6. 测试Set操作:")
|
||||
if err := utils.SAddCache(ctx, "tags:1001", "golang", "redis", "mysql"); err != nil {
|
||||
log.Fatalf("SAdd失败: %v", err)
|
||||
}
|
||||
fmt.Println("✓ Set成员添加成功")
|
||||
|
||||
members, err := utils.SMembersCache(ctx, "tags:1001")
|
||||
if err != nil {
|
||||
log.Fatalf("SMembers失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 获取Set所有成员: %v\n", members)
|
||||
|
||||
// 7. 测试ZSet操作
|
||||
fmt.Println("\n7. 测试ZSet操作:")
|
||||
if err := utils.ZAddCache(ctx, "rank:score", 100.5, "user1"); err != nil {
|
||||
log.Fatalf("ZAdd失败: %v", err)
|
||||
}
|
||||
if err := utils.ZAddCache(ctx, "rank:score", 95.0, "user2"); err != nil {
|
||||
log.Fatalf("ZAdd失败: %v", err)
|
||||
}
|
||||
if err := utils.ZAddCache(ctx, "rank:score", 105.5, "user3"); err != nil {
|
||||
log.Fatalf("ZAdd失败: %v", err)
|
||||
}
|
||||
fmt.Println("✓ ZSet成员添加成功")
|
||||
|
||||
rangeResult, err := utils.ZRangeCache(ctx, "rank:score", 0, -1)
|
||||
if err != nil {
|
||||
log.Fatalf("ZRange失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ 获取ZSet所有成员(按分数排序): %v\n", rangeResult)
|
||||
|
||||
// 8. 测试删除操作
|
||||
fmt.Println("\n8. 测试删除操作:")
|
||||
if err := utils.DelCache(ctx, "counter:test", "tags:1001", "rank:score"); err != nil {
|
||||
log.Fatalf("删除缓存失败: %v", err)
|
||||
}
|
||||
fmt.Println("✓ 缓存删除成功")
|
||||
|
||||
// 9. 测试SetNX (仅当key不存在时设置)
|
||||
fmt.Println("\n9. 测试SetNX操作:")
|
||||
success, err := utils.SetCacheNX(ctx, "lock:test", "locked", 10*time.Second)
|
||||
if err != nil {
|
||||
log.Fatalf("SetNX失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ SetNX首次设置: %v\n", success)
|
||||
|
||||
success, err = utils.SetCacheNX(ctx, "lock:test", "locked", 10*time.Second)
|
||||
if err != nil {
|
||||
log.Fatalf("SetNX失败: %v", err)
|
||||
}
|
||||
fmt.Printf("✓ SetNX重复设置(应该失败): %v\n", success)
|
||||
|
||||
// 清理测试数据
|
||||
fmt.Println("\n10. 清理测试数据:")
|
||||
if err := utils.DelCache(ctx, "user:1001", "user:hash:1001", "lock:test"); err != nil {
|
||||
log.Fatalf("清理失败: %v", err)
|
||||
}
|
||||
fmt.Println("✓ 测试数据清理完成")
|
||||
|
||||
fmt.Println("\n=== 所有测试完成! ===")
|
||||
}
|
||||
33
go_backend/cmd/test_service_alert.go
Normal file
33
go_backend/cmd/test_service_alert.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ai_xhs/config"
|
||||
"ai_xhs/service"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// 测试发送服务宕机通知短信
|
||||
func main() {
|
||||
// 加载配置
|
||||
config.InitConfig()
|
||||
|
||||
// 初始化短信服务
|
||||
smsService := service.GetSmsService()
|
||||
|
||||
// 发送宕机通知到指定手机号
|
||||
alertPhone := "15707023967"
|
||||
serviceName := "AI小红书服务"
|
||||
|
||||
fmt.Printf("正在发送服务宕机通知到 %s...\n", alertPhone)
|
||||
|
||||
err := smsService.SendServiceDownAlert(alertPhone, serviceName)
|
||||
if err != nil {
|
||||
log.Fatalf("发送宕机通知失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ 宕机通知发送成功!\n")
|
||||
fmt.Printf("手机号: %s\n", alertPhone)
|
||||
fmt.Printf("通知码: 11111\n")
|
||||
fmt.Printf("服务名: %s\n", serviceName)
|
||||
}
|
||||
@@ -60,3 +60,42 @@ const (
|
||||
CodeCopyNotAvailable = 1002
|
||||
CodeAlreadyClaimed = 1003
|
||||
)
|
||||
|
||||
// ResponseData 带分页的响应数据结构
|
||||
type ResponseData struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Total int64 `json:"total,omitempty"`
|
||||
Page int `json:"page,omitempty"`
|
||||
PageSize int `json:"page_size,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse 返回错误响应对象(不直接发送)
|
||||
func ErrorResponse(message string) Response {
|
||||
return Response{
|
||||
Code: CodeInternalError,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// SuccessResponse 返回成功响应对象(不直接发送)
|
||||
func SuccessResponse(data interface{}, message string) Response {
|
||||
return Response{
|
||||
Code: CodeSuccess,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// SuccessResponseWithPage 返回带分页的成功响应对象(不直接发送)
|
||||
func SuccessResponseWithPage(data interface{}, total int64, page, pageSize int, message string) ResponseData {
|
||||
return ResponseData{
|
||||
Code: CodeSuccess,
|
||||
Message: message,
|
||||
Data: data,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,13 @@ database:
|
||||
max_open_conns: 100
|
||||
conn_max_lifetime: 3600
|
||||
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
password:
|
||||
db: 0
|
||||
pool_size: 10
|
||||
|
||||
jwt:
|
||||
secret: dev_secret_key_change_in_production
|
||||
expire_hours: 168 # 7天
|
||||
@@ -24,11 +31,11 @@ wechat:
|
||||
app_secret: "69d2a3ddc902b26f82f4b56a6e277f7a" # 微信小程序AppSecret
|
||||
|
||||
xhs:
|
||||
python_service_url: "http://localhost:8000" # Python服务地址
|
||||
python_service_url: "http://localhost:8000" # Python FastAPI服务地址(用于登录和发布,享受浏览器池+预热加速)
|
||||
|
||||
scheduler:
|
||||
enabled: false # 是否启用定时任务
|
||||
publish_cron: "* * * * * *" # 每1小时执行一次(开发环境测试用)
|
||||
publish_cron: "*/5 * * * * *" # 每5秒执行一次
|
||||
max_concurrent: 2 # 最大并发发布数
|
||||
publish_timeout: 300 # 发布超时时间(秒)
|
||||
max_articles_per_user_per_run: 2 # 每轮每个用户最大发文数
|
||||
@@ -39,4 +46,28 @@ scheduler:
|
||||
user_agent: "" # 可选:自定义User-Agent,不填则使用默认
|
||||
proxy_pool:
|
||||
enabled: true # 开发环境启用代理池
|
||||
api_url: "http://api.tianqiip.com/getip?secret=lu29e593&num=1&type=txt&port=1&mr=1&sign=4b81a62eaed89ba802a8f34053e2c964"
|
||||
api_url: "http://api.tianqiip.com/getip?secret=xo0uhiz5&num=1&type=txt&port=1&mr=1&sign=d82157fb70c21bae87437ec17eb3e0aa"
|
||||
|
||||
upload:
|
||||
max_image_size: 5242880 # 5MB (5 * 1024 * 1024)
|
||||
max_file_size: 10485760 # 10MB (10 * 1024 * 1024)
|
||||
image_types: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||
static_path: "./static"
|
||||
base_url: "http://localhost:8080"
|
||||
storage_type: "oss" # local(本地存储) 或 oss(阿里云OSS)
|
||||
|
||||
# 阿里云OSS配置(当storageType为oss时生效)
|
||||
oss:
|
||||
endpoint: "https://oss-cn-beijing.aliyuncs.com/" # OSS访问域名
|
||||
access_key_id: "LTAI5tNesdhDH4ErqEUZmEg2" # AccessKey ID
|
||||
access_key_secret: "xZn7WUkTW76TqOLTh01zZATnU6p3Tf" # AccessKey Secret
|
||||
bucket_name: "bxmkb-beijing" # Bucket名称
|
||||
base_path: "wht/" # 文件存储基础路径
|
||||
domain: "" # 自定义域名(可选)
|
||||
|
||||
# ========== 阿里云短信配置 ==========
|
||||
ali_sms:
|
||||
access_key_id: "LTAI5tSMvnCJdqkZtCVWgh8R" # AccessKey ID
|
||||
access_key_secret: "nyFzXyIi47peVLK4wR2qqbPezmU79W" # AccessKey Secret
|
||||
sign_name: "北京乐航时代科技" # 短信签名
|
||||
template_code: "SMS_486210104" # 短信模板CODE
|
||||
|
||||
@@ -11,10 +11,13 @@ import (
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Wechat WechatConfig `mapstructure:"wechat"`
|
||||
XHS XHSConfig `mapstructure:"xhs"`
|
||||
Scheduler SchedulerConfig `mapstructure:"scheduler"`
|
||||
Upload UploadConfig `mapstructure:"upload"`
|
||||
AliSms AliSmsConfig `mapstructure:"ali_sms"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -36,6 +39,14 @@ type DatabaseConfig struct {
|
||||
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"`
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
PoolSize int `mapstructure:"pool_size"`
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string `mapstructure:"secret"`
|
||||
ExpireHours int `mapstructure:"expire_hours"`
|
||||
@@ -64,6 +75,34 @@ type SchedulerConfig struct {
|
||||
ProxyFetchURL string `mapstructure:"proxy_fetch_url"` // 动态获取代理的接口地址(可选)
|
||||
}
|
||||
|
||||
// UploadConfig 文件上传配置
|
||||
type UploadConfig struct {
|
||||
MaxImageSize int64 `mapstructure:"max_image_size"` // 图片最大大小(字节)
|
||||
MaxFileSize int64 `mapstructure:"max_file_size"` // 文件最大大小(字节)
|
||||
ImageTypes []string `mapstructure:"image_types"` // 允许的图片类型
|
||||
StaticPath string `mapstructure:"static_path"` // 静态文件路径(本地存储)
|
||||
BaseURL string `mapstructure:"base_url"` // 静态文件访问基础URL(本地存储)
|
||||
StorageType string `mapstructure:"storage_type"` // 存储类型:local(本地) 或 oss(阿里云OSS)
|
||||
OSS OSSConfig `mapstructure:"oss"` // OSS配置
|
||||
}
|
||||
|
||||
type OSSConfig struct {
|
||||
Endpoint string `mapstructure:"endpoint"` // OSS访问域名
|
||||
AccessKeyID string `mapstructure:"access_key_id"` // AccessKey ID
|
||||
AccessKeySecret string `mapstructure:"access_key_secret"` // AccessKey Secret
|
||||
BucketName string `mapstructure:"bucket_name"` // Bucket名称
|
||||
BasePath string `mapstructure:"base_path"` // 文件存储基础路径
|
||||
Domain string `mapstructure:"domain"` // 自定义域名(可选)
|
||||
}
|
||||
|
||||
// AliSmsConfig 阿里云短信配置
|
||||
type AliSmsConfig struct {
|
||||
AccessKeyID string `mapstructure:"access_key_id"` // AccessKey ID
|
||||
AccessKeySecret string `mapstructure:"access_key_secret"` // AccessKey Secret
|
||||
SignName string `mapstructure:"sign_name"` // 短信签名
|
||||
TemplateCode string `mapstructure:"template_code"` // 短信模板CODE
|
||||
}
|
||||
|
||||
var AppConfig *Config
|
||||
|
||||
// LoadConfig 加载配置文件
|
||||
@@ -100,8 +139,16 @@ func LoadConfig(env string) error {
|
||||
return fmt.Errorf("解析配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 打印OSS配置来源调试信息
|
||||
log.Printf("\n=== OSS配置来源检查 ===")
|
||||
log.Printf("upload.oss.access_key_secret 配置值: [%s]", AppConfig.Upload.OSS.AccessKeySecret)
|
||||
log.Printf("环境变量 OSS_ACCESS_KEY_SECRET: [%s]", os.Getenv("OSS_ACCESS_KEY_SECRET"))
|
||||
log.Printf("环境变量 OSS_TEST_ACCESS_KEY_SECRET: [%s]", os.Getenv("OSS_TEST_ACCESS_KEY_SECRET"))
|
||||
log.Printf("====================\n")
|
||||
|
||||
log.Printf("配置加载成功: %s 环境", env)
|
||||
log.Printf("数据库配置: %s@%s:%d/%s", AppConfig.Database.Username, AppConfig.Database.Host, AppConfig.Database.Port, AppConfig.Database.DBName)
|
||||
log.Printf("Python服务地址: %s", AppConfig.XHS.PythonServiceURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -119,6 +166,13 @@ func bindEnvVariables() {
|
||||
viper.BindEnv("database.dbname", "DB_NAME")
|
||||
viper.BindEnv("database.charset", "DB_CHARSET")
|
||||
|
||||
// Redis 配置
|
||||
viper.BindEnv("redis.host", "REDIS_HOST")
|
||||
viper.BindEnv("redis.port", "REDIS_PORT")
|
||||
viper.BindEnv("redis.password", "REDIS_PASSWORD")
|
||||
viper.BindEnv("redis.db", "REDIS_DB")
|
||||
viper.BindEnv("redis.pool_size", "REDIS_POOL_SIZE")
|
||||
|
||||
// JWT 配置
|
||||
viper.BindEnv("jwt.secret", "JWT_SECRET")
|
||||
viper.BindEnv("jwt.expire_hours", "JWT_EXPIRE_HOURS")
|
||||
@@ -142,6 +196,27 @@ func bindEnvVariables() {
|
||||
viper.BindEnv("scheduler.proxy", "SCHEDULER_PROXY")
|
||||
viper.BindEnv("scheduler.user_agent", "SCHEDULER_USER_AGENT")
|
||||
viper.BindEnv("scheduler.proxy_fetch_url", "SCHEDULER_PROXY_FETCH_URL")
|
||||
|
||||
// OSS 配置 - 强制从配置文件读取,不使用环境变量
|
||||
// viper.BindEnv("upload.oss.endpoint", "OSS_ENDPOINT")
|
||||
// viper.BindEnv("upload.oss.access_key_id", "OSS_ACCESS_KEY_ID")
|
||||
// viper.BindEnv("upload.oss.access_key_secret", "OSS_ACCESS_KEY_SECRET")
|
||||
// viper.BindEnv("upload.oss.bucket_name", "OSS_BUCKET_NAME")
|
||||
// viper.BindEnv("upload.oss.base_path", "OSS_BASE_PATH")
|
||||
// viper.BindEnv("upload.oss.domain", "OSS_DOMAIN")
|
||||
|
||||
// Upload 配置
|
||||
viper.BindEnv("upload.max_image_size", "UPLOAD_MAX_IMAGE_SIZE")
|
||||
viper.BindEnv("upload.max_file_size", "UPLOAD_MAX_FILE_SIZE")
|
||||
viper.BindEnv("upload.static_path", "UPLOAD_STATIC_PATH")
|
||||
viper.BindEnv("upload.base_url", "UPLOAD_BASE_URL")
|
||||
viper.BindEnv("upload.storage_type", "UPLOAD_STORAGE_TYPE")
|
||||
|
||||
// AliSms 配置
|
||||
viper.BindEnv("ali_sms.access_key_id", "ALI_SMS_ACCESS_KEY_ID")
|
||||
viper.BindEnv("ali_sms.access_key_secret", "ALI_SMS_ACCESS_KEY_SECRET")
|
||||
viper.BindEnv("ali_sms.sign_name", "ALI_SMS_SIGN_NAME")
|
||||
viper.BindEnv("ali_sms.template_code", "ALI_SMS_TEMPLATE_CODE")
|
||||
}
|
||||
|
||||
// GetDSN 获取数据库连接字符串
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
server:
|
||||
port: 8070
|
||||
mode: release
|
||||
mode: release # debug, release, test
|
||||
|
||||
database:
|
||||
host: 8.149.233.36
|
||||
port: 3306
|
||||
username: ai_wht_write
|
||||
password: 7aK_H2yvokVumr84lLNDt8fDBp6P
|
||||
password: 7aK_H2yvokVumr84lLNDt8fDBp6P # 生产环境请修改密码
|
||||
dbname: ai_wht
|
||||
charset: utf8mb4
|
||||
parse_time: true
|
||||
@@ -15,28 +15,58 @@ database:
|
||||
max_open_conns: 200
|
||||
conn_max_lifetime: 3600
|
||||
|
||||
redis:
|
||||
host: 8.140.194.184
|
||||
port: 6379
|
||||
password: Redis@123456
|
||||
db: 0
|
||||
pool_size: 20
|
||||
|
||||
jwt:
|
||||
secret: prod_secret_key_please_change_this
|
||||
expire_hours: 168
|
||||
secret: your_production_secret_key_change_this # 生产环境请修改密钥
|
||||
expire_hours: 168 # 7天
|
||||
|
||||
wechat:
|
||||
app_id: "wxa5bf062342ef754d" # 微信小程序AppID,留空则使用默认登录
|
||||
app_secret: "69d2a3ddc902b26f82f4b56a6e277f7a" # 微信小程序AppSecret
|
||||
app_id: "wxa5bf062342ef754d"
|
||||
app_secret: "69d2a3ddc902b26f82f4b56a6e277f7a"
|
||||
|
||||
xhs:
|
||||
python_service_url: "http://localhost:8000" # Python服务地址,生产环境请修改为实际地址
|
||||
python_service_url: "http://127.0.0.1:8020" # Python FastAPI服务地址(用于登录和发布,享受浏览器池+预热加速)
|
||||
|
||||
scheduler:
|
||||
enabled: false # 是否启用定时任务
|
||||
publish_cron: "0 0 */2 * * *" # 每2小时执行一次(防封号策略)
|
||||
max_concurrent: 2 # 最大并发发布数
|
||||
enabled: false # 生产环境启用定时任务
|
||||
publish_cron: "0 0 * * * *" # 每小时执行一次
|
||||
max_concurrent: 5 # 最大并发发布数
|
||||
publish_timeout: 300 # 发布超时时间(秒)
|
||||
max_articles_per_user_per_run: 2 # 每轮每个用户最大发文数
|
||||
max_articles_per_user_per_run: 5 # 每轮每个用户最大发文数
|
||||
max_failures_per_user_per_run: 3 # 每轮每个用户最大失败次数
|
||||
max_daily_articles_per_user: 5 # 每个用户每日最大发文数(自动发布)
|
||||
max_hourly_articles_per_user: 1 # 每个用户每小时最大发文数(自动发布)
|
||||
max_daily_articles_per_user: 20 # 每个用户每日最大发文数(自动发布)
|
||||
max_hourly_articles_per_user: 3 # 每个用户每小时最大发文数(自动发布)
|
||||
proxy: "" # 可选:静态全局代理地址,例如 http://user:pass@ip:port
|
||||
user_agent: "" # 可选:自定义User-Agent,不填则使用默认
|
||||
proxy_pool:
|
||||
enabled: true # 生产环境启用代理池
|
||||
api_url: "http://api.tianqiip.com/getip?secret=lu29e593&num=1&type=txt&port=1&mr=1&sign=4b81a62eaed89ba802a8f34053e2c964"
|
||||
api_url: "http://api.tianqiip.com/getip?secret=xo0uhiz5&num=1&type=txt&port=1&mr=1&sign=d82157fb70c21bae87437ec17eb3e0aa"
|
||||
|
||||
upload:
|
||||
max_image_size: 5242880 # 5MB
|
||||
max_file_size: 10485760 # 10MB
|
||||
image_types: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||
static_path: "./static"
|
||||
base_url: "https://your-domain.com" # 生产环境域名
|
||||
storage_type: "oss" # 生产环境使用OSS
|
||||
|
||||
oss:
|
||||
endpoint: "oss-cn-beijing.aliyuncs.com"
|
||||
access_key_id: "LTAI5tNesdhDH4ErqEUZmEg2"
|
||||
access_key_secret: "xZn7WUkTW76TqOLTh01zZATnU6p3Tf"
|
||||
bucket_name: "bxmkb-beijing"
|
||||
base_path: "wht/"
|
||||
domain: ""
|
||||
|
||||
# ========== 阿里云短信配置 ==========
|
||||
ali_sms:
|
||||
access_key_id: "LTAI5tSMvnCJdqkZtCVWgh8R" # 生产环境建议使用环境变量
|
||||
access_key_secret: "nyFzXyIi47peVLK4wR2qqbPezmU79W" # 生产环境建议使用环境变量
|
||||
sign_name: "北京乐航时代科技" # 短信签名
|
||||
template_code: "SMS_486210104" # 短信模板CODE
|
||||
|
||||
@@ -2,7 +2,10 @@ package controller
|
||||
|
||||
import (
|
||||
"ai_xhs/common"
|
||||
"ai_xhs/config"
|
||||
"ai_xhs/service"
|
||||
"ai_xhs/utils"
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -98,3 +101,137 @@ func (ctrl *AuthController) PhoneLogin(c *gin.Context) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// PhonePasswordLogin 手机号密码登录
|
||||
func (ctrl *AuthController) PhonePasswordLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用手机号密码登录服务
|
||||
token, employee, err := ctrl.authService.PhonePasswordLogin(req.Phone, req.Password)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户显示名称(优先使用真实姓名,其次用户名)
|
||||
displayName := employee.RealName
|
||||
if displayName == "" {
|
||||
displayName = employee.Username
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "登录成功", gin.H{
|
||||
"token": token,
|
||||
"employee": gin.H{
|
||||
"id": employee.ID,
|
||||
"name": displayName,
|
||||
"username": employee.Username,
|
||||
"real_name": employee.RealName,
|
||||
"phone": employee.Phone,
|
||||
"role": employee.Role,
|
||||
"enterprise_id": employee.EnterpriseID,
|
||||
"enterprise_name": employee.EnterpriseName,
|
||||
"is_bound_xhs": employee.IsBoundXHS,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// XHSPhoneCodeLogin 小红书手机号验证码登录
|
||||
func (ctrl *AuthController) XHSPhoneCodeLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用手机号验证码登录服务
|
||||
token, employee, err := ctrl.authService.XHSPhoneCodeLogin(req.Phone, req.Code)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户显示名称(优先使用真实姓名,其次用户名)
|
||||
displayName := employee.RealName
|
||||
if displayName == "" {
|
||||
displayName = employee.Username
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "登录成功", gin.H{
|
||||
"token": token,
|
||||
"employee": gin.H{
|
||||
"id": employee.ID,
|
||||
"name": displayName,
|
||||
"username": employee.Username,
|
||||
"real_name": employee.RealName,
|
||||
"phone": employee.Phone,
|
||||
"role": employee.Role,
|
||||
"enterprise_id": employee.EnterpriseID,
|
||||
"enterprise_name": employee.EnterpriseName,
|
||||
"is_bound_xhs": employee.IsBoundXHS,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SendXHSVerificationCode 发送小红书手机号验证码
|
||||
func (ctrl *AuthController) SendXHSVerificationCode(c *gin.Context) {
|
||||
var req struct {
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 预检查:验证手机号是否存在于user表中
|
||||
if err := ctrl.authService.CheckPhoneExists(req.Phone); err != nil {
|
||||
common.Error(c, common.CodeServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用短信服务发送验证码
|
||||
smsService := service.GetSmsService()
|
||||
code, err := smsService.SendVerificationCode(req.Phone)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 开发环境返回验证码,生产环境不返回
|
||||
response := gin.H{
|
||||
"message": "验证码已发送,5分钟内有效",
|
||||
}
|
||||
|
||||
if config.AppConfig.Server.Mode == "debug" {
|
||||
response["code"] = code // 仅开发环境返回
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "验证码已发送", response)
|
||||
}
|
||||
|
||||
// Logout 退出登录(删除Redis中的Token)
|
||||
func (ctrl *AuthController) Logout(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
|
||||
// 从Redis删除token
|
||||
ctx := context.Background()
|
||||
if err := utils.RevokeToken(ctx, employeeID); err != nil {
|
||||
// 即使删除失败也返回成功,因为token有过期时间
|
||||
common.SuccessWithMessage(c, "退出成功", nil)
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "退出成功", nil)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,17 @@ package controller
|
||||
|
||||
import (
|
||||
"ai_xhs/common"
|
||||
"ai_xhs/database"
|
||||
"ai_xhs/models"
|
||||
"ai_xhs/service"
|
||||
"ai_xhs/utils"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -29,7 +38,10 @@ func (ctrl *EmployeeController) SendXHSCode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := ctrl.service.SendXHSCode(req.XHSPhone)
|
||||
// 获取当前登录用户ID
|
||||
employeeID := c.GetInt("employee_id")
|
||||
|
||||
err := ctrl.service.SendXHSCode(req.XHSPhone, employeeID)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
@@ -59,24 +71,149 @@ func (ctrl *EmployeeController) GetProfile(c *gin.Context) {
|
||||
"name": displayName,
|
||||
"username": employee.Username,
|
||||
"real_name": employee.RealName,
|
||||
"nickname": employee.Nickname,
|
||||
"email": employee.Email,
|
||||
"phone": employee.Phone,
|
||||
"role": employee.Role,
|
||||
"enterprise_id": employee.EnterpriseID,
|
||||
"enterprise_name": employee.Enterprise.Name,
|
||||
"avatar": employee.Icon,
|
||||
"is_bound_xhs": employee.IsBoundXHS,
|
||||
"xhs_account": employee.XHSAccount,
|
||||
"xhs_phone": employee.XHSPhone,
|
||||
"has_xhs_cookie": employee.XHSCookie != "", // 标识是否有Cookie,不返回完整Cookie
|
||||
}
|
||||
|
||||
if employee.BoundAt != nil {
|
||||
data["bound_at"] = employee.BoundAt.Format("2006-01-02 15:04:05")
|
||||
// 如果已绑定,从 ai_authors 表获取小红书账号信息(根据 created_user_id 查询)
|
||||
if employee.IsBoundXHS == 1 {
|
||||
var author models.Author
|
||||
err := database.DB.Where(
|
||||
"created_user_id = ? AND enterprise_id = ? AND channel = 1 AND status = 'active'",
|
||||
employeeID, employee.EnterpriseID,
|
||||
).First(&author).Error
|
||||
|
||||
if err == nil {
|
||||
data["xhs_account"] = author.XHSAccount
|
||||
data["xhs_phone"] = author.XHSPhone
|
||||
data["has_xhs_cookie"] = author.XHSCookie != ""
|
||||
if author.BoundAt != nil {
|
||||
data["bound_at"] = author.BoundAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
} else {
|
||||
// 没有找到author记录,返回默认值
|
||||
data["xhs_account"] = ""
|
||||
data["xhs_phone"] = ""
|
||||
data["has_xhs_cookie"] = false
|
||||
}
|
||||
} else {
|
||||
data["xhs_account"] = ""
|
||||
data["xhs_phone"] = ""
|
||||
data["has_xhs_cookie"] = false
|
||||
}
|
||||
|
||||
common.Success(c, data)
|
||||
}
|
||||
|
||||
// BindXHS 绑定小红书账号
|
||||
// UpdateProfile 更新个人资料(昵称、邮箱、头像)
|
||||
func (ctrl *EmployeeController) UpdateProfile(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
|
||||
var req struct {
|
||||
Nickname *string `json:"nickname"`
|
||||
Email *string `json:"email"`
|
||||
Avatar *string `json:"avatar"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Nickname == nil && req.Email == nil && req.Avatar == nil {
|
||||
common.Error(c, common.CodeInvalidParams, "没有可更新的字段")
|
||||
return
|
||||
}
|
||||
|
||||
// 简单校验邮箱格式
|
||||
if req.Email != nil && *req.Email != "" {
|
||||
if !strings.Contains(*req.Email, "@") {
|
||||
common.Error(c, common.CodeInvalidParams, "邮箱格式不正确")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := ctrl.service.UpdateProfile(employeeID, req.Nickname, req.Email, req.Avatar); err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "更新成功", nil)
|
||||
}
|
||||
|
||||
// UploadAvatar 上传头像
|
||||
func (ctrl *EmployeeController) UploadAvatar(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "请选择要上传的图片")
|
||||
return
|
||||
}
|
||||
|
||||
// 校验文件类型
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
common.Error(c, common.CodeInvalidParams, "只能上传图片文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 校验文件大小(5MB)
|
||||
if file.Size > 5*1024*1024 {
|
||||
common.Error(c, common.CodeInvalidParams, "图片大小不能超过5MB")
|
||||
return
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, "打开文件失败")
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 读取文件内容
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = buf.ReadFrom(src)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, "读取文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 上传到 OSS
|
||||
fileExt := ".jpg"
|
||||
if strings.Contains(contentType, "png") {
|
||||
fileExt = ".png"
|
||||
} else if strings.Contains(contentType, "webp") {
|
||||
fileExt = ".webp"
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("avatar_%d_%d%s", employeeID, time.Now().Unix(), fileExt)
|
||||
ossURL, err := utils.UploadToOSS(bytes.NewReader(buf.Bytes()), fileName)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, fmt.Sprintf("上传失败: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
if err := ctrl.service.UpdateProfile(employeeID, nil, nil, &ossURL); err != nil {
|
||||
common.Error(c, common.CodeInternalError, "更新头像失败")
|
||||
return
|
||||
}
|
||||
|
||||
common.Success(c, map[string]interface{}{
|
||||
"url": ossURL,
|
||||
})
|
||||
}
|
||||
|
||||
// BindXHS 绑定小红书账号(异步处理)
|
||||
func (ctrl *EmployeeController) BindXHS(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
|
||||
@@ -90,17 +227,31 @@ func (ctrl *EmployeeController) BindXHS(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
xhsAccount, err := ctrl.service.BindXHS(employeeID, req.XHSPhone, req.Code)
|
||||
_, err := ctrl.service.BindXHS(employeeID, req.XHSPhone, req.Code)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeBindXHSFailed, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "绑定成功", map[string]interface{}{
|
||||
"xhs_account": xhsAccount,
|
||||
// 立即返回成功,告知前端正在处理
|
||||
common.SuccessWithMessage(c, "正在验证登录,请稍候...", map[string]interface{}{
|
||||
"status": "processing",
|
||||
})
|
||||
}
|
||||
|
||||
// GetBindXHSStatus 获取小红书绑定状态
|
||||
func (ctrl *EmployeeController) GetBindXHSStatus(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
|
||||
status, err := ctrl.service.GetBindXHSStatus(employeeID)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.Success(c, status)
|
||||
}
|
||||
|
||||
// UnbindXHS 解绑小红书账号
|
||||
func (ctrl *EmployeeController) UnbindXHS(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
@@ -246,14 +397,24 @@ func (ctrl *EmployeeController) CheckXHSStatus(c *gin.Context) {
|
||||
|
||||
// GetProducts 获取产品列表
|
||||
func (ctrl *EmployeeController) GetProducts(c *gin.Context) {
|
||||
data, err := ctrl.service.GetProducts()
|
||||
employeeID := c.GetInt("employee_id")
|
||||
if employeeID == 0 {
|
||||
common.Error(c, common.CodeUnauthorized, "未登录或token无效")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
|
||||
data, hasMore, err := ctrl.service.GetProducts(employeeID, page, pageSize)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.Success(c, map[string]interface{}{
|
||||
"list": data,
|
||||
"list": data,
|
||||
"has_more": hasMore,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -294,3 +455,294 @@ func (ctrl *EmployeeController) UpdateArticleStatus(c *gin.Context) {
|
||||
|
||||
common.SuccessWithMessage(c, message, nil)
|
||||
}
|
||||
|
||||
// UpdateArticleContent 更新文案内容(标题、正文)
|
||||
func (ctrl *EmployeeController) UpdateArticleContent(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
articleID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "文案ID参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证标题和内容字数
|
||||
if len([]rune(req.Title)) > 20 {
|
||||
common.Error(c, common.CodeInvalidParams, "标题最多20字")
|
||||
return
|
||||
}
|
||||
if len([]rune(req.Content)) > 1000 {
|
||||
common.Error(c, common.CodeInvalidParams, "内容最多1000字")
|
||||
return
|
||||
}
|
||||
|
||||
err = ctrl.service.UpdateArticleContent(employeeID, articleID, req.Title, req.Content)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "更新成功", nil)
|
||||
}
|
||||
|
||||
// UpdatePublishRecord 编辑发布记录(修改标题、内容、图片、标签)
|
||||
func (ctrl *EmployeeController) UpdatePublishRecord(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
recordID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "记录ID参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req service.UpdatePublishRecordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证标题和内容字数
|
||||
if req.Title != nil && len([]rune(*req.Title)) > 20 {
|
||||
common.Error(c, common.CodeInvalidParams, "标题最多20字")
|
||||
return
|
||||
}
|
||||
if req.Content != nil && len([]rune(*req.Content)) > 1000 {
|
||||
common.Error(c, common.CodeInvalidParams, "内容最多1000字")
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctrl.service.UpdatePublishRecord(employeeID, recordID, req); err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "更新成功", nil)
|
||||
}
|
||||
|
||||
// RepublishRecord 重新发布种草内容
|
||||
func (ctrl *EmployeeController) RepublishRecord(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
recordID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "记录ID参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
publishLink, err := ctrl.service.RepublishRecord(employeeID, recordID)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "重新发布成功", map[string]interface{}{
|
||||
"publish_link": publishLink,
|
||||
})
|
||||
}
|
||||
|
||||
// AddArticleImage 添加文案图片
|
||||
func (ctrl *EmployeeController) AddArticleImage(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
articleID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "文案ID参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ImageURL string `json:"image_url" binding:"required"`
|
||||
ImageThumbURL string `json:"image_thumb_url"`
|
||||
KeywordsName string `json:"keywords_name"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有缩略图,使用原图
|
||||
if req.ImageThumbURL == "" {
|
||||
req.ImageThumbURL = req.ImageURL
|
||||
}
|
||||
|
||||
image, err := ctrl.service.AddArticleImage(employeeID, articleID, req.ImageURL, req.ImageThumbURL, req.KeywordsName)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "添加成功", image)
|
||||
}
|
||||
|
||||
// DeleteArticleImage 删除文案图片
|
||||
func (ctrl *EmployeeController) DeleteArticleImage(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
imageID, err := strconv.Atoi(c.Param("imageId"))
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "图片ID参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = ctrl.service.DeleteArticleImage(employeeID, imageID)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "删除成功", nil)
|
||||
}
|
||||
|
||||
// UpdateArticleImagesOrder 更新文案图片排序
|
||||
func (ctrl *EmployeeController) UpdateArticleImagesOrder(c *gin.Context) {
|
||||
employeeID := c.GetInt("employee_id")
|
||||
articleID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "文案ID参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ImageOrders []map[string]int `json:"image_orders" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = ctrl.service.UpdateArticleImagesOrder(employeeID, articleID, req.ImageOrders)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "更新成功", nil)
|
||||
}
|
||||
|
||||
// UploadImage 上传图片(支持base64和multipart/form-data)
|
||||
func (ctrl *EmployeeController) UploadImage(c *gin.Context) {
|
||||
// 尝试从表单获取文件
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err == nil {
|
||||
// 处理文件上传
|
||||
defer file.Close()
|
||||
|
||||
// 验证文件类型
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
common.Error(c, common.CodeInvalidParams, "只支持图片文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 上传到OSS
|
||||
imageURL, err := utils.UploadToOSS(file, header.Filename)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, fmt.Sprintf("上传失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "上传成功", map[string]interface{}{
|
||||
"image_url": imageURL,
|
||||
"image_thumb_url": imageURL, // 简化处理,缩略图与原图相同
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试介ase64获取
|
||||
var req struct {
|
||||
Base64 string `json:"base64" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "请上传文件或base64数据")
|
||||
return
|
||||
}
|
||||
|
||||
// 解析base64
|
||||
var imageData []byte
|
||||
if strings.Contains(req.Base64, "base64,") {
|
||||
// 移除data:image/xxx;base64,前缀
|
||||
parts := strings.Split(req.Base64, "base64,")
|
||||
if len(parts) != 2 {
|
||||
common.Error(c, common.CodeInvalidParams, "base64格式错误")
|
||||
return
|
||||
}
|
||||
imageData, err = base64.StdEncoding.DecodeString(parts[1])
|
||||
} else {
|
||||
imageData, err = base64.StdEncoding.DecodeString(req.Base64)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "base64解码失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 上传到OSS
|
||||
reader := bytes.NewReader(imageData)
|
||||
imageURL, err := utils.UploadToOSS(reader, "image.jpg")
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, fmt.Sprintf("上传失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "上传成功", map[string]interface{}{
|
||||
"image_url": imageURL,
|
||||
"image_thumb_url": imageURL,
|
||||
})
|
||||
}
|
||||
|
||||
// RevokeUserToken 禁用用户(撤销Token)
|
||||
func (ctrl *EmployeeController) RevokeUserToken(c *gin.Context) {
|
||||
// 只有管理员可以禁用用户
|
||||
employeeID := c.GetInt("employee_id")
|
||||
|
||||
// 获取当前用户信息,检查是否为管理员
|
||||
var currentUser models.User
|
||||
if err := database.DB.Where("id = ?", employeeID).First(¤tUser).Error; err != nil {
|
||||
common.Error(c, common.CodeUnauthorized, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if currentUser.Role != "admin" {
|
||||
common.Error(c, common.CodeUnauthorized, "无权操作,只有管理员可以禁用用户")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TargetUserID int `json:"target_user_id" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误:需要提供目标用户ID")
|
||||
return
|
||||
}
|
||||
|
||||
// 不能禁用自己
|
||||
if req.TargetUserID == employeeID {
|
||||
common.Error(c, common.CodeInvalidParams, "不能禁用自己")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查目标用户是否存在
|
||||
var targetUser models.User
|
||||
if err := database.DB.Where("id = ?", req.TargetUserID).First(&targetUser).Error; err != nil {
|
||||
common.Error(c, common.CodeNotFound, "目标用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 撤销该用户的Token
|
||||
ctx := context.Background()
|
||||
if err := utils.RevokeToken(ctx, req.TargetUserID); err != nil {
|
||||
common.Error(c, common.CodeInternalError, fmt.Sprintf("禁用失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, fmt.Sprintf("已禁用用户 %s (手机号: %s),该用户需要重新登录", targetUser.Username, targetUser.Phone), nil)
|
||||
}
|
||||
|
||||
104
go_backend/controller/feedback_controller.go
Normal file
104
go_backend/controller/feedback_controller.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"ai_xhs/common"
|
||||
"ai_xhs/models"
|
||||
"ai_xhs/service"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CreateFeedbackRequest 创建反馈请求
|
||||
type CreateFeedbackRequest struct {
|
||||
FeedbackType string `json:"feedback_type" binding:"required"`
|
||||
Description string `json:"description" binding:"required,max=500"`
|
||||
ContactInfo string `json:"contact_info"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
// FeedbackController 反馈控制器
|
||||
type FeedbackController struct {
|
||||
feedbackService *service.FeedbackService
|
||||
}
|
||||
|
||||
// NewFeedbackController 创建反馈控制器
|
||||
func NewFeedbackController(feedbackService *service.FeedbackService) *FeedbackController {
|
||||
return &FeedbackController{
|
||||
feedbackService: feedbackService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFeedback 创建反馈
|
||||
func (fc *FeedbackController) CreateFeedback(c *gin.Context) {
|
||||
var req CreateFeedbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 从上下文获取员工ID
|
||||
employeeID, exists := c.Get("employee_id")
|
||||
if !exists {
|
||||
common.Error(c, common.CodeUnauthorized, "未登录")
|
||||
return
|
||||
}
|
||||
|
||||
feedback := &models.Feedback{
|
||||
FeedbackType: req.FeedbackType,
|
||||
Description: req.Description,
|
||||
ContactInfo: req.ContactInfo,
|
||||
Nickname: req.Nickname,
|
||||
CreatedUserID: employeeID.(int),
|
||||
Status: "待处理",
|
||||
}
|
||||
|
||||
if err := fc.feedbackService.CreateFeedback(feedback); err != nil {
|
||||
common.Error(c, common.CodeInternalError, "提交反馈失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "反馈提交成功", feedback)
|
||||
}
|
||||
|
||||
// GetFeedbackList 获取反馈列表
|
||||
func (fc *FeedbackController) GetFeedbackList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
feedbackType := c.Query("feedback_type")
|
||||
status := c.Query("status")
|
||||
|
||||
// 从上下文获取员工ID(仅查看自己的反馈)
|
||||
employeeID, exists := c.Get("employee_id")
|
||||
if !exists {
|
||||
common.Error(c, common.CodeUnauthorized, "未登录")
|
||||
return
|
||||
}
|
||||
|
||||
feedbacks, total, err := fc.feedbackService.GetFeedbackList(employeeID.(int), page, pageSize, feedbackType, status)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInternalError, "获取反馈列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, common.SuccessResponseWithPage(feedbacks, total, page, pageSize, "获取成功"))
|
||||
}
|
||||
|
||||
// GetFeedbackDetail 获取反馈详情
|
||||
func (fc *FeedbackController) GetFeedbackDetail(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeInvalidParams, "无效的反馈ID")
|
||||
return
|
||||
}
|
||||
|
||||
feedback, err := fc.feedbackService.GetFeedbackByID(id)
|
||||
if err != nil {
|
||||
common.Error(c, common.CodeNotFound, "反馈不存在")
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithMessage(c, "获取成功", feedback)
|
||||
}
|
||||
44
go_backend/database/redis.go
Normal file
44
go_backend/database/redis.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"ai_xhs/config"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var RDB *redis.Client
|
||||
|
||||
// InitRedis 初始化Redis连接
|
||||
func InitRedis() error {
|
||||
cfg := config.AppConfig.Redis
|
||||
|
||||
RDB = redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
PoolSize: cfg.PoolSize,
|
||||
})
|
||||
|
||||
// 测试连接
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := RDB.Ping(ctx).Err(); err != nil {
|
||||
return fmt.Errorf("Redis连接失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Redis连接成功: %s:%d (DB:%d)", cfg.Host, cfg.Port, cfg.DB)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseRedis 关闭Redis连接
|
||||
func CloseRedis() error {
|
||||
if RDB != nil {
|
||||
return RDB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -3,16 +3,32 @@ module ai_xhs
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
|
||||
github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.3
|
||||
github.com/alibabacloud-go/tea v1.3.14
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/viper v1.18.2
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.5 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@@ -33,7 +49,6 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
@@ -41,16 +56,18 @@ require (
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -1,13 +1,85 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11/go.mod h1:wHxkgZT1ClZdcwEVP/pDgYK/9HucsnCfMipmJgCz4xY=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.3 h1:32N2pGk28weVZ5/rjNk9gPx/jrRkR0rX9i8Id6IlyUY=
|
||||
github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.3/go.mod h1:gPbHx4BTxLIDNRfYNGGmp6aIpeNBamtdDlPcK4UTUto=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw=
|
||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea v1.3.14 h1:/Uzj5ZCFPpbPR+Bs7jfzsyXkYIVsi5TOIuQNOWwc/9c=
|
||||
github.com/alibabacloud-go/tea v1.3.14/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
|
||||
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
@@ -32,24 +104,47 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
@@ -63,13 +158,20 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
@@ -78,6 +180,9 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
@@ -89,9 +194,11 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -101,10 +208,16 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
@@ -112,27 +225,153 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -141,4 +380,6 @@ gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb
|
||||
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"ai_xhs/middleware"
|
||||
"ai_xhs/router"
|
||||
"ai_xhs/service"
|
||||
// "ai_xhs/tools" // 临时注释,避免包冲突
|
||||
"ai_xhs/utils"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -28,6 +30,35 @@ func main() {
|
||||
log.Fatalf("数据库初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始化Redis
|
||||
if err := database.InitRedis(); err != nil {
|
||||
log.Fatalf("Redis初始化失败: %v", err)
|
||||
}
|
||||
defer database.CloseRedis()
|
||||
|
||||
// 初始化OSS
|
||||
log.Printf("OSS配置: Endpoint=%s, AccessKeyID=%s..., AccessKeySecret=%s..., BucketName=%s",
|
||||
config.AppConfig.Upload.OSS.Endpoint,
|
||||
config.AppConfig.Upload.OSS.AccessKeyID[:8],
|
||||
config.AppConfig.Upload.OSS.AccessKeySecret[:8],
|
||||
config.AppConfig.Upload.OSS.BucketName)
|
||||
|
||||
if err := utils.InitOSS(); err != nil {
|
||||
log.Fatalf("OSS初始化失败: %v", err)
|
||||
}
|
||||
log.Println("OSS客户端初始化成功")
|
||||
|
||||
// 初始化短信服务
|
||||
smsService := service.GetSmsService()
|
||||
smsService.StartCleanupTask()
|
||||
log.Println("短信服务已初始化")
|
||||
|
||||
// 启动服务监控(宕机时发送短信通知)
|
||||
// 临时注释:避免tools包冲突导致编译失败
|
||||
// monitor := tools.GetServiceMonitor("15707023967", "AI小红书服务")
|
||||
// monitor.StartMonitoring()
|
||||
// log.Println("服务监控已启动")
|
||||
|
||||
// 自动迁移数据库表
|
||||
//if err := database.AutoMigrate(); err != nil {
|
||||
// log.Fatalf("数据库迁移失败: %v", err)
|
||||
@@ -53,9 +84,9 @@ func main() {
|
||||
|
||||
// 创建路由
|
||||
r := gin.New()
|
||||
|
||||
|
||||
// 添加中间件
|
||||
r.Use(gin.Recovery()) // 崩溃恢复
|
||||
r.Use(gin.Recovery()) // 崩溃恢复
|
||||
r.Use(middleware.RequestLogger()) // API请求日志
|
||||
|
||||
// 设置路由
|
||||
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"ai_xhs/common"
|
||||
"ai_xhs/utils"
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -35,6 +36,14 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 新增:验证token是否在Redis中存在(校验是否被禁用)
|
||||
ctx := context.Background()
|
||||
if err := utils.ValidateTokenInRedis(ctx, claims.EmployeeID, parts[1]); err != nil {
|
||||
common.Error(c, common.CodeUnauthorized, err.Error())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将员工ID存入上下文
|
||||
c.Set("employee_id", claims.EmployeeID)
|
||||
c.Next()
|
||||
|
||||
@@ -27,9 +27,12 @@ func RequestLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 读取请求体
|
||||
// 读取请求体(跳过文件上传)
|
||||
var requestBody []byte
|
||||
if c.Request.Body != nil {
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
|
||||
// 如果不是文件上传,才读取请求体
|
||||
if c.Request.Body != nil && !strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
requestBody, _ = io.ReadAll(c.Request.Body)
|
||||
// 恢复请求体供后续处理使用
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
@@ -61,14 +64,14 @@ func printRequest(c *gin.Context, body []byte) {
|
||||
fmt.Println("\n" + strings.Repeat("=", 100))
|
||||
fmt.Printf("📥 [REQUEST] %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Println(strings.Repeat("=", 100))
|
||||
|
||||
|
||||
// 请求基本信息
|
||||
fmt.Printf("Method: %s\n", c.Request.Method)
|
||||
fmt.Printf("Path: %s\n", c.Request.URL.Path)
|
||||
fmt.Printf("Full URL: %s\n", c.Request.URL.String())
|
||||
fmt.Printf("Client IP: %s\n", c.ClientIP())
|
||||
fmt.Printf("User-Agent: %s\n", c.Request.UserAgent())
|
||||
|
||||
|
||||
// 请求头
|
||||
if len(c.Request.Header) > 0 {
|
||||
fmt.Println("\n--- Headers ---")
|
||||
@@ -100,6 +103,9 @@ func printRequest(c *gin.Context, body []byte) {
|
||||
} else {
|
||||
fmt.Println(string(body))
|
||||
}
|
||||
} else if strings.HasPrefix(c.GetHeader("Content-Type"), "multipart/form-data") {
|
||||
fmt.Println("\n--- Request Body ---")
|
||||
fmt.Println("[File upload: multipart/form-data]")
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("-", 100))
|
||||
@@ -110,11 +116,11 @@ func printResponse(c *gin.Context, body []byte, duration time.Duration) {
|
||||
fmt.Println("\n" + strings.Repeat("=", 100))
|
||||
fmt.Printf("📤 [RESPONSE] %s | Duration: %v\n", time.Now().Format("2006-01-02 15:04:05"), duration)
|
||||
fmt.Println(strings.Repeat("=", 100))
|
||||
|
||||
|
||||
// 响应基本信息
|
||||
fmt.Printf("Status Code: %d %s\n", c.Writer.Status(), getStatusText(c.Writer.Status()))
|
||||
fmt.Printf("Size: %d bytes\n", c.Writer.Size())
|
||||
|
||||
|
||||
// 响应头
|
||||
if len(c.Writer.Header()) > 0 {
|
||||
fmt.Println("\n--- Response Headers ---")
|
||||
@@ -126,12 +132,21 @@ func printResponse(c *gin.Context, body []byte, duration time.Duration) {
|
||||
// 响应体
|
||||
if len(body) > 0 {
|
||||
fmt.Println("\n--- Response Body ---")
|
||||
// 尝试格式化 JSON
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, body, "", " "); err == nil {
|
||||
fmt.Println(prettyJSON.String())
|
||||
|
||||
// 检查Content-Type,跳过二进制数据
|
||||
contentType := c.Writer.Header().Get("Content-Type")
|
||||
if strings.Contains(contentType, "image/") ||
|
||||
strings.Contains(contentType, "application/octet-stream") ||
|
||||
len(body) > 10240 { // 超过10KB的响应不打印
|
||||
fmt.Printf("[Binary data: %d bytes, Content-Type: %s]\n", len(body), contentType)
|
||||
} else {
|
||||
fmt.Println(string(body))
|
||||
// 尝试格式化 JSON
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, body, "", " "); err == nil {
|
||||
fmt.Println(prettyJSON.String())
|
||||
} else {
|
||||
fmt.Println(string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
go_backend/migrations/add_xhs_storage_state_path.sql
Normal file
8
go_backend/migrations/add_xhs_storage_state_path.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- 添加小红书storage_state文件路径字段
|
||||
-- 用于存储Playwright storage_state文件的路径,提升登录态恢复的可靠性
|
||||
|
||||
ALTER TABLE `ai_authors`
|
||||
ADD COLUMN `xhs_storage_state_path` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '小红书storage_state文件路径' AFTER `xhs_cookie`;
|
||||
|
||||
-- 为方便查询,添加索引
|
||||
CREATE INDEX `idx_xhs_storage_state_path` ON `ai_authors` (`xhs_storage_state_path`);
|
||||
16
go_backend/migrations/create_feedback_table.sql
Normal file
16
go_backend/migrations/create_feedback_table.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 创建意见反馈表
|
||||
CREATE TABLE IF NOT EXISTS ai_feedback (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||||
feedback_type ENUM('功能建议', 'Bug反馈', '体验问题', '其他') NOT NULL COMMENT '反馈类型',
|
||||
description TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '问题描述,最多500字',
|
||||
contact_info VARCHAR(255) DEFAULT NULL COMMENT '联系方式(如邮箱),选填',
|
||||
nickname VARCHAR(255) NOT NULL DEFAULT '' COMMENT '填写用户昵称',
|
||||
created_user_id INT(10) UNSIGNED NOT NULL COMMENT '创建该反馈的用户ID,关联用户表',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
status ENUM('待处理', '处理中', '已解决', '已关闭') DEFAULT '待处理' COMMENT '反馈状态',
|
||||
|
||||
INDEX idx_created_user_id (created_user_id),
|
||||
INDEX idx_feedback_type (feedback_type),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI系统用户反馈表';
|
||||
40
go_backend/migrations/init_password_login.sql
Normal file
40
go_backend/migrations/init_password_login.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- 手机号密码登录 - 测试数据初始化脚本
|
||||
-- 为测试用户设置密码(使用 SHA256 加密)
|
||||
|
||||
-- 常用密码哈希值(SHA256):
|
||||
-- admin123: 240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9
|
||||
-- user123: e606e38b0d8c19b24cf0ee3808183162ea7cd63ff7912dbb22b5e803286b4446
|
||||
-- 123456: 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
|
||||
|
||||
-- 示例1:为手机号 13800138000 的用户设置密码为 123456
|
||||
UPDATE ai_users
|
||||
SET password = '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'
|
||||
WHERE phone = '13800138000' AND status = 'active';
|
||||
|
||||
-- 示例2:为手机号 13800138001 的用户设置密码为 user123
|
||||
UPDATE ai_users
|
||||
SET password = 'e606e38b0d8c19b24cf0ee3808183162ea7cd63ff7912dbb22b5e803286b4446'
|
||||
WHERE phone = '13800138001' AND status = 'active';
|
||||
|
||||
-- 示例3:为手机号 13800138002 的用户设置密码为 admin123
|
||||
UPDATE ai_users
|
||||
SET password = '240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9'
|
||||
WHERE phone = '13800138002' AND status = 'active';
|
||||
|
||||
-- 查询验证(password 字段默认不显示,需要手动选择)
|
||||
SELECT
|
||||
id,
|
||||
phone,
|
||||
username,
|
||||
real_name,
|
||||
LEFT(password, 20) as password_preview,
|
||||
status,
|
||||
created_at
|
||||
FROM ai_users
|
||||
WHERE phone IN ('13800138000', '13800138001', '13800138002')
|
||||
ORDER BY id;
|
||||
|
||||
-- 注意事项:
|
||||
-- 1. 密码使用 SHA256 加密存储,不可逆
|
||||
-- 2. 如需生成新密码哈希,使用工具:go run tools/generate_password.go [密码]
|
||||
-- 3. 测试时使用明文密码登录,系统会自动验证哈希值
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
// Enterprise 企业表
|
||||
type Enterprise struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID string `gorm:"type:varchar(255);not null;default:''" json:"enterprise_id" comment:"企业ID"`
|
||||
ID int `gorm:"primaryKey;column:id;autoIncrement" json:"id"`
|
||||
EnterpriseID string `gorm:"column:enterprise_id;type:varchar(255);not null;default:''" json:"enterprise_id" comment:"企业ID"`
|
||||
Name string `gorm:"type:varchar(200);not null;default:''" json:"name" comment:"企业名称"`
|
||||
ShortName string `gorm:"type:varchar(100);not null;default:''" json:"short_name" comment:"企业简称"`
|
||||
Icon string `gorm:"type:varchar(500);not null;default:''" json:"icon" comment:"企业图标URL"`
|
||||
@@ -27,24 +27,22 @@ type Enterprise struct {
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// User 用户账号表(原Employee,对应ai_users)
|
||||
// User 用户账号表(原Employee,对应ai_users)
|
||||
type User struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"`
|
||||
Enterprise Enterprise `gorm:"foreignKey:EnterpriseID" json:"enterprise,omitempty"`
|
||||
Enterprise Enterprise `gorm:"foreignKey:EnterpriseID;references:ID" json:"enterprise,omitempty"`
|
||||
EnterpriseName string `gorm:"type:varchar(255);not null;default:''" json:"enterprise_name" comment:"企业名称"`
|
||||
Username string `gorm:"type:varchar(50);not null;default:'';uniqueIndex:uk_username" json:"username" comment:"用户名"`
|
||||
Password string `gorm:"type:varchar(255);not null;default:''" json:"-" comment:"密码"`
|
||||
RealName string `gorm:"type:varchar(50)" json:"real_name" comment:"真实姓名"`
|
||||
Nickname string `gorm:"type:varchar(256);not null;default:''" json:"nickname" comment:"用户昵称"`
|
||||
Icon string `gorm:"type:varchar(512);not null;default:''" json:"icon" comment:"企业图标URL"`
|
||||
Email string `gorm:"type:varchar(100)" json:"email" comment:"邮箱"`
|
||||
Phone string `gorm:"type:varchar(20)" json:"phone" comment:"手机号"`
|
||||
WechatOpenID *string `gorm:"column:wechat_openid;type:varchar(100);uniqueIndex:uk_wechat_openid" json:"wechat_openid,omitempty" comment:"微信OpenID"`
|
||||
WechatUnionID *string `gorm:"column:wechat_unionid;type:varchar(100)" json:"wechat_unionid,omitempty" comment:"微信UnionID"`
|
||||
XHSPhone string `gorm:"type:varchar(20);not null;default:''" json:"xhs_phone" comment:"小红书绑定手机号"`
|
||||
XHSAccount string `gorm:"type:varchar(255);not null;default:''" json:"xhs_account" comment:"小红书账号名称"`
|
||||
XHSCookie string `gorm:"type:text" json:"xhs_cookie" comment:"小红书Cookie"`
|
||||
IsBoundXHS int `gorm:"type:tinyint(1);not null;default:0;index:idx_is_bound_xhs" json:"is_bound_xhs" comment:"是否绑定小红书:0=未绑定,1=已绑定"`
|
||||
BoundAt *time.Time `json:"bound_at" comment:"绑定小红书的时间"`
|
||||
Department string `gorm:"type:varchar(50)" json:"department" comment:"部门"`
|
||||
Role string `gorm:"type:enum('admin','editor','reviewer','publisher','each_title_reviewer','enterprise');default:'editor'" json:"role" comment:"角色"`
|
||||
Status string `gorm:"type:enum('active','inactive','deleted');default:'active';index:idx_status" json:"status" comment:"状态"`
|
||||
@@ -72,37 +70,43 @@ type Product struct {
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// Article 文章表(原Copy,对应ai_articles)
|
||||
// Article 文章表(原Copy,对应ai_articles)
|
||||
type Article struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
BatchID uint64 `gorm:"type:bigint unsigned;not null;default:0" json:"batch_id" comment:"批次ID"`
|
||||
EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"`
|
||||
ProductID int `gorm:"not null;default:0;index:idx_product_id" json:"product_id" comment:"关联产品ID"`
|
||||
TopicTypeID int `gorm:"type:int unsigned;not null;default:0" json:"topic_type_id" comment:"topic类型ID"`
|
||||
PromptWorkflowID int `gorm:"type:int unsigned;not null;default:0" json:"prompt_workflow_id" comment:"提示词工作流ID"`
|
||||
Topic string `gorm:"type:varchar(255);not null;default:''" json:"topic" comment:"topic主题"`
|
||||
Title string `gorm:"type:varchar(200);not null;default:''" json:"title" comment:"标题"`
|
||||
Content string `gorm:"type:text" json:"content" comment:"文章内容"`
|
||||
Department string `gorm:"type:varchar(255);not null;default:''" json:"department" comment:"部门"`
|
||||
DepartmentIDs string `gorm:"column:departmentids;type:varchar(255);not null;default:''" json:"department_ids" comment:"部门IDs"`
|
||||
AuthorID *int `json:"author_id" comment:"作者ID"`
|
||||
AuthorName string `gorm:"type:varchar(100)" json:"author_name" comment:"作者名称"`
|
||||
DepartmentID *int `json:"department_id" comment:"部门ID"`
|
||||
DepartmentName string `gorm:"type:varchar(255)" json:"department_name" comment:"部门名称"`
|
||||
CreatedUserID int `gorm:"not null;default:0" json:"created_user_id" comment:"创建用户ID"`
|
||||
ReviewUserID *int `json:"review_user_id" comment:"审核用户ID"`
|
||||
PublishUserID *int `json:"publish_user_id" comment:"发布用户ID"`
|
||||
Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','assign_authors','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=baidu|2=toutiao|3=weixin"`
|
||||
ReviewComment string `gorm:"type:text" json:"review_comment" comment:"审核评论"`
|
||||
PublishTime *time.Time `json:"publish_time" comment:"发布时间"`
|
||||
BaijiahaoID string `gorm:"type:varchar(100)" json:"baijiahao_id" comment:"百家号ID"`
|
||||
BaijiahaoStatus string `gorm:"type:varchar(50)" json:"baijiahao_status" comment:"百家号状态"`
|
||||
WordCount int `gorm:"default:0" json:"word_count" comment:"字数统计"`
|
||||
ImageCount int `gorm:"default:0" json:"image_count" comment:"图片数量"`
|
||||
CozeTag string `gorm:"type:varchar(500)" json:"coze_tag" comment:"Coze生成的标签"`
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"index:idx_updated_at" json:"updated_at" comment:"更新时间"`
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
BatchID uint64 `gorm:"type:bigint unsigned;not null;default:0" json:"batch_id" comment:"批次ID"`
|
||||
EnterpriseID int `gorm:"not null;default:0;index:idx_enterprise_id" json:"enterprise_id" comment:"所属企业ID"`
|
||||
ProductID int `gorm:"not null;default:0;index:idx_product_id" json:"product_id" comment:"关联产品ID"`
|
||||
ProductName string `gorm:"type:varchar(256);not null;default:''" json:"product_name" comment:"产品名称"`
|
||||
TopicTypeID int `gorm:"type:int unsigned;not null;default:0" json:"topic_type_id" comment:"topic类型ID"`
|
||||
PromptWorkflowID int `gorm:"type:int unsigned;not null;default:0" json:"prompt_workflow_id" comment:"提示词工作流ID"`
|
||||
PromptWorkflowName string `gorm:"type:varchar(100);not null;default:''" json:"prompt_workflow_name" comment:"提示词工作流名称"`
|
||||
Topic string `gorm:"type:varchar(255);not null;default:''" json:"topic" comment:"topic主题"`
|
||||
Title string `gorm:"type:varchar(200);not null;default:''" json:"title" comment:"标题"`
|
||||
ContextSummary string `gorm:"type:varchar(1024);not null;default:''" json:"context_summary" comment:"上下文摘要"`
|
||||
Content string `gorm:"type:text" json:"content" comment:"文章内容"`
|
||||
Department string `gorm:"type:varchar(255);not null;default:''" json:"department" comment:"部门"`
|
||||
DepartmentIDs string `gorm:"column:departmentids;type:varchar(255);not null;default:''" json:"department_ids" comment:"部门IDs"`
|
||||
AuthorID *int `json:"author_id" comment:"作者ID"`
|
||||
AuthorName string `gorm:"type:varchar(100)" json:"author_name" comment:"作者名称"`
|
||||
DepartmentID *int `json:"department_id" comment:"部门ID"`
|
||||
DepartmentName string `gorm:"type:varchar(255)" json:"department_name" comment:"部门名称"`
|
||||
CreatedUserID int `gorm:"not null;default:0" json:"created_user_id" comment:"创建用户ID"`
|
||||
ReviewUserID *int `json:"review_user_id" comment:"审核用户ID"`
|
||||
PublishUserID *int `json:"publish_user_id" comment:"发布用户ID"`
|
||||
Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','assign_authors','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=小红书|2=douyin|3=toutiao|4=weixin"`
|
||||
ReviewComment string `gorm:"type:text" json:"review_comment" comment:"审核评论"`
|
||||
PublishTime *time.Time `json:"publish_time" comment:"发布时间"`
|
||||
BaijiahaoID string `gorm:"type:varchar(100)" json:"baijiahao_id" comment:"百家号ID"`
|
||||
BaijiahaoStatus string `gorm:"type:varchar(50)" json:"baijiahao_status" comment:"百家号状态"`
|
||||
WordCount int `gorm:"default:0" json:"word_count" comment:"字数统计"`
|
||||
ImageCount int `gorm:"default:0" json:"image_count" comment:"图片数量"`
|
||||
CozeTag string `gorm:"type:varchar(500)" json:"coze_tag" comment:"Coze生成的标签"`
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"index:idx_updated_at" json:"updated_at" comment:"更新时间"`
|
||||
|
||||
// 关联字段
|
||||
Images []ArticleImage `gorm:"foreignKey:ArticleID" json:"images,omitempty" comment:"文章图片"`
|
||||
}
|
||||
|
||||
// Copy 文案表别名,兼容旧代码
|
||||
@@ -120,10 +124,10 @@ type PublishRecord struct {
|
||||
ReviewUserID *int `json:"review_user_id" comment:"审核用户ID"`
|
||||
PublishUserID *int `json:"publish_user_id" comment:"发布用户ID"`
|
||||
Status string `gorm:"type:enum('topic','cover_image','generate','generate_failed','draft','pending_review','assign_authors','approved','rejected','published_review','published','failed');default:'draft';index:idx_status" json:"status" comment:"状态"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=baidu|2=toutiao|3=weixin"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=小红书|2=douyin|3=toutiao|4=weixin"`
|
||||
ReviewComment string `gorm:"type:text" json:"review_comment" comment:"审核评论"`
|
||||
PublishTime *time.Time `json:"publish_time" comment:"发布时间"`
|
||||
PublishLink string `gorm:"type:varchar(128);not null;default:''" json:"publish_link" comment:"发布访问链接"`
|
||||
PublishLink string `gorm:"type:varchar(255);not null;default:''" json:"publish_link" comment:"发布访问链接"`
|
||||
WordCount int `gorm:"default:0" json:"word_count" comment:"字数统计"`
|
||||
ImageCount int `gorm:"default:0" json:"image_count" comment:"图片数量"`
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
@@ -243,29 +247,46 @@ type Log struct {
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
}
|
||||
|
||||
// Author 作者表(对应ai_authors)
|
||||
// Author 作者表(对应ai_authors)
|
||||
type Author struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID int `gorm:"not null;default:0" json:"enterprise_id" comment:"所属企业ID"`
|
||||
CreatedUserID int `gorm:"not null;default:0" json:"created_user_id" comment:"创建用户ID"`
|
||||
Phone string `gorm:"type:varchar(20)" json:"phone" comment:"手机号"`
|
||||
AuthorName string `gorm:"type:varchar(100);not null;default:''" json:"author_name" comment:"作者名称"`
|
||||
AppID string `gorm:"type:varchar(127);not null;default:''" json:"app_id" comment:"应用ID"`
|
||||
AppToken string `gorm:"type:varchar(127);not null;default:''" json:"app_token" comment:"应用Token"`
|
||||
DepartmentID int `gorm:"not null;default:0" json:"department_id" comment:"部门ID"`
|
||||
DepartmentName string `gorm:"type:varchar(255);not null;default:''" json:"department_name" comment:"部门名称"`
|
||||
Department string `gorm:"type:varchar(50);not null;default:''" json:"department" comment:"部门"`
|
||||
Title string `gorm:"type:varchar(50)" json:"title" comment:"职称"`
|
||||
Hospital string `gorm:"type:varchar(100)" json:"hospital" comment:"医院"`
|
||||
Specialty string `gorm:"type:text" json:"specialty" comment:"专业"`
|
||||
ToutiaoCookie string `gorm:"type:text" json:"toutiao_cookie" comment:"头条Cookie"`
|
||||
ToutiaoImagesCookie string `gorm:"type:text" json:"toutiao_images_cookie" comment:"头条图片Cookie"`
|
||||
Introduction string `gorm:"type:text" json:"introduction" comment:"介绍"`
|
||||
AvatarURL string `gorm:"type:varchar(255)" json:"avatar_url" comment:"头像URL"`
|
||||
Status string `gorm:"type:enum('active','inactive');default:'active';index:idx_status" json:"status" comment:"状态"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=baidu|2=toutiao|3=weixin"`
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
EnterpriseID int `gorm:"not null;default:0" json:"enterprise_id" comment:"所属企业ID"`
|
||||
CreatedUserID int `gorm:"not null;default:0" json:"created_user_id" comment:"创建用户ID"`
|
||||
Phone string `gorm:"type:varchar(20)" json:"phone" comment:"手机号"`
|
||||
AuthorName string `gorm:"type:varchar(100);not null;default:''" json:"author_name" comment:"作者名称"`
|
||||
XHSCookie string `gorm:"type:text" json:"xhs_cookie" comment:"小红书登录状态(login_state JSON)"`
|
||||
XHSPhone string `gorm:"type:varchar(20);not null;default:''" json:"xhs_phone" comment:"小红书绑定手机号"`
|
||||
XHSAccount string `gorm:"type:varchar(255);not null;default:''" json:"xhs_account" comment:"小红书账号名称"`
|
||||
BoundAt *time.Time `json:"bound_at" comment:"绑定的时间"`
|
||||
AppID string `gorm:"type:varchar(127);not null;default:''" json:"app_id" comment:"应用ID"`
|
||||
AppToken string `gorm:"type:varchar(127);not null;default:''" json:"app_token" comment:"应用Token"`
|
||||
DepartmentID int `gorm:"not null;default:0" json:"department_id" comment:"部门ID"`
|
||||
DepartmentName string `gorm:"type:varchar(255);not null;default:''" json:"department_name" comment:"部门名称"`
|
||||
Department string `gorm:"type:varchar(50);not null;default:''" json:"department" comment:"部门"`
|
||||
Title string `gorm:"type:varchar(50)" json:"title" comment:"职称"`
|
||||
Hospital string `gorm:"type:varchar(100)" json:"hospital" comment:"医院"`
|
||||
Specialty string `gorm:"type:text" json:"specialty" comment:"专业"`
|
||||
ToutiaoCookie string `gorm:"type:text" json:"toutiao_cookie" comment:"头条Cookie"`
|
||||
ToutiaoImagesCookie string `gorm:"type:text" json:"toutiao_images_cookie" comment:"头条图片Cookie"`
|
||||
Introduction string `gorm:"type:text" json:"introduction" comment:"介绍"`
|
||||
AvatarURL string `gorm:"type:varchar(255)" json:"avatar_url" comment:"头像URL"`
|
||||
Status string `gorm:"type:enum('active','inactive');default:'active';index:idx_status" json:"status" comment:"状态"`
|
||||
Channel int `gorm:"type:tinyint(1);not null;default:1" json:"channel" comment:"渠道:1=小红书|2=douyin|3=toutiao|4=weixin"`
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// Feedback 用户反馈表
|
||||
type Feedback struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
FeedbackType string `gorm:"type:enum('功能建议','Bug反馈','体验问题','其他');not null;index:idx_feedback_type" json:"feedback_type" comment:"反馈类型"`
|
||||
Description string `gorm:"type:text;charset=utf8mb4;collate=utf8mb4_unicode_ci" json:"description" comment:"问题描述,最多500字"`
|
||||
ContactInfo string `gorm:"type:varchar(255)" json:"contact_info" comment:"联系方式(如邮箱),选填"`
|
||||
Nickname string `gorm:"type:varchar(255);not null;default:''" json:"nickname" comment:"填写用户昵称"`
|
||||
CreatedUserID int `gorm:"type:int unsigned;not null;index:idx_created_user_id" json:"created_user_id" comment:"创建该反馈的用户ID,关联用户表"`
|
||||
CreatedAt time.Time `gorm:"index:idx_created_at" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
Status string `gorm:"type:enum('待处理','处理中','已解决','已关闭');default:'待处理'" json:"status" comment:"反馈状态"`
|
||||
}
|
||||
|
||||
// TableName 指定表名(带ai_前缀)
|
||||
@@ -320,3 +341,7 @@ func (Log) TableName() string {
|
||||
func (Author) TableName() string {
|
||||
return "ai_authors"
|
||||
}
|
||||
|
||||
func (Feedback) TableName() string {
|
||||
return "ai_feedback"
|
||||
}
|
||||
|
||||
83
go_backend/monitor_service.bat
Normal file
83
go_backend/monitor_service.bat
Normal file
@@ -0,0 +1,83 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
|
||||
REM 服务监控脚本 - Windows版本
|
||||
REM 用于外部监控服务状态
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "SERVICE_NAME=AI小红书服务"
|
||||
set "ALERT_PHONE=15707023967"
|
||||
set "HEARTBEAT_FILE=%TEMP%\ai_xhs_service_heartbeat.json"
|
||||
set "CHECK_INTERVAL=120"
|
||||
|
||||
echo ========================================
|
||||
echo 服务监控检查 - %date% %time%
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM 检查心跳文件是否存在
|
||||
if not exist "%HEARTBEAT_FILE%" (
|
||||
echo [错误] 心跳文件不存在: %HEARTBEAT_FILE%
|
||||
echo [错误] 服务可能未启动或已宕机
|
||||
goto :SEND_ALERT
|
||||
)
|
||||
|
||||
echo [信息] 心跳文件: %HEARTBEAT_FILE%
|
||||
|
||||
REM 读取心跳文件内容
|
||||
for /f "delims=" %%i in ('powershell -Command "Get-Content '%HEARTBEAT_FILE%' | ConvertFrom-Json | Select-Object -ExpandProperty last_heartbeat"') do (
|
||||
set "LAST_HEARTBEAT=%%i"
|
||||
)
|
||||
|
||||
if "!LAST_HEARTBEAT!"=="" (
|
||||
echo [错误] 无法读取心跳信息
|
||||
goto :SEND_ALERT
|
||||
)
|
||||
|
||||
echo [信息] 上次心跳: !LAST_HEARTBEAT!
|
||||
|
||||
REM 计算时间差(使用PowerShell)
|
||||
for /f %%i in ('powershell -Command "$now=[DateTime]::Now; $last=[DateTime]::Parse('!LAST_HEARTBEAT!'); ($now - $last).TotalSeconds"') do (
|
||||
set "TIME_DIFF=%%i"
|
||||
)
|
||||
|
||||
REM 去除小数点
|
||||
for /f "tokens=1 delims=." %%a in ("!TIME_DIFF!") do set "TIME_DIFF_INT=%%a"
|
||||
|
||||
echo [信息] 距离上次心跳: !TIME_DIFF_INT! 秒
|
||||
|
||||
REM 检查是否超时
|
||||
if !TIME_DIFF_INT! GTR %CHECK_INTERVAL% (
|
||||
echo [错误] 服务可能已宕机(超过%CHECK_INTERVAL%秒未更新心跳)
|
||||
goto :SEND_ALERT
|
||||
)
|
||||
|
||||
echo [信息] 服务运行正常
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 检查完成 - 状态正常
|
||||
echo ========================================
|
||||
exit /b 0
|
||||
|
||||
:SEND_ALERT
|
||||
echo.
|
||||
echo [警告] 检测到服务异常,正在发送通知...
|
||||
echo.
|
||||
|
||||
REM 发送宕机通知
|
||||
cd /d %~dp0
|
||||
go run test_service_alert.go
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo [信息] 宕机通知发送成功
|
||||
) else (
|
||||
echo [错误] 宕机通知发送失败
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 检查完成 - 服务异常
|
||||
echo ========================================
|
||||
pause
|
||||
exit /b 1
|
||||
124
go_backend/monitor_service.sh
Normal file
124
go_backend/monitor_service.sh
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 服务监控脚本 - 用于外部监控服务状态
|
||||
# 可以配合cron定时任务使用
|
||||
|
||||
# 配置
|
||||
SERVICE_NAME="AI小红书服务"
|
||||
ALERT_PHONE="15707023967"
|
||||
HEARTBEAT_FILE="/tmp/ai_xhs_service_heartbeat.json"
|
||||
CHECK_INTERVAL=120 # 检查间隔(秒),心跳超过这个时间未更新则认为服务宕机
|
||||
MONITOR_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"
|
||||
}
|
||||
|
||||
# 检查心跳文件是否存在
|
||||
check_heartbeat_file() {
|
||||
if [ ! -f "$HEARTBEAT_FILE" ]; then
|
||||
log_error "心跳文件不存在: $HEARTBEAT_FILE"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# 获取最后心跳时间
|
||||
get_last_heartbeat() {
|
||||
if ! check_heartbeat_file; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# 从JSON文件中提取last_heartbeat时间
|
||||
last_heartbeat=$(grep -o '"last_heartbeat":"[^"]*"' "$HEARTBEAT_FILE" | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$last_heartbeat" ]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# 转换为Unix时间戳
|
||||
heartbeat_timestamp=$(date -d "$last_heartbeat" +%s 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "$heartbeat_timestamp"
|
||||
}
|
||||
|
||||
# 检查服务是否运行
|
||||
check_service_status() {
|
||||
log_info "开始检查服务状态..."
|
||||
|
||||
last_heartbeat_ts=$(get_last_heartbeat)
|
||||
|
||||
if [ "$last_heartbeat_ts" = "0" ]; then
|
||||
log_error "无法获取心跳信息"
|
||||
return 1
|
||||
fi
|
||||
|
||||
current_ts=$(date +%s)
|
||||
time_diff=$((current_ts - last_heartbeat_ts))
|
||||
|
||||
log_info "距离上次心跳: ${time_diff}秒"
|
||||
|
||||
if [ $time_diff -gt $CHECK_INTERVAL ]; then
|
||||
log_error "服务可能已宕机(超过${CHECK_INTERVAL}秒未更新心跳)"
|
||||
return 1
|
||||
else
|
||||
log_info "服务运行正常"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 发送宕机通知
|
||||
send_alert() {
|
||||
log_warn "尝试发送宕机通知..."
|
||||
|
||||
# 调用Go程序发送通知
|
||||
cd "$MONITOR_DIR"
|
||||
go run test_service_alert.go
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "宕机通知发送成功"
|
||||
return 0
|
||||
else
|
||||
log_error "宕机通知发送失败"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
echo "========================================"
|
||||
echo "服务监控检查 - $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "========================================"
|
||||
|
||||
if ! check_service_status; then
|
||||
log_error "检测到服务异常"
|
||||
send_alert
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "服务状态正常"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
main
|
||||
@@ -1,246 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#########################################
|
||||
# AI小红书 Go 后端服务重启脚本
|
||||
# 用途: Ubuntu/Linux 环境下重启 Go 服务
|
||||
# 支持: 开发环境(dev) 和 生产环境(prod)
|
||||
#########################################
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_ENV="prod"
|
||||
DEFAULT_PORT=8080
|
||||
PROD_PORT=8070
|
||||
LOG_FILE="ai_xhs.log"
|
||||
MAIN_FILE="main.go"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 帮助信息
|
||||
show_help() {
|
||||
echo -e "${BLUE}用法:${NC}"
|
||||
echo " ./restart.sh [环境]"
|
||||
echo ""
|
||||
echo -e "${BLUE}环境参数:${NC}"
|
||||
echo " dev - 开发环境 (默认, 端口 8080)"
|
||||
echo " prod - 生产环境 (端口 8070)"
|
||||
echo ""
|
||||
echo -e "${BLUE}示例:${NC}"
|
||||
echo " ./restart.sh # 启动开发环境"
|
||||
echo " ./restart.sh dev # 启动开发环境"
|
||||
echo " ./restart.sh prod # 启动生产环境"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 解析参数
|
||||
ENV="${1:-$DEFAULT_ENV}"
|
||||
|
||||
if [ "$ENV" = "help" ] || [ "$ENV" = "-h" ] || [ "$ENV" = "--help" ]; then
|
||||
show_help
|
||||
fi
|
||||
|
||||
# 确定端口
|
||||
if [ "$ENV" = "prod" ]; then
|
||||
PORT=$PROD_PORT
|
||||
else
|
||||
PORT=$DEFAULT_PORT
|
||||
ENV="dev"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} AI小红书 Go 后端服务重启脚本${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${YELLOW}环境: $ENV${NC}"
|
||||
echo -e "${YELLOW}端口: $PORT${NC}"
|
||||
echo -e "${YELLOW}日志: $LOG_FILE${NC}"
|
||||
echo ""
|
||||
|
||||
#########################################
|
||||
# 1. 停止现有服务
|
||||
#########################################
|
||||
echo -e "${BLUE}=== [1/4] 停止现有服务 ===${NC}"
|
||||
|
||||
# 方法1: 查找 go run main.go 进程
|
||||
GO_PIDS=$(ps aux | grep "go run $MAIN_FILE" | grep -v grep | awk '{print $2}')
|
||||
|
||||
if [ -n "$GO_PIDS" ]; then
|
||||
echo -e "${YELLOW}找到 Go 服务进程: $GO_PIDS${NC}"
|
||||
for PID in $GO_PIDS; do
|
||||
kill -9 $PID 2>/dev/null && echo " 已终止进程: $PID"
|
||||
done
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# 方法2: 查找编译后的可执行文件进程
|
||||
COMPILED_PIDS=$(ps aux | grep "/tmp/go-build.*$MAIN_FILE" | grep -v grep | awk '{print $2}')
|
||||
|
||||
if [ -n "$COMPILED_PIDS" ]; then
|
||||
echo -e "${YELLOW}找到编译后的进程: $COMPILED_PIDS${NC}"
|
||||
for PID in $COMPILED_PIDS; do
|
||||
kill -9 $PID 2>/dev/null && echo " 已终止进程: $PID"
|
||||
done
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# 方法3: 强制清理占用端口的进程
|
||||
echo -e "${YELLOW}清理端口 $PORT...${NC}"
|
||||
|
||||
# 使用 lsof 查找占用端口的进程
|
||||
PORT_PID=$(lsof -ti:$PORT 2>/dev/null)
|
||||
if [ -n "$PORT_PID" ]; then
|
||||
echo " 端口 $PORT 被进程 $PORT_PID 占用"
|
||||
kill -9 $PORT_PID 2>/dev/null && echo " 已终止进程: $PORT_PID"
|
||||
fi
|
||||
|
||||
# 使用 fuser 强制清理 (需要 sudo)
|
||||
if command -v fuser &> /dev/null; then
|
||||
sudo fuser -k $PORT/tcp 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 额外的清理方法
|
||||
sudo pkill -f ":$PORT" 2>/dev/null || true
|
||||
sudo pkill -f "main.go" 2>/dev/null || true
|
||||
|
||||
# 等待端口完全释放
|
||||
sleep 2
|
||||
|
||||
# 验证端口是否释放
|
||||
if lsof -ti:$PORT &> /dev/null; then
|
||||
echo -e "${RED}⚠️ 警告: 端口 $PORT 仍被占用${NC}"
|
||||
lsof -i:$PORT
|
||||
else
|
||||
echo -e "${GREEN}✅ 端口 $PORT 已释放${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
#########################################
|
||||
# 2. 环境检查
|
||||
#########################################
|
||||
echo -e "${BLUE}=== [2/4] 环境检查 ===${NC}"
|
||||
|
||||
# 检查 Go 环境
|
||||
if ! command -v go &> /dev/null; then
|
||||
echo -e "${RED}❌ 错误: 未检测到 Go 环境,请先安装 Go${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GO_VERSION=$(go version)
|
||||
echo -e "${GREEN}✅ Go 环境: $GO_VERSION${NC}"
|
||||
|
||||
# 检查 main.go 是否存在
|
||||
if [ ! -f "$MAIN_FILE" ]; then
|
||||
echo -e "${RED}❌ 错误: 未找到 $MAIN_FILE 文件${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ 主文件: $MAIN_FILE${NC}"
|
||||
|
||||
# 检查配置文件
|
||||
CONFIG_FILE="config/config.${ENV}.yaml"
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo -e "${RED}❌ 错误: 未找到配置文件 $CONFIG_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ 配置文件: $CONFIG_FILE${NC}"
|
||||
echo ""
|
||||
|
||||
#########################################
|
||||
# 3. 下载依赖
|
||||
#########################################
|
||||
echo -e "${BLUE}=== [3/4] 下载依赖 ===${NC}"
|
||||
|
||||
if [ -f "go.mod" ]; then
|
||||
go mod tidy
|
||||
echo -e "${GREEN}✅ 依赖下载完成${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ 未找到 go.mod 文件,跳过依赖下载${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
#########################################
|
||||
# 4. 启动服务
|
||||
#########################################
|
||||
echo -e "${BLUE}=== [4/4] 启动服务 ===${NC}"
|
||||
|
||||
# 设置环境变量
|
||||
export APP_ENV=$ENV
|
||||
|
||||
# 清空旧日志
|
||||
> $LOG_FILE
|
||||
|
||||
# 启动服务 (后台运行)
|
||||
echo -e "${YELLOW}启动命令: nohup go run $MAIN_FILE > $LOG_FILE 2>&1 &${NC}"
|
||||
nohup go run $MAIN_FILE > $LOG_FILE 2>&1 &
|
||||
|
||||
# 记录进程 PID
|
||||
NEW_PID=$!
|
||||
echo -e "${GREEN}✅ 服务已启动,进程 PID: $NEW_PID${NC}"
|
||||
|
||||
echo ""
|
||||
|
||||
#########################################
|
||||
# 5. 验证启动
|
||||
#########################################
|
||||
echo -e "${BLUE}=== 启动验证 ===${NC}"
|
||||
echo -e "${YELLOW}等待服务启动 (5秒)...${NC}"
|
||||
sleep 5
|
||||
|
||||
# 检查进程是否存在
|
||||
if ps -p $NEW_PID > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ 服务进程运行正常 (PID: $NEW_PID)${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ 服务进程未找到,可能启动失败${NC}"
|
||||
echo -e "${YELLOW}最近日志:${NC}"
|
||||
tail -n 20 $LOG_FILE
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查端口是否监听
|
||||
if lsof -ti:$PORT &> /dev/null; then
|
||||
echo -e "${GREEN}✅ 端口 $PORT 监听正常${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ 端口 $PORT 未监听,服务可能启动失败${NC}"
|
||||
echo -e "${YELLOW}最近日志:${NC}"
|
||||
tail -n 20 $LOG_FILE
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查日志中是否有错误
|
||||
if grep -i "fatal\|panic\|error" $LOG_FILE > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}⚠️ 日志中发现错误信息:${NC}"
|
||||
grep -i "fatal\|panic\|error" $LOG_FILE | head -n 5
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
#########################################
|
||||
# 6. 完成信息
|
||||
#########################################
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} 🎉 服务启动成功!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}服务信息:${NC}"
|
||||
echo -e " 环境: ${YELLOW}$ENV${NC}"
|
||||
echo -e " 端口: ${YELLOW}$PORT${NC}"
|
||||
echo -e " 进程PID: ${YELLOW}$NEW_PID${NC}"
|
||||
echo -e " 日志文件: ${YELLOW}$LOG_FILE${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}快捷命令:${NC}"
|
||||
echo -e " 查看日志: ${YELLOW}tail -f $LOG_FILE${NC}"
|
||||
echo -e " 查看进程: ${YELLOW}ps aux | grep 'go run'${NC}"
|
||||
echo -e " 停止服务: ${YELLOW}kill -9 $NEW_PID${NC}"
|
||||
echo -e " 检查端口: ${YELLOW}lsof -i:$PORT${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}访问地址:${NC}"
|
||||
echo -e " 本地: ${GREEN}http://localhost:$PORT${NC}"
|
||||
echo -e " API测试: ${GREEN}http://localhost:$PORT/api/health${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}提示: 使用 Ctrl+C 不会停止后台服务,请使用 kill 命令停止${NC}"
|
||||
@@ -3,6 +3,7 @@ package router
|
||||
import (
|
||||
"ai_xhs/controller"
|
||||
"ai_xhs/middleware"
|
||||
"ai_xhs/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -18,18 +19,25 @@ func SetupRouter(r *gin.Engine) {
|
||||
})
|
||||
})
|
||||
|
||||
// 静态文件服务(上传的图片)
|
||||
r.Static("/uploads", "./uploads")
|
||||
|
||||
// API路由组
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 公开接口(不需要认证)
|
||||
authCtrl := controller.NewAuthController()
|
||||
api.POST("/login/wechat", authCtrl.WechatLogin) // 微信登录
|
||||
api.POST("/login/phone", authCtrl.PhoneLogin) // 手机号登录(测试用)
|
||||
api.POST("/login/wechat", authCtrl.WechatLogin) // 微信登录
|
||||
api.POST("/login/phone", authCtrl.PhoneLogin) // 手机号登录(测试用)
|
||||
api.POST("/login/phone-password", authCtrl.PhonePasswordLogin) // 手机号密码登录
|
||||
api.POST("/login/xhs-phone-code", authCtrl.XHSPhoneCodeLogin) // 小红书手机号验证码登录
|
||||
api.POST("/xhs/send-verification-code", authCtrl.SendXHSVerificationCode) // 发送小红书验证码
|
||||
api.POST("/logout", middleware.AuthMiddleware(), authCtrl.Logout) // 退出登录(需要认证)
|
||||
|
||||
// 小红书相关公开接口
|
||||
// 小红书相关接口
|
||||
employeeCtrlPublic := controller.NewEmployeeController()
|
||||
api.POST("/xhs/send-code", employeeCtrlPublic.SendXHSCode) // 发送小红书验证码
|
||||
api.GET("/products", employeeCtrlPublic.GetProducts) // 获取产品列表(公开)
|
||||
api.POST("/xhs/send-code", employeeCtrlPublic.SendXHSCode) // 发送小红书验证码
|
||||
api.GET("/products", middleware.AuthMiddleware(), employeeCtrlPublic.GetProducts) // 获取产品列表
|
||||
|
||||
// 员工路由(需要认证)
|
||||
employee := api.Group("/employee")
|
||||
@@ -39,10 +47,17 @@ func SetupRouter(r *gin.Engine) {
|
||||
|
||||
// 10.1 获取员工个人信息
|
||||
employee.GET("/profile", employeeCtrl.GetProfile)
|
||||
// 10.1.1 更新个人信息
|
||||
employee.PUT("/profile", employeeCtrl.UpdateProfile)
|
||||
// 10.1.2 上传头像
|
||||
employee.POST("/upload-avatar", employeeCtrl.UploadAvatar)
|
||||
|
||||
// 10.2 绑定小红书账号
|
||||
employee.POST("/bind-xhs", employeeCtrl.BindXHS)
|
||||
|
||||
// 10.2.1 获取绑定状态
|
||||
employee.GET("/bind-xhs-status", employeeCtrl.GetBindXHSStatus)
|
||||
|
||||
// 10.3 解绑小红书账号
|
||||
employee.POST("/unbind-xhs", employeeCtrl.UnbindXHS)
|
||||
|
||||
@@ -69,6 +84,36 @@ func SetupRouter(r *gin.Engine) {
|
||||
|
||||
// 10.10 更新文案状态(通过/拒绝)
|
||||
employee.POST("/article/:id/status", employeeCtrl.UpdateArticleStatus)
|
||||
|
||||
// 10.10.1 更新文案内容(标题、正文)
|
||||
employee.PUT("/article/:id", employeeCtrl.UpdateArticleContent)
|
||||
|
||||
// 10.10.2 添加文案图片
|
||||
employee.POST("/article/:id/image", employeeCtrl.AddArticleImage)
|
||||
|
||||
// 10.10.3 删除文案图片
|
||||
employee.DELETE("/article/image/:imageId", employeeCtrl.DeleteArticleImage)
|
||||
|
||||
// 10.10.4 更新文案图片排序
|
||||
employee.PUT("/article/:id/images/order", employeeCtrl.UpdateArticleImagesOrder)
|
||||
|
||||
// 10.10.5 上传图片
|
||||
employee.POST("/upload/image", employeeCtrl.UploadImage)
|
||||
|
||||
// 10.11 编辑发布记录
|
||||
employee.PUT("/publish-record/:id", employeeCtrl.UpdatePublishRecord)
|
||||
|
||||
// 10.12 重新发布种草内容
|
||||
employee.POST("/publish-record/:id/republish", employeeCtrl.RepublishRecord)
|
||||
|
||||
// 10.13 禁用用户(撤销Token)
|
||||
employee.POST("/revoke-token", employeeCtrl.RevokeUserToken)
|
||||
|
||||
// 反馈相关接口
|
||||
feedbackCtrl := controller.NewFeedbackController(service.NewFeedbackService())
|
||||
employee.POST("/feedback", feedbackCtrl.CreateFeedback) // 创建反馈
|
||||
employee.GET("/feedback", feedbackCtrl.GetFeedbackList) // 获取反馈列表
|
||||
employee.GET("/feedback/:id", feedbackCtrl.GetFeedbackDetail) // 获取反馈详情
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"ai_xhs/models"
|
||||
"ai_xhs/utils"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -188,9 +189,9 @@ func (s *AuthService) WechatLogin(code string, phone string, phoneCode string) (
|
||||
return fmt.Errorf("保存微信绑定信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 检查是否已存在作者记录(通过手机号和企业ID)
|
||||
// 2. 检查是否已存在作者记录(通过 created_user_id 和企业ID)
|
||||
var existingAuthor models.Author
|
||||
result := tx.Where("phone = ? AND enterprise_id = ?", employee.Phone, employee.EnterpriseID).First(&existingAuthor)
|
||||
result := tx.Where("created_user_id = ? AND enterprise_id = ?", employee.ID, employee.EnterpriseID).First(&existingAuthor)
|
||||
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
// 作者记录不存在,创建新记录
|
||||
@@ -201,7 +202,7 @@ func (s *AuthService) WechatLogin(code string, phone string, phoneCode string) (
|
||||
AuthorName: employee.RealName,
|
||||
Department: employee.Department,
|
||||
Status: "active",
|
||||
Channel: 3, // 3=weixin (微信小程序)
|
||||
Channel: 1, // 1=小红书(默认渠道)
|
||||
}
|
||||
|
||||
// 如果真实姓名为空,使用用户名
|
||||
@@ -213,7 +214,7 @@ func (s *AuthService) WechatLogin(code string, phone string, phoneCode string) (
|
||||
return fmt.Errorf("创建作者记录失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[微信登录] 创建作者记录成功: ID=%d, Name=%s", author.ID, author.AuthorName)
|
||||
log.Printf("[微信登录] 创建作者记录成功: ID=%d, Name=%s, Channel=1(小红书)", author.ID, author.AuthorName)
|
||||
} else if result.Error != nil {
|
||||
// 其他数据库错误
|
||||
return fmt.Errorf("检查作者记录失败: %v", result.Error)
|
||||
@@ -237,6 +238,15 @@ func (s *AuthService) WechatLogin(code string, phone string, phoneCode string) (
|
||||
return "", nil, fmt.Errorf("生成token失败: %v", err)
|
||||
}
|
||||
|
||||
// 5. 将token存入Redis
|
||||
ctx := context.Background()
|
||||
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
|
||||
log.Printf("[微信登录] 存储token到Redis失败: %v", err)
|
||||
// 不阻断登录流程,但记录错误
|
||||
} else {
|
||||
log.Printf("[微信登录] 用户%d token已存入Redis", employee.ID)
|
||||
}
|
||||
|
||||
return token, &employee, nil
|
||||
}
|
||||
|
||||
@@ -256,6 +266,12 @@ func (s *AuthService) PhoneLogin(phone string) (string, *models.User, error) {
|
||||
return "", nil, fmt.Errorf("生成token失败: %v", err)
|
||||
}
|
||||
|
||||
// 将token存入Redis
|
||||
ctx := context.Background()
|
||||
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
|
||||
log.Printf("[手机号登录] 存储token到Redis失败: %v", err)
|
||||
}
|
||||
|
||||
return token, &employee, nil
|
||||
}
|
||||
|
||||
@@ -274,5 +290,154 @@ func (s *AuthService) loginByEmployeeID(employeeID int) (string, *models.User, e
|
||||
return "", nil, fmt.Errorf("生成token失败: %v", err)
|
||||
}
|
||||
|
||||
// 将token存入Redis
|
||||
ctx := context.Background()
|
||||
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
|
||||
log.Printf("[ID登录] 存储token到Redis失败: %v", err)
|
||||
}
|
||||
|
||||
return token, &employee, nil
|
||||
}
|
||||
|
||||
// PhonePasswordLogin 手机号密码登录
|
||||
func (s *AuthService) PhonePasswordLogin(phone string, password string) (string, *models.User, error) {
|
||||
if phone == "" || password == "" {
|
||||
return "", nil, errors.New("手机号和密码不能为空")
|
||||
}
|
||||
|
||||
var employee models.User
|
||||
|
||||
// 查找员工
|
||||
result := database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee)
|
||||
if result.Error != nil {
|
||||
return "", nil, errors.New("手机号或密码错误")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !utils.VerifyPassword(password, employee.Password) {
|
||||
return "", nil, errors.New("手机号或密码错误")
|
||||
}
|
||||
|
||||
// 生成token
|
||||
token, err := utils.GenerateToken(employee.ID)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("生成token失败: %v", err)
|
||||
}
|
||||
|
||||
// 将token存入Redis
|
||||
ctx := context.Background()
|
||||
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
|
||||
log.Printf("[密码登录] 存储token到Redis失败: %v", err)
|
||||
}
|
||||
|
||||
return token, &employee, nil
|
||||
}
|
||||
|
||||
// CheckPhoneExists 检查手机号是否存在于user表中
|
||||
func (s *AuthService) CheckPhoneExists(phone string) error {
|
||||
var count int64
|
||||
result := database.DB.Model(&models.User{}).Where("phone = ? AND status = ?", phone, "active").Count(&count)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("查询用户信息失败: %v", result.Error)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return errors.New("手机号未注册,请联系管理员添加")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// XHSPhoneCodeLogin 小红书手机号验证码登录
|
||||
func (s *AuthService) XHSPhoneCodeLogin(phone string, code string) (string, *models.User, error) {
|
||||
if phone == "" || code == "" {
|
||||
return "", nil, errors.New("手机号和验证码不能为空")
|
||||
}
|
||||
|
||||
// 调用短信服务验证验证码
|
||||
smsService := GetSmsService()
|
||||
if err := smsService.VerifyCode(phone, code); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
var employee models.User
|
||||
|
||||
// 查找员工
|
||||
result := database.DB.Where("phone = ? AND status = ?", phone, "active").First(&employee)
|
||||
if result.Error != nil {
|
||||
// 用户不存在,不允许登录
|
||||
return "", nil, errors.New("手机号未注册,请联系管理员添加")
|
||||
}
|
||||
|
||||
// 生成token
|
||||
token, err := utils.GenerateToken(employee.ID)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("生成token失败: %v", err)
|
||||
}
|
||||
|
||||
// 将token存入Redis
|
||||
ctx := context.Background()
|
||||
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
|
||||
log.Printf("[验证码登录] 存储token到Redis失败: %v", err)
|
||||
}
|
||||
|
||||
return token, &employee, nil
|
||||
}
|
||||
|
||||
// createNewUserFromPhone 从手机号创建新用户
|
||||
func (s *AuthService) createNewUserFromPhone(phone string) (string, *models.User, error) {
|
||||
// 使用事务创建用户和作者记录
|
||||
var employee models.User
|
||||
|
||||
err := database.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 创建用户记录
|
||||
employee = models.User{
|
||||
Phone: phone,
|
||||
Username: phone, // 默认用户名为手机号
|
||||
Role: "user",
|
||||
Status: "active",
|
||||
EnterpriseID: 1, // 默认企业ID,可根据实际调整
|
||||
EnterpriseName: "默认企业", // 默认企业名称
|
||||
}
|
||||
|
||||
if err := tx.Create(&employee).Error; err != nil {
|
||||
return fmt.Errorf("创建用户失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 创建作者记录
|
||||
author := models.Author{
|
||||
EnterpriseID: employee.EnterpriseID,
|
||||
CreatedUserID: employee.ID,
|
||||
Phone: employee.Phone,
|
||||
AuthorName: employee.Username,
|
||||
Department: "",
|
||||
Status: "active",
|
||||
Channel: 1, // 1=小红书
|
||||
}
|
||||
|
||||
if err := tx.Create(&author).Error; err != nil {
|
||||
return fmt.Errorf("创建作者记录失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[手机号登录] 创建新用户成功: Phone=%s, UserID=%d, AuthorID=%d", phone, employee.ID, author.ID)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// 生成token
|
||||
token, err := utils.GenerateToken(employee.ID)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("生成token失败: %v", err)
|
||||
}
|
||||
|
||||
// 将token存入Redis
|
||||
ctx := context.Background()
|
||||
if err := utils.StoreTokenInRedis(ctx, employee.ID, token); err != nil {
|
||||
log.Printf("[新用户登录] 存储token到Redis失败: %v", err)
|
||||
}
|
||||
|
||||
return token, &employee, nil
|
||||
}
|
||||
|
||||
169
go_backend/service/cache_service.go
Normal file
169
go_backend/service/cache_service.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ai_xhs/database"
|
||||
"ai_xhs/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CacheService 缓存管理服务 - 统一管理缓存键和清除策略
|
||||
type CacheService struct{}
|
||||
|
||||
func NewCacheService() *CacheService {
|
||||
return &CacheService{}
|
||||
}
|
||||
|
||||
// 缓存键前缀常量
|
||||
const (
|
||||
CacheKeyPrefixUser = "user:profile:"
|
||||
CacheKeyPrefixAuthor = "author:user:"
|
||||
CacheKeyPrefixXHSStatus = "xhs:status:"
|
||||
CacheKeyPrefixProducts = "products:enterprise:"
|
||||
CacheKeyPrefixRateLimit = "rate:sms:"
|
||||
CacheKeyPrefixLock = "lock:"
|
||||
)
|
||||
|
||||
// GetUserCacheKey 获取用户缓存键
|
||||
func (s *CacheService) GetUserCacheKey(userID int) string {
|
||||
return fmt.Sprintf("%s%d", CacheKeyPrefixUser, userID)
|
||||
}
|
||||
|
||||
// GetAuthorCacheKey 获取作者缓存键
|
||||
func (s *CacheService) GetAuthorCacheKey(userID int) string {
|
||||
return fmt.Sprintf("%s%d", CacheKeyPrefixAuthor, userID)
|
||||
}
|
||||
|
||||
// GetXHSStatusCacheKey 获取小红书状态缓存键
|
||||
func (s *CacheService) GetXHSStatusCacheKey(userID int) string {
|
||||
return fmt.Sprintf("%s%d", CacheKeyPrefixXHSStatus, userID)
|
||||
}
|
||||
|
||||
// GetProductsCacheKey 获取产品列表缓存键
|
||||
func (s *CacheService) GetProductsCacheKey(enterpriseID, page, pageSize int) string {
|
||||
return fmt.Sprintf("%spage:%d:size:%d", CacheKeyPrefixProducts+fmt.Sprintf("%d:", enterpriseID), page, pageSize)
|
||||
}
|
||||
|
||||
// GetRateLimitKey 获取限流键
|
||||
func (s *CacheService) GetRateLimitKey(phone string) string {
|
||||
return fmt.Sprintf("%s%s", CacheKeyPrefixRateLimit, phone)
|
||||
}
|
||||
|
||||
// GetLockKey 获取分布式锁键
|
||||
func (s *CacheService) GetLockKey(resource string) string {
|
||||
return fmt.Sprintf("%s%s", CacheKeyPrefixLock, resource)
|
||||
}
|
||||
|
||||
// ClearUserRelatedCache 清除用户相关的所有缓存
|
||||
func (s *CacheService) ClearUserRelatedCache(ctx context.Context, userID int) error {
|
||||
keys := []string{
|
||||
s.GetUserCacheKey(userID),
|
||||
s.GetAuthorCacheKey(userID),
|
||||
s.GetXHSStatusCacheKey(userID),
|
||||
}
|
||||
|
||||
if err := utils.DelCache(ctx, keys...); err != nil {
|
||||
log.Printf("清除用户缓存失败 (userID=%d): %v", userID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("已清除用户相关缓存: userID=%d", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearProductsCache 清除企业的产品列表缓存
|
||||
func (s *CacheService) ClearProductsCache(ctx context.Context, enterpriseID int) error {
|
||||
// 使用模糊匹配删除所有分页缓存
|
||||
pattern := fmt.Sprintf("%s%d:*", CacheKeyPrefixProducts, enterpriseID)
|
||||
|
||||
// 注意: 这需要扫描所有键,生产环境建议记录所有已创建的缓存键
|
||||
// 这里简化处理,实际应该维护一个产品缓存键集合
|
||||
log.Printf("需要清除产品缓存: enterpriseID=%d, pattern=%s", enterpriseID, pattern)
|
||||
log.Printf("建议: 在产品更新时调用此方法")
|
||||
|
||||
// 简化版: 只清除前几页的缓存
|
||||
for page := 1; page <= 10; page++ {
|
||||
for _, pageSize := range []int{10, 20, 50} {
|
||||
key := s.GetProductsCacheKey(enterpriseID, page, pageSize)
|
||||
utils.DelCache(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AcquireLock 获取分布式锁
|
||||
func (s *CacheService) AcquireLock(ctx context.Context, resource string, ttl time.Duration) (bool, error) {
|
||||
lockKey := s.GetLockKey(resource)
|
||||
return utils.SetCacheNX(ctx, lockKey, "locked", ttl)
|
||||
}
|
||||
|
||||
// ReleaseLock 释放分布式锁
|
||||
func (s *CacheService) ReleaseLock(ctx context.Context, resource string) error {
|
||||
lockKey := s.GetLockKey(resource)
|
||||
return utils.DelCache(ctx, lockKey)
|
||||
}
|
||||
|
||||
// WithLock 使用分布式锁执行函数
|
||||
func (s *CacheService) WithLock(ctx context.Context, resource string, ttl time.Duration, fn func() error) error {
|
||||
// 尝试获取锁
|
||||
log.Printf("[分布式锁] 尝试获取锁: %s (TTL: %v)", resource, ttl)
|
||||
acquired, err := s.AcquireLock(ctx, resource, ttl)
|
||||
if err != nil {
|
||||
log.Printf("[分布式锁] 获取锁失败: %s, 错误: %v", resource, err)
|
||||
return fmt.Errorf("获取锁失败: %w", err)
|
||||
}
|
||||
|
||||
if !acquired {
|
||||
log.Printf("[分布式锁] 锁已被占用: %s", resource)
|
||||
// 检查锁的剩余时间
|
||||
lockKey := s.GetLockKey(resource)
|
||||
ttl, _ := database.RDB.TTL(ctx, lockKey).Result()
|
||||
return fmt.Errorf("资源被锁定,请稍后重试(剩余时间: %v)", ttl)
|
||||
}
|
||||
|
||||
log.Printf("[分布式锁] 成功获取锁: %s", resource)
|
||||
|
||||
// 确保释放锁
|
||||
defer func() {
|
||||
if err := s.ReleaseLock(ctx, resource); err != nil {
|
||||
log.Printf("[分布式锁] 释放锁失败 (resource=%s): %v", resource, err)
|
||||
} else {
|
||||
log.Printf("[分布式锁] 成功释放锁: %s", resource)
|
||||
}
|
||||
}()
|
||||
|
||||
// 执行函数
|
||||
log.Printf("[分布式锁] 开始执行受保护的函数: %s", resource)
|
||||
return fn()
|
||||
}
|
||||
|
||||
// SetCacheWithNullProtection 设置缓存(带空值保护,防止缓存穿透)
|
||||
func (s *CacheService) SetCacheWithNullProtection(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
|
||||
if value == nil {
|
||||
// 缓存空值,但使用较短的过期时间(1分钟)
|
||||
return utils.SetCache(ctx, key, "NULL", 1*time.Minute)
|
||||
}
|
||||
return utils.SetCache(ctx, key, value, ttl)
|
||||
}
|
||||
|
||||
// GetCacheWithNullCheck 获取缓存(检查空值标记)
|
||||
func (s *CacheService) GetCacheWithNullCheck(ctx context.Context, key string, dest interface{}) (bool, error) {
|
||||
var tempValue interface{}
|
||||
err := utils.GetCache(ctx, key, &tempValue)
|
||||
|
||||
if err != nil {
|
||||
// 缓存不存在
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 检查是否是空值标记
|
||||
if strValue, ok := tempValue.(string); ok && strValue == "NULL" {
|
||||
return true, fmt.Errorf("cached null value")
|
||||
}
|
||||
|
||||
// 正常获取缓存
|
||||
return true, utils.GetCache(ctx, key, dest)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
62
go_backend/service/feedback_service.go
Normal file
62
go_backend/service/feedback_service.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ai_xhs/database"
|
||||
"ai_xhs/models"
|
||||
)
|
||||
|
||||
// FeedbackService 反馈服务
|
||||
type FeedbackService struct{}
|
||||
|
||||
// NewFeedbackService 创建反馈服务
|
||||
func NewFeedbackService() *FeedbackService {
|
||||
return &FeedbackService{}
|
||||
}
|
||||
|
||||
// CreateFeedback 创建反馈
|
||||
func (fs *FeedbackService) CreateFeedback(feedback *models.Feedback) error {
|
||||
return database.DB.Create(feedback).Error
|
||||
}
|
||||
|
||||
// GetFeedbackList 获取反馈列表
|
||||
func (fs *FeedbackService) GetFeedbackList(userID, page, pageSize int, feedbackType, status string) ([]models.Feedback, int64, error) {
|
||||
var feedbacks []models.Feedback
|
||||
var total int64
|
||||
|
||||
query := database.DB.Model(&models.Feedback{}).Where("created_user_id = ?", userID)
|
||||
|
||||
// 筛选条件
|
||||
if feedbackType != "" {
|
||||
query = query.Where("feedback_type = ?", feedbackType)
|
||||
}
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&feedbacks).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return feedbacks, total, nil
|
||||
}
|
||||
|
||||
// GetFeedbackByID 根据ID获取反馈
|
||||
func (fs *FeedbackService) GetFeedbackByID(id int) (*models.Feedback, error) {
|
||||
var feedback models.Feedback
|
||||
if err := database.DB.First(&feedback, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feedback, nil
|
||||
}
|
||||
|
||||
// UpdateFeedbackStatus 更新反馈状态(管理员使用)
|
||||
func (fs *FeedbackService) UpdateFeedbackStatus(id int, status string) error {
|
||||
return database.DB.Model(&models.Feedback{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
@@ -13,8 +13,6 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -332,7 +330,7 @@ func (s *SchedulerService) AutoPublishArticles() {
|
||||
len(articles), successCount, failCount, duration)
|
||||
}
|
||||
|
||||
// publishArticle 发布单篇文案
|
||||
// publishArticle 发布单篇文案(使用FastAPI服务)
|
||||
func (s *SchedulerService) publishArticle(article models.Article) error {
|
||||
// 1. 获取用户信息(发布用户)
|
||||
var user models.User
|
||||
@@ -347,9 +345,22 @@ func (s *SchedulerService) publishArticle(article models.Article) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查用户是否绑定了小红书
|
||||
if user.IsBoundXHS != 1 || user.XHSCookie == "" {
|
||||
return errors.New("用户未绑定小红书账号或Cookie已失效")
|
||||
// 2. 检查用户是否绑定了小红书并获取author记录
|
||||
if user.IsBoundXHS != 1 {
|
||||
return errors.New("用户未绑定小红书账号")
|
||||
}
|
||||
|
||||
// 查询对应的 author 记录获取Cookie
|
||||
var author models.Author
|
||||
if err := database.DB.Where(
|
||||
"phone = ? AND enterprise_id = ? AND channel = 1 AND status = 'active'",
|
||||
user.Phone, user.EnterpriseID,
|
||||
).First(&author).Error; err != nil {
|
||||
return fmt.Errorf("未找到有效的小红书作者记录: %w", err)
|
||||
}
|
||||
|
||||
if author.XHSCookie == "" {
|
||||
return errors.New("小红书Cookie已失效")
|
||||
}
|
||||
|
||||
// 3. 获取文章图片
|
||||
@@ -378,142 +389,130 @@ func (s *SchedulerService) publishArticle(article models.Article) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 解析Cookie(数据库中存储的是JSON字符串)
|
||||
var cookies interface{}
|
||||
if err := json.Unmarshal([]byte(user.XHSCookie), &cookies); err != nil {
|
||||
return fmt.Errorf("解析Cookie失败: %w,Cookie内容: %s", err, user.XHSCookie)
|
||||
}
|
||||
// 6. 准备发布数据:优先使用storage_state文件,其次使用login_state
|
||||
var cookiesData interface{}
|
||||
var loginStateData map[string]interface{}
|
||||
var useStorageStateMode bool
|
||||
|
||||
// 7. 构造发布配置
|
||||
publishConfig := map[string]interface{}{
|
||||
"cookies": cookies, // 解析后的Cookie对象或数组
|
||||
"title": article.Title,
|
||||
"content": article.Content,
|
||||
"images": imageURLs,
|
||||
"tags": tags,
|
||||
}
|
||||
// 检查storage_state文件是否存在(根据手机号查找)
|
||||
storageStateFile := fmt.Sprintf("../backend/storage_states/xhs_%s.json", author.XHSPhone)
|
||||
if _, err := os.Stat(storageStateFile); err == nil {
|
||||
log.Printf("[调度器] 检测到storage_state文件: %s", storageStateFile)
|
||||
useStorageStateMode = true
|
||||
} else {
|
||||
log.Printf("[调度器] storage_state文件不存在,使用login_state或cookies模式")
|
||||
useStorageStateMode = false
|
||||
|
||||
// 决定本次发布使用的代理
|
||||
proxyToUse := config.AppConfig.Scheduler.Proxy
|
||||
if proxyToUse == "" && config.AppConfig.Scheduler.ProxyFetchURL != "" {
|
||||
if dynamicProxy, err := fetchProxyFromPool(); err != nil {
|
||||
log.Printf("[代理池] 获取代理失败: %v", err)
|
||||
} else if dynamicProxy != "" {
|
||||
proxyToUse = dynamicProxy
|
||||
log.Printf("[代理池] 使用动态代理: %s", proxyToUse)
|
||||
}
|
||||
}
|
||||
|
||||
// 注入代理和User-Agent(如果有配置)
|
||||
if proxyToUse != "" {
|
||||
publishConfig["proxy"] = proxyToUse
|
||||
}
|
||||
if ua := config.AppConfig.Scheduler.UserAgent; ua != "" {
|
||||
publishConfig["user_agent"] = ua
|
||||
}
|
||||
|
||||
// 8. 保存临时配置文件
|
||||
tempDir := filepath.Join("..", "backend", "temp")
|
||||
os.MkdirAll(tempDir, 0755)
|
||||
|
||||
configFile := filepath.Join(tempDir, fmt.Sprintf("publish_%d_%d.json", article.ID, time.Now().Unix()))
|
||||
configData, err := json.MarshalIndent(publishConfig, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configFile, configData, 0644); err != nil {
|
||||
return fmt.Errorf("保存配置文件失败: %w", err)
|
||||
}
|
||||
defer os.Remove(configFile) // 发布完成后删除临时文件
|
||||
|
||||
// 9. 调用Python发布脚本
|
||||
backendDir := filepath.Join("..", "backend")
|
||||
pythonScript := filepath.Join(backendDir, "xhs_publish.py")
|
||||
pythonCmd := getPythonPath(backendDir)
|
||||
|
||||
cmd := exec.Command(pythonCmd, pythonScript, "--config", configFile)
|
||||
cmd.Dir = backendDir
|
||||
|
||||
// 设置超时
|
||||
if s.publishTimeout > 0 {
|
||||
timer := time.AfterFunc(time.Duration(s.publishTimeout)*time.Second, func() {
|
||||
cmd.Process.Kill()
|
||||
})
|
||||
defer timer.Stop()
|
||||
}
|
||||
|
||||
// 捕获输出
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// 执行命令
|
||||
err = cmd.Run()
|
||||
|
||||
// 打印Python脚本日志
|
||||
if stderr.Len() > 0 {
|
||||
log.Printf("[Python日志-发布文案%d] %s", article.ID, stderr.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// 更新文章状态为failed
|
||||
s.updateArticleStatus(article.ID, "failed", fmt.Sprintf("发布失败: %v", err))
|
||||
return fmt.Errorf("执行Python脚本失败: %w, stderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
// 10. 解析发布结果
|
||||
// 注意:Python脚本可能输出日志到stdout,需要提取最后一行JSON
|
||||
outputStr := stdout.String()
|
||||
|
||||
// 查找最后一个完整的JSON对象
|
||||
var result map[string]interface{}
|
||||
found := false
|
||||
|
||||
// 尝试从最后一行开始解析JSON
|
||||
lines := strings.Split(strings.TrimSpace(outputStr), "\n")
|
||||
|
||||
// 从后往前找第一个有效的JSON
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 尝试解析为JSON(必须以{开头)
|
||||
if strings.HasPrefix(line, "{") {
|
||||
if err := json.Unmarshal([]byte(line), &result); err == nil {
|
||||
found = true
|
||||
log.Printf("成功解析JSON结果(第%d行): %s", i+1, line)
|
||||
break
|
||||
// 尝试解析为JSON对象
|
||||
if err := json.Unmarshal([]byte(author.XHSCookie), &loginStateData); err == nil {
|
||||
// 检查是否是login_state格式(包含cookies字段)
|
||||
if _, ok := loginStateData["cookies"]; ok {
|
||||
log.Printf("[调度器] 检测到login_state格式,将使用完整登录状态")
|
||||
cookiesData = loginStateData // 使用完整的login_state
|
||||
} else {
|
||||
// 可能是cookies数组
|
||||
log.Printf("[调度器] 检测到纯cookies格式")
|
||||
cookiesData = loginStateData
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("解析Cookie失败: %w,Cookie内容: %s", err, author.XHSCookie[:100])
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
errMsg := "Python脚本未返回有效JSON结果"
|
||||
s.updateArticleStatus(article.ID, "failed", errMsg)
|
||||
log.Printf("完整输出内容:\n%s", outputStr)
|
||||
if stderr.Len() > 0 {
|
||||
log.Printf("错误输出:\n%s", stderr.String())
|
||||
// 7. 调用FastAPI服务(使用浏览器池+预热)
|
||||
fastAPIURL := config.AppConfig.XHS.PythonServiceURL
|
||||
if fastAPIURL == "" {
|
||||
fastAPIURL = "http://localhost:8000" // 默认地址
|
||||
}
|
||||
publishEndpoint := fastAPIURL + "/api/xhs/publish-with-cookies"
|
||||
|
||||
// 构造请求体
|
||||
// 优先级:storage_state文件 > login_state > cookies
|
||||
var fullRequest map[string]interface{}
|
||||
if useStorageStateMode {
|
||||
// 模式1:使用storage_state文件(通过手机号查找)
|
||||
fullRequest = map[string]interface{}{
|
||||
"phone": author.XHSPhone, // 传递手机号,Python后端会根据手机号查找文件
|
||||
"title": article.Title,
|
||||
"content": article.Content,
|
||||
"images": imageURLs,
|
||||
"topics": tags,
|
||||
}
|
||||
log.Printf("[调度器] 使用storage_state模式发布,手机号: %s", author.XHSPhone)
|
||||
} else if loginState, ok := cookiesData.(map[string]interface{}); ok {
|
||||
if _, hasLoginStateStructure := loginState["cookies"]; hasLoginStateStructure {
|
||||
// 模式2:完整的login_state格式
|
||||
fullRequest = map[string]interface{}{
|
||||
"login_state": loginState,
|
||||
"title": article.Title,
|
||||
"content": article.Content,
|
||||
"images": imageURLs,
|
||||
"topics": tags,
|
||||
}
|
||||
log.Printf("[调度器] 使用login_state模式发布")
|
||||
} else {
|
||||
// 模式3:纺cookies格式
|
||||
fullRequest = map[string]interface{}{
|
||||
"cookies": loginState,
|
||||
"title": article.Title,
|
||||
"content": article.Content,
|
||||
"images": imageURLs,
|
||||
"topics": tags,
|
||||
}
|
||||
log.Printf("[调度器] 使用cookies模式发布")
|
||||
}
|
||||
} else {
|
||||
// 兜底:直接发送
|
||||
fullRequest = map[string]interface{}{
|
||||
"cookies": cookiesData,
|
||||
"title": article.Title,
|
||||
"content": article.Content,
|
||||
"images": imageURLs,
|
||||
"topics": tags,
|
||||
}
|
||||
return fmt.Errorf("%s, output: %s", errMsg, outputStr)
|
||||
}
|
||||
|
||||
// 11. 检查发布是否成功
|
||||
success, ok := result["success"].(bool)
|
||||
if !ok || !success {
|
||||
requestBody, err := json.Marshal(fullRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("构造请求数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送HTTP请求
|
||||
timeout := time.Duration(s.publishTimeout) * time.Second
|
||||
if s.publishTimeout <= 0 {
|
||||
timeout = 120 * time.Second // 默认120秒超时
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := client.Post(publishEndpoint, "application/json", bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
s.updateArticleStatus(article.ID, "failed", fmt.Sprintf("调用FastAPI服务失败: %v", err))
|
||||
return fmt.Errorf("调用FastAPI服务失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 9. 解析响应
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
s.updateArticleStatus(article.ID, "failed", fmt.Sprintf("解析FastAPI响应失败: %v", err))
|
||||
return fmt.Errorf("解析FastAPI响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 10. 检查发布是否成功
|
||||
code, ok := result["code"].(float64)
|
||||
if !ok || code != 0 {
|
||||
errMsg := "未知错误"
|
||||
if errStr, ok := result["error"].(string); ok {
|
||||
errMsg = errStr
|
||||
if msg, ok := result["message"].(string); ok {
|
||||
errMsg = msg
|
||||
}
|
||||
s.updateArticleStatus(article.ID, "failed", errMsg)
|
||||
return fmt.Errorf("发布失败: %s", errMsg)
|
||||
}
|
||||
|
||||
// 12. 更新文章状态为published
|
||||
// 11. 更新文章状态为published
|
||||
s.updateArticleStatus(article.ID, "published", "发布成功")
|
||||
|
||||
log.Printf("[使用FastAPI] 文章 %d 发布成功,享受浏览器池+预热加速", article.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
264
go_backend/service/sms_service.go
Normal file
264
go_backend/service/sms_service.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"ai_xhs/config"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v4/client"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
)
|
||||
|
||||
// SmsService 短信服务
|
||||
type SmsService struct {
|
||||
client *dysmsapi20170525.Client
|
||||
signName string
|
||||
templateCode string
|
||||
codeCache map[string]*VerificationCode
|
||||
cacheMutex sync.RWMutex
|
||||
alertPhone string // 宕机通知手机号
|
||||
}
|
||||
|
||||
// VerificationCode 验证码缓存
|
||||
type VerificationCode struct {
|
||||
Code string
|
||||
ExpireTime time.Time
|
||||
SentAt time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
smsServiceInstance *SmsService
|
||||
smsServiceOnce sync.Once
|
||||
)
|
||||
|
||||
// GetSmsService 获取短信服务单例
|
||||
func GetSmsService() *SmsService {
|
||||
smsServiceOnce.Do(func() {
|
||||
smsServiceInstance = NewSmsService()
|
||||
})
|
||||
return smsServiceInstance
|
||||
}
|
||||
|
||||
// NewSmsService 创建短信服务
|
||||
func NewSmsService() *SmsService {
|
||||
// 从配置读取阿里云短信配置
|
||||
accessKeyId := config.AppConfig.AliSms.AccessKeyID
|
||||
accessKeySecret := config.AppConfig.AliSms.AccessKeySecret
|
||||
signName := config.AppConfig.AliSms.SignName
|
||||
templateCode := config.AppConfig.AliSms.TemplateCode
|
||||
|
||||
if accessKeyId == "" || accessKeySecret == "" {
|
||||
log.Printf("[短信服务] 警告: 阿里云短信配置未设置,短信功能将不可用")
|
||||
return &SmsService{
|
||||
signName: signName,
|
||||
templateCode: templateCode,
|
||||
codeCache: make(map[string]*VerificationCode),
|
||||
}
|
||||
}
|
||||
|
||||
// 创建阿里云短信客户端
|
||||
apiConfig := &openapi.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
apiConfig.Endpoint = tea.String("dysmsapi.aliyuncs.com")
|
||||
|
||||
client, err := dysmsapi20170525.NewClient(apiConfig)
|
||||
if err != nil {
|
||||
log.Printf("[短信服务] 创建阿里云客户端失败: %v", err)
|
||||
return &SmsService{
|
||||
signName: signName,
|
||||
templateCode: templateCode,
|
||||
codeCache: make(map[string]*VerificationCode),
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[短信服务] 阿里云短信服务初始化成功")
|
||||
|
||||
return &SmsService{
|
||||
client: client,
|
||||
signName: signName,
|
||||
templateCode: templateCode,
|
||||
codeCache: make(map[string]*VerificationCode),
|
||||
}
|
||||
}
|
||||
|
||||
// generateCode 生成随机6位数字验证码
|
||||
func (s *SmsService) generateCode() string {
|
||||
code := ""
|
||||
for i := 0; i < 6; i++ {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(10))
|
||||
code += fmt.Sprintf("%d", n.Int64())
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// SendVerificationCode 发送验证码
|
||||
func (s *SmsService) SendVerificationCode(phone string) (string, error) {
|
||||
if s.client == nil {
|
||||
return "", errors.New("短信服务未配置")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
code := s.generateCode()
|
||||
|
||||
log.Printf("[短信服务] 正在发送验证码到 %s,验证码: %s", phone, code)
|
||||
|
||||
// 构建短信请求
|
||||
sendSmsRequest := &dysmsapi20170525.SendSmsRequest{
|
||||
PhoneNumbers: tea.String(phone),
|
||||
SignName: tea.String(s.signName),
|
||||
TemplateCode: tea.String(s.templateCode),
|
||||
TemplateParam: tea.String(fmt.Sprintf(`{"code":"%s"}`, code)),
|
||||
}
|
||||
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
// 发送短信
|
||||
resp, err := s.client.SendSmsWithOptions(sendSmsRequest, runtime)
|
||||
if err != nil {
|
||||
log.Printf("[短信服务] 发送短信失败: %v", err)
|
||||
return "", fmt.Errorf("发送短信失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查返回结果
|
||||
if resp.Body.Code == nil || *resp.Body.Code != "OK" {
|
||||
errMsg := "未知错误"
|
||||
if resp.Body.Message != nil {
|
||||
errMsg = *resp.Body.Message
|
||||
}
|
||||
log.Printf("[短信服务] 短信发送失败: %s", errMsg)
|
||||
return "", fmt.Errorf("短信发送失败: %s", errMsg)
|
||||
}
|
||||
|
||||
// 缓存验证码
|
||||
s.cacheMutex.Lock()
|
||||
s.codeCache[phone] = &VerificationCode{
|
||||
Code: code,
|
||||
ExpireTime: time.Now().Add(5 * time.Minute), // 5分钟过期
|
||||
SentAt: time.Now(),
|
||||
}
|
||||
s.cacheMutex.Unlock()
|
||||
|
||||
log.Printf("[短信服务] 验证码发送成功,手机号: %s", phone)
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// VerifyCode 验证验证码
|
||||
func (s *SmsService) VerifyCode(phone, code string) error {
|
||||
s.cacheMutex.RLock()
|
||||
cached, exists := s.codeCache[phone]
|
||||
s.cacheMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return errors.New("验证码未发送或已过期,请重新获取")
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if time.Now().After(cached.ExpireTime) {
|
||||
s.cacheMutex.Lock()
|
||||
delete(s.codeCache, phone)
|
||||
s.cacheMutex.Unlock()
|
||||
return errors.New("验证码已过期,请重新获取")
|
||||
}
|
||||
|
||||
// 验证码匹配
|
||||
if code != cached.Code {
|
||||
return errors.New("验证码错误,请重新输入")
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码(一次性使用)
|
||||
s.cacheMutex.Lock()
|
||||
delete(s.codeCache, phone)
|
||||
s.cacheMutex.Unlock()
|
||||
|
||||
log.Printf("[短信服务] 验证码验证成功,手机号: %s", phone)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupExpiredCodes 清理过期的验证码(定时任务调用)
|
||||
func (s *SmsService) CleanupExpiredCodes() {
|
||||
s.cacheMutex.Lock()
|
||||
defer s.cacheMutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
expiredPhones := []string{}
|
||||
|
||||
for phone, cached := range s.codeCache {
|
||||
if now.After(cached.ExpireTime) {
|
||||
expiredPhones = append(expiredPhones, phone)
|
||||
}
|
||||
}
|
||||
|
||||
for _, phone := range expiredPhones {
|
||||
delete(s.codeCache, phone)
|
||||
}
|
||||
|
||||
if len(expiredPhones) > 0 {
|
||||
log.Printf("[短信服务] 已清理 %d 个过期验证码", len(expiredPhones))
|
||||
}
|
||||
}
|
||||
|
||||
// StartCleanupTask 启动清理过期验证码的定时任务
|
||||
func (s *SmsService) StartCleanupTask() {
|
||||
ticker := time.NewTicker(1 * time.Minute) // 每分钟清理一次
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
s.CleanupExpiredCodes()
|
||||
}
|
||||
}()
|
||||
log.Printf("[短信服务] 验证码清理任务已启动")
|
||||
}
|
||||
|
||||
// SendServiceDownAlert 发送服务宕机通知短信
|
||||
// 向指定手机号发送验证码为11111的通知短信
|
||||
func (s *SmsService) SendServiceDownAlert(phone string, serviceName string) error {
|
||||
if s.client == nil {
|
||||
return errors.New("短信服务未配置")
|
||||
}
|
||||
|
||||
// 固定验证码为11111作为宕机通知标识
|
||||
alertCode := "11111"
|
||||
|
||||
log.Printf("[短信服务] 发送服务宕机通知到 %s,服务: %s", phone, serviceName)
|
||||
|
||||
// 构建短信请求
|
||||
sendSmsRequest := &dysmsapi20170525.SendSmsRequest{
|
||||
PhoneNumbers: tea.String(phone),
|
||||
SignName: tea.String(s.signName),
|
||||
TemplateCode: tea.String(s.templateCode),
|
||||
TemplateParam: tea.String(fmt.Sprintf(`{"code":"%s"}`, alertCode)),
|
||||
}
|
||||
|
||||
runtime := &util.RuntimeOptions{}
|
||||
|
||||
// 发送短信
|
||||
resp, err := s.client.SendSmsWithOptions(sendSmsRequest, runtime)
|
||||
if err != nil {
|
||||
log.Printf("[短信服务] 发送宕机通知失败: %v", err)
|
||||
return fmt.Errorf("发送宕机通知失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查返回结果
|
||||
if resp.Body.Code == nil || *resp.Body.Code != "OK" {
|
||||
errMsg := "未知错误"
|
||||
if resp.Body.Message != nil {
|
||||
errMsg = *resp.Body.Message
|
||||
}
|
||||
log.Printf("[短信服务] 宕机通知发送失败: %s", errMsg)
|
||||
return fmt.Errorf("宕机通知发送失败: %s", errMsg)
|
||||
}
|
||||
|
||||
log.Printf("[短信服务] 服务宕机通知发送成功,手机号: %s,通知码: %s", phone, alertCode)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo 启动AI小红书后端服务(开发环境)...
|
||||
|
||||
:: 检查go环境
|
||||
where go >nul 2>nul
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo 错误: 未检测到Go环境,请先安装Go
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 下载依赖
|
||||
echo 下载依赖...
|
||||
go mod tidy
|
||||
|
||||
:: 启动服务
|
||||
echo 启动开发环境服务...
|
||||
go run main.go -env=dev
|
||||
|
||||
pause
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "启动AI小红书后端服务(开发环境)..."
|
||||
|
||||
# 检查go环境
|
||||
if ! command -v go &> /dev/null
|
||||
then
|
||||
echo "错误: 未检测到Go环境,请先安装Go"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 下载依赖
|
||||
echo "下载依赖..."
|
||||
go mod tidy
|
||||
|
||||
# 启动服务
|
||||
echo "启动开发环境服务..."
|
||||
go run main.go -env=dev
|
||||
@@ -1,27 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo 启动AI小红书后端服务(生产环境)...
|
||||
|
||||
:: 检查go环境
|
||||
where go >nul 2>nul
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo 错误: 未检测到Go环境,请先安装Go
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 编译项目
|
||||
echo 编译项目...
|
||||
go build -o ai_xhs.exe main.go
|
||||
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo 编译失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 启动服务
|
||||
echo 启动生产环境服务...
|
||||
ai_xhs.exe -env=prod
|
||||
|
||||
pause
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#########################################
|
||||
# AI小红书 Go 后端 - 生产环境启动脚本
|
||||
# 专用于生产环境快速部署
|
||||
#########################################
|
||||
|
||||
PORT=8070
|
||||
ENV="prod"
|
||||
LOG_FILE="ai_xhs_prod.log"
|
||||
|
||||
echo "=== 停止端口 $PORT 上的 Go 服务 ==="
|
||||
|
||||
# 方法1: 查找 go run 进程
|
||||
GO_PID=$(ps aux | grep "go run main.go" | grep -v grep | awk '{print $2}')
|
||||
|
||||
if [ -n "$GO_PID" ]; then
|
||||
echo "找到 Go 服务进程: $GO_PID"
|
||||
kill -9 $GO_PID 2>/dev/null
|
||||
echo "Go 服务进程已终止"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# 方法2: 强制清理端口
|
||||
echo "强制清理端口 $PORT..."
|
||||
PORT_PID=$(lsof -ti:$PORT 2>/dev/null)
|
||||
if [ -n "$PORT_PID" ]; then
|
||||
echo "端口被进程 $PORT_PID 占用,正在终止..."
|
||||
kill -9 $PORT_PID 2>/dev/null
|
||||
fi
|
||||
|
||||
sudo fuser -k $PORT/tcp 2>/dev/null || true
|
||||
sudo pkill -f ":$PORT" 2>/dev/null || true
|
||||
|
||||
# 等待端口释放
|
||||
sleep 3
|
||||
|
||||
echo ""
|
||||
echo "=== 环境检查 ==="
|
||||
|
||||
# 检查 Go 环境
|
||||
if ! command -v go &> /dev/null; then
|
||||
echo "❌ 错误: 未检测到 Go 环境"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Go 环境: $(go version)"
|
||||
|
||||
# 检查配置文件
|
||||
if [ ! -f "config/config.prod.yaml" ]; then
|
||||
echo "❌ 错误: 未找到生产环境配置文件"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 配置文件: config/config.prod.yaml"
|
||||
|
||||
echo ""
|
||||
echo "=== 下载依赖 ==="
|
||||
go mod tidy
|
||||
|
||||
echo ""
|
||||
echo "=== 启动生产环境服务 ==="
|
||||
|
||||
# 设置环境变量
|
||||
export APP_ENV=prod
|
||||
|
||||
# 清空旧日志
|
||||
> $LOG_FILE
|
||||
|
||||
# 启动服务
|
||||
nohup go run main.go > $LOG_FILE 2>&1 &
|
||||
NEW_PID=$!
|
||||
|
||||
echo "✅ 服务已启动 (PID: $NEW_PID)"
|
||||
|
||||
# 验证启动
|
||||
echo ""
|
||||
echo "=== 启动验证 (等待 5 秒) ==="
|
||||
sleep 5
|
||||
|
||||
if ps -p $NEW_PID > /dev/null 2>&1; then
|
||||
echo "✅ Go 服务启动成功"
|
||||
echo "📋 日志文件: $LOG_FILE"
|
||||
echo "👀 查看日志: tail -f $LOG_FILE"
|
||||
echo "🌐 服务地址: http://localhost:$PORT"
|
||||
echo "🔍 进程PID: $NEW_PID"
|
||||
|
||||
# 检查端口监听
|
||||
if lsof -ti:$PORT > /dev/null 2>&1; then
|
||||
echo "✅ 端口 $PORT 监听正常"
|
||||
else
|
||||
echo "⚠️ 端口 $PORT 未监听,请检查日志"
|
||||
fi
|
||||
|
||||
# 显示最近日志
|
||||
echo ""
|
||||
echo "=== 最近日志 ==="
|
||||
tail -n 10 $LOG_FILE
|
||||
else
|
||||
echo "❌ Go 服务启动失败,请检查日志"
|
||||
echo ""
|
||||
tail -n 20 $LOG_FILE
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
#########################################
|
||||
# AI小红书 Go 后端 - 停止服务脚本
|
||||
#########################################
|
||||
|
||||
# 默认端口
|
||||
DEV_PORT=8080
|
||||
PROD_PORT=8070
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} 停止 AI小红书 Go 后端服务${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# 停止指定端口的服务
|
||||
stop_port() {
|
||||
local PORT=$1
|
||||
echo -e "${YELLOW}正在停止端口 $PORT 上的服务...${NC}"
|
||||
|
||||
# 查找占用端口的进程
|
||||
PORT_PID=$(lsof -ti:$PORT 2>/dev/null)
|
||||
|
||||
if [ -n "$PORT_PID" ]; then
|
||||
echo " 找到进程: $PORT_PID"
|
||||
kill -9 $PORT_PID 2>/dev/null && echo -e " ${GREEN}✅ 已终止进程 $PORT_PID${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}未找到占用端口 $PORT 的进程${NC}"
|
||||
fi
|
||||
|
||||
# 使用 fuser 强制清理
|
||||
sudo fuser -k $PORT/tcp 2>/dev/null || true
|
||||
}
|
||||
|
||||
# 停止所有 go run main.go 进程
|
||||
echo -e "${BLUE}=== 方法1: 停止 go run 进程 ===${NC}"
|
||||
GO_PIDS=$(ps aux | grep "go run main.go" | grep -v grep | awk '{print $2}')
|
||||
|
||||
if [ -n "$GO_PIDS" ]; then
|
||||
echo -e "${YELLOW}找到 Go 服务进程:${NC}"
|
||||
ps aux | grep "go run main.go" | grep -v grep
|
||||
echo ""
|
||||
for PID in $GO_PIDS; do
|
||||
kill -9 $PID 2>/dev/null && echo -e "${GREEN}✅ 已终止进程: $PID${NC}"
|
||||
done
|
||||
else
|
||||
echo -e "${YELLOW}未找到 go run main.go 进程${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# 停止开发环境端口
|
||||
echo -e "${BLUE}=== 方法2: 清理开发环境端口 ($DEV_PORT) ===${NC}"
|
||||
stop_port $DEV_PORT
|
||||
|
||||
echo ""
|
||||
|
||||
# 停止生产环境端口
|
||||
echo -e "${BLUE}=== 方法3: 清理生产环境端口 ($PROD_PORT) ===${NC}"
|
||||
stop_port $PROD_PORT
|
||||
|
||||
echo ""
|
||||
|
||||
# 清理所有相关进程
|
||||
echo -e "${BLUE}=== 方法4: 清理所有相关进程 ===${NC}"
|
||||
sudo pkill -f "main.go" 2>/dev/null && echo -e "${GREEN}✅ 已清理所有 main.go 进程${NC}" || echo -e "${YELLOW}未找到其他相关进程${NC}"
|
||||
|
||||
# 等待进程完全退出
|
||||
sleep 2
|
||||
|
||||
echo ""
|
||||
|
||||
# 验证
|
||||
echo -e "${BLUE}=== 验证结果 ===${NC}"
|
||||
|
||||
# 检查端口
|
||||
for PORT in $DEV_PORT $PROD_PORT; do
|
||||
if lsof -ti:$PORT > /dev/null 2>&1; then
|
||||
echo -e "${RED}⚠️ 端口 $PORT 仍被占用:${NC}"
|
||||
lsof -i:$PORT
|
||||
else
|
||||
echo -e "${GREEN}✅ 端口 $PORT 已释放${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 检查进程
|
||||
if ps aux | grep "go run main.go" | grep -v grep > /dev/null; then
|
||||
echo -e "${RED}⚠️ 仍有 go run 进程在运行:${NC}"
|
||||
ps aux | grep "go run main.go" | grep -v grep
|
||||
else
|
||||
echo -e "${GREEN}✅ 所有 go run 进程已停止${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} ✅ 服务已停止${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
@@ -1,42 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// GeneratePassword 生成bcrypt加密密码
|
||||
func GeneratePassword(password string) (string, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 为测试数据生成加密密码
|
||||
passwords := []string{
|
||||
"admin123", // 企业管理员密码
|
||||
"user123", // 普通用户密码
|
||||
}
|
||||
|
||||
fmt.Println("生成加密密码:")
|
||||
fmt.Println("=====================================")
|
||||
|
||||
for i, pwd := range passwords {
|
||||
hashed, err := GeneratePassword(pwd)
|
||||
if err != nil {
|
||||
fmt.Printf("生成密码失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%d. 原始密码: %s\n", i+1, pwd)
|
||||
fmt.Printf(" 加密后: %s\n\n", hashed)
|
||||
}
|
||||
|
||||
fmt.Println("=====================================")
|
||||
fmt.Println("使用说明:")
|
||||
fmt.Println("1. 复制上面的加密密码")
|
||||
fmt.Println("2. 在 test_data_ai_wht.sql 中替换对应的密码占位符")
|
||||
fmt.Println("3. 执行 SQL 文件导入测试数据")
|
||||
}
|
||||
257
go_backend/tools/service_monitor.go
Normal file
257
go_backend/tools/service_monitor.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"ai_xhs/service"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServiceMonitor 服务监控器
|
||||
type ServiceMonitor struct {
|
||||
alertPhone string
|
||||
serviceName string
|
||||
smsService *service.SmsService
|
||||
isRunning bool
|
||||
mutex sync.Mutex
|
||||
shutdownChan chan os.Signal
|
||||
alertSent bool // 标记是否已发送通知,避免重复发送
|
||||
heartbeatFile string // 心跳文件路径
|
||||
lastHeartbeat time.Time // 最后心跳时间
|
||||
}
|
||||
|
||||
// HeartbeatData 心跳数据
|
||||
type HeartbeatData struct {
|
||||
ServiceName string `json:"service_name"`
|
||||
LastHeartbeat time.Time `json:"last_heartbeat"`
|
||||
PID int `json:"pid"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
GracefulShut bool `json:"graceful_shutdown"` // 是否为正常关闭
|
||||
}
|
||||
|
||||
var (
|
||||
monitorInstance *ServiceMonitor
|
||||
monitorOnce sync.Once
|
||||
)
|
||||
|
||||
// GetServiceMonitor 获取服务监控器单例
|
||||
func GetServiceMonitor(alertPhone string, serviceName string) *ServiceMonitor {
|
||||
monitorOnce.Do(func() {
|
||||
heartbeatFile := filepath.Join(os.TempDir(), "ai_xhs_service_heartbeat.json")
|
||||
monitorInstance = &ServiceMonitor{
|
||||
alertPhone: alertPhone,
|
||||
serviceName: serviceName,
|
||||
smsService: service.GetSmsService(),
|
||||
isRunning: true,
|
||||
shutdownChan: make(chan os.Signal, 1),
|
||||
alertSent: false,
|
||||
heartbeatFile: heartbeatFile,
|
||||
lastHeartbeat: time.Now(),
|
||||
}
|
||||
})
|
||||
return monitorInstance
|
||||
}
|
||||
|
||||
// StartMonitoring 启动服务监控
|
||||
// 监听系统信号,在服务异常退出时发送短信通知
|
||||
func (m *ServiceMonitor) StartMonitoring() {
|
||||
// 检查上次启动是否异常关闭
|
||||
m.checkLastShutdown()
|
||||
|
||||
// 启动心跳任务
|
||||
m.startHeartbeat()
|
||||
|
||||
// 监听退出信号
|
||||
signal.Notify(m.shutdownChan,
|
||||
os.Interrupt, // Ctrl+C
|
||||
syscall.SIGTERM, // kill命令
|
||||
syscall.SIGQUIT, // Ctrl+\
|
||||
syscall.SIGABRT, // abort
|
||||
)
|
||||
|
||||
go func() {
|
||||
sig := <-m.shutdownChan
|
||||
log.Printf("[服务监控] 捕获到退出信号: %v", sig)
|
||||
|
||||
m.mutex.Lock()
|
||||
m.isRunning = false
|
||||
m.mutex.Unlock()
|
||||
|
||||
// 标记为正常关闭
|
||||
m.markGracefulShutdown()
|
||||
|
||||
// 发送宕机通知
|
||||
if !m.alertSent {
|
||||
m.sendAlert("服务接收到退出信号")
|
||||
}
|
||||
|
||||
// 给短信发送一些时间
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 退出程序
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
log.Printf("[服务监控] 服务监控已启动,监控电话: %s", m.alertPhone)
|
||||
log.Printf("[服务监控] 心跳文件: %s", m.heartbeatFile)
|
||||
}
|
||||
|
||||
// SetAlertSent 设置通知已发送标记(供外部调用,避免重复发送)
|
||||
func (m *ServiceMonitor) SetAlertSent() {
|
||||
m.mutex.Lock()
|
||||
m.alertSent = true
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SendManualAlert 手动发送服务宕机通知
|
||||
func (m *ServiceMonitor) SendManualAlert(reason string) error {
|
||||
return m.sendAlert(reason)
|
||||
}
|
||||
|
||||
// sendAlert 发送宕机通知
|
||||
func (m *ServiceMonitor) sendAlert(reason string) error {
|
||||
if m.alertSent {
|
||||
log.Printf("[服务监控] 宕机通知已发送,跳过重复发送")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("[服务监控] 服务宕机,原因: %s", reason)
|
||||
|
||||
err := m.smsService.SendServiceDownAlert(m.alertPhone, m.serviceName)
|
||||
if err != nil {
|
||||
log.Printf("[服务监控] 发送宕机通知失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
m.alertSent = true
|
||||
log.Printf("[服务监控] 宕机通知已发送到 %s", m.alertPhone)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning 检查服务是否运行中
|
||||
func (m *ServiceMonitor) IsRunning() bool {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return m.isRunning
|
||||
}
|
||||
|
||||
// Shutdown 优雅关闭
|
||||
func (m *ServiceMonitor) Shutdown() {
|
||||
if m.shutdownChan != nil {
|
||||
m.shutdownChan <- syscall.SIGTERM
|
||||
}
|
||||
}
|
||||
|
||||
// startHeartbeat 启动心跳任务,每30秒更新一次
|
||||
func (m *ServiceMonitor) startHeartbeat() {
|
||||
// 立即写入一次
|
||||
m.updateHeartbeat()
|
||||
|
||||
// 启动定时任务
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
if !m.IsRunning() {
|
||||
break
|
||||
}
|
||||
m.updateHeartbeat()
|
||||
}
|
||||
}()
|
||||
log.Printf("[服务监控] 心跳任务已启动,每30秒更新一次")
|
||||
}
|
||||
|
||||
// updateHeartbeat 更新心跳文件
|
||||
func (m *ServiceMonitor) updateHeartbeat() {
|
||||
m.mutex.Lock()
|
||||
m.lastHeartbeat = time.Now()
|
||||
m.mutex.Unlock()
|
||||
|
||||
data := HeartbeatData{
|
||||
ServiceName: m.serviceName,
|
||||
LastHeartbeat: m.lastHeartbeat,
|
||||
PID: os.Getpid(),
|
||||
StartTime: time.Now(), // 在实际应用中应该记录启动时间
|
||||
GracefulShut: false, // 默认未正常关闭
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("[服务监控] 序列化心跳数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(m.heartbeatFile, jsonData, 0644); err != nil {
|
||||
log.Printf("[服务监控] 写入心跳文件失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// markGracefulShutdown 标记为正常关闭
|
||||
func (m *ServiceMonitor) markGracefulShutdown() {
|
||||
data := HeartbeatData{
|
||||
ServiceName: m.serviceName,
|
||||
LastHeartbeat: time.Now(),
|
||||
PID: os.Getpid(),
|
||||
StartTime: m.lastHeartbeat,
|
||||
GracefulShut: true, // 标记为正常关闭
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("[服务监控] 序列化关闭数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(m.heartbeatFile, jsonData, 0644); err != nil {
|
||||
log.Printf("[服务监控] 写入关闭标记失败: %v", err)
|
||||
}
|
||||
log.Printf("[服务监控] 已标记为正常关闭")
|
||||
}
|
||||
|
||||
// checkLastShutdown 检查上次关闭是否异常
|
||||
func (m *ServiceMonitor) checkLastShutdown() {
|
||||
// 读取心跳文件
|
||||
if _, err := os.Stat(m.heartbeatFile); os.IsNotExist(err) {
|
||||
log.Printf("[服务监控] 未找到历史心跳文件,可能是首次启动")
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := ioutil.ReadFile(m.heartbeatFile)
|
||||
if err != nil {
|
||||
log.Printf("[服务监控] 读取心跳文件失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var lastData HeartbeatData
|
||||
if err := json.Unmarshal(fileData, &lastData); err != nil {
|
||||
log.Printf("[服务监控] 解析心跳数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[服务监控] 上次心跳: %v, PID: %d, 正常关闭: %v",
|
||||
lastData.LastHeartbeat.Format("2006-01-02 15:04:05"),
|
||||
lastData.PID,
|
||||
lastData.GracefulShut)
|
||||
|
||||
// 如果上次不是正常关闭,发送通知
|
||||
if !lastData.GracefulShut {
|
||||
timeSinceLastHeartbeat := time.Since(lastData.LastHeartbeat)
|
||||
// 如果距离上次心跳超过2分钟,认为是异常关闭
|
||||
if timeSinceLastHeartbeat > 2*time.Minute {
|
||||
log.Printf("[服务监控] 检测到上次服务异常关闭(%v前),发送通知", timeSinceLastHeartbeat)
|
||||
err := m.smsService.SendServiceDownAlert(m.alertPhone, m.serviceName)
|
||||
if err != nil {
|
||||
log.Printf("[服务监控] 发送异常关闭通知失败: %v", err)
|
||||
} else {
|
||||
log.Printf("[服务监控] 已发送异常关闭通知")
|
||||
}
|
||||
} else {
|
||||
log.Printf("[服务监控] 距离上次心跳仅%v,可能是快速重启,不发送通知", timeSinceLastHeartbeat)
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
go_backend/uploads/images/1_1766285903026006700.png
Normal file
BIN
go_backend/uploads/images/1_1766285903026006700.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 MiB |
125
go_backend/utils/cache.go
Normal file
125
go_backend/utils/cache.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"ai_xhs/database"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// SetCache 设置缓存
|
||||
func SetCache(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return database.RDB.Set(ctx, key, data, expiration).Err()
|
||||
}
|
||||
|
||||
// GetCache 获取缓存
|
||||
func GetCache(ctx context.Context, key string, dest interface{}) error {
|
||||
data, err := database.RDB.Get(ctx, key).Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, dest)
|
||||
}
|
||||
|
||||
// DelCache 删除缓存
|
||||
func DelCache(ctx context.Context, keys ...string) error {
|
||||
return database.RDB.Del(ctx, keys...).Err()
|
||||
}
|
||||
|
||||
// ExistsCache 检查缓存是否存在
|
||||
func ExistsCache(ctx context.Context, key string) (bool, error) {
|
||||
count, err := database.RDB.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// ExpireCache 设置缓存过期时间
|
||||
func ExpireCache(ctx context.Context, key string, expiration time.Duration) error {
|
||||
return database.RDB.Expire(ctx, key, expiration).Err()
|
||||
}
|
||||
|
||||
// GetTTL 获取缓存剩余生存时间
|
||||
func GetTTL(ctx context.Context, key string) (time.Duration, error) {
|
||||
return database.RDB.TTL(ctx, key).Result()
|
||||
}
|
||||
|
||||
// IncrCache 递增计数器
|
||||
func IncrCache(ctx context.Context, key string) (int64, error) {
|
||||
return database.RDB.Incr(ctx, key).Result()
|
||||
}
|
||||
|
||||
// DecrCache 递减计数器
|
||||
func DecrCache(ctx context.Context, key string) (int64, error) {
|
||||
return database.RDB.Decr(ctx, key).Result()
|
||||
}
|
||||
|
||||
// SetCacheNX 设置缓存(仅当key不存在时)
|
||||
func SetCacheNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return database.RDB.SetNX(ctx, key, data, expiration).Result()
|
||||
}
|
||||
|
||||
// HSetCache 设置哈希字段
|
||||
func HSetCache(ctx context.Context, key, field string, value interface{}) error {
|
||||
return database.RDB.HSet(ctx, key, field, value).Err()
|
||||
}
|
||||
|
||||
// HGetCache 获取哈希字段
|
||||
func HGetCache(ctx context.Context, key, field string) (string, error) {
|
||||
return database.RDB.HGet(ctx, key, field).Result()
|
||||
}
|
||||
|
||||
// HGetAllCache 获取哈希所有字段
|
||||
func HGetAllCache(ctx context.Context, key string) (map[string]string, error) {
|
||||
return database.RDB.HGetAll(ctx, key).Result()
|
||||
}
|
||||
|
||||
// HDelCache 删除哈希字段
|
||||
func HDelCache(ctx context.Context, key string, fields ...string) error {
|
||||
return database.RDB.HDel(ctx, key, fields...).Err()
|
||||
}
|
||||
|
||||
// SAddCache 添加集合成员
|
||||
func SAddCache(ctx context.Context, key string, members ...interface{}) error {
|
||||
return database.RDB.SAdd(ctx, key, members...).Err()
|
||||
}
|
||||
|
||||
// SMembersCache 获取集合所有成员
|
||||
func SMembersCache(ctx context.Context, key string) ([]string, error) {
|
||||
return database.RDB.SMembers(ctx, key).Result()
|
||||
}
|
||||
|
||||
// SRemCache 删除集合成员
|
||||
func SRemCache(ctx context.Context, key string, members ...interface{}) error {
|
||||
return database.RDB.SRem(ctx, key, members...).Err()
|
||||
}
|
||||
|
||||
// ZAddCache 添加有序集合成员
|
||||
func ZAddCache(ctx context.Context, key string, score float64, member interface{}) error {
|
||||
z := redis.Z{
|
||||
Score: score,
|
||||
Member: member,
|
||||
}
|
||||
return database.RDB.ZAdd(ctx, key, z).Err()
|
||||
}
|
||||
|
||||
// ZRangeCache 获取有序集合指定范围成员
|
||||
func ZRangeCache(ctx context.Context, key string, start, stop int64) ([]string, error) {
|
||||
return database.RDB.ZRange(ctx, key, start, stop).Result()
|
||||
}
|
||||
|
||||
// ZRemCache 删除有序集合成员
|
||||
func ZRemCache(ctx context.Context, key string, members ...interface{}) error {
|
||||
return database.RDB.ZRem(ctx, key, members...).Err()
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package utils
|
||||
|
||||
import (
|
||||
"ai_xhs/config"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -44,3 +46,45 @@ func ParseToken(tokenString string) (*Claims, error) {
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// StoreTokenInRedis 将Token存入Redis
|
||||
func StoreTokenInRedis(ctx context.Context, employeeID int, tokenString string) error {
|
||||
// Redis key: token:employee:{employeeID}
|
||||
key := fmt.Sprintf("token:employee:%d", employeeID)
|
||||
|
||||
// 存储token,过期时间与JWT一致
|
||||
expiration := time.Duration(config.AppConfig.JWT.ExpireHours) * time.Hour
|
||||
return SetCache(ctx, key, tokenString, expiration)
|
||||
}
|
||||
|
||||
// ValidateTokenInRedis 验证Token是否在Redis中存在(校验是否被禁用)
|
||||
func ValidateTokenInRedis(ctx context.Context, employeeID int, tokenString string) error {
|
||||
key := fmt.Sprintf("token:employee:%d", employeeID)
|
||||
|
||||
// 从Redis获取存储的token
|
||||
var storedToken string
|
||||
err := GetCache(ctx, key, &storedToken)
|
||||
if err != nil {
|
||||
return errors.New("token已失效或用户已被禁用")
|
||||
}
|
||||
|
||||
// 比对token是否一致
|
||||
if storedToken != tokenString {
|
||||
return errors.New("token不匹配,用户可能已重新登录")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeToken 撤销Token(禁用用户)
|
||||
func RevokeToken(ctx context.Context, employeeID int) error {
|
||||
key := fmt.Sprintf("token:employee:%d", employeeID)
|
||||
return DelCache(ctx, key)
|
||||
}
|
||||
|
||||
// RevokeAllUserTokens 撤销用户的所有Token(如果有多设备登录)
|
||||
func RevokeAllUserTokens(ctx context.Context, employeeID int) error {
|
||||
// 当前实现:一个用户只保存一个token
|
||||
// 如果需要支持多设备,可以改为 token:employee:{employeeID}:{deviceID}
|
||||
return RevokeToken(ctx, employeeID)
|
||||
}
|
||||
|
||||
351
go_backend/utils/oss.go
Normal file
351
go_backend/utils/oss.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"ai_xhs/config"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// OSSStorage 阿里云OSS存储服务
|
||||
type OSSStorage struct {
|
||||
client *oss.Client
|
||||
bucket *oss.Bucket
|
||||
config *config.OSSConfig
|
||||
}
|
||||
|
||||
var ossStorage *OSSStorage
|
||||
|
||||
// InitOSS 初始化OSS客户端
|
||||
func InitOSS() error {
|
||||
cfg := &config.AppConfig.Upload.OSS
|
||||
|
||||
// 打印详细配置信息用于调试
|
||||
fmt.Printf("\n=== OSS初始化配置 ===\n")
|
||||
fmt.Printf("Endpoint: [%s]\n", cfg.Endpoint)
|
||||
fmt.Printf("AccessKeyID: [%s]\n", cfg.AccessKeyID)
|
||||
fmt.Printf("AccessKeySecret: [%s] (长度: %d)\n", cfg.AccessKeySecret, len(cfg.AccessKeySecret))
|
||||
fmt.Printf("BucketName: [%s]\n", cfg.BucketName)
|
||||
fmt.Printf("BasePath: [%s]\n", cfg.BasePath)
|
||||
fmt.Printf("Domain: [%s]\n", cfg.Domain)
|
||||
fmt.Printf("==================\n\n")
|
||||
|
||||
// 创建OSSClient实例
|
||||
client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建OSS客户端失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取存储空间
|
||||
bucket, err := client.Bucket(cfg.BucketName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取OSS Bucket失败: %w", err)
|
||||
}
|
||||
|
||||
ossStorage = &OSSStorage{
|
||||
client: client,
|
||||
bucket: bucket,
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// min 辅助函数
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// UploadFile 上传文件到OSS
|
||||
func (s *OSSStorage) UploadFile(file multipart.File, filename string, objectPath string) (string, error) {
|
||||
// 生成OSS对象路径
|
||||
objectKey := s.generateObjectKey(objectPath, filename)
|
||||
|
||||
// 获取文件MIME类型
|
||||
contentType := getContentType(filename)
|
||||
|
||||
// 上传文件到OSS,设置Content-Type和其他元数据
|
||||
// 使用 ObjectACL 设置为公共读,确保文件可以直接访问
|
||||
err := s.bucket.PutObject(objectKey, file,
|
||||
oss.ContentType(contentType),
|
||||
oss.ObjectACL(oss.ACLPublicRead),
|
||||
oss.ContentDisposition("inline"), // 关键:设置为inline而不是attachment
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取OSS返回的文件URL
|
||||
fileURL, err := s.GetFileURL(objectKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
|
||||
}
|
||||
return fileURL, nil
|
||||
}
|
||||
|
||||
// UploadFromBytes 从字节数组上传文件到OSS
|
||||
func (s *OSSStorage) UploadFromBytes(data []byte, filename string, objectPath string) (string, error) {
|
||||
// 生成OSS对象路径
|
||||
objectKey := s.generateObjectKey(objectPath, filename)
|
||||
|
||||
// 获取文件MIME类型
|
||||
contentType := getContentType(filename)
|
||||
|
||||
// 上传文件到OSS,设置Content-Type和其他元数据
|
||||
err := s.bucket.PutObject(objectKey, strings.NewReader(string(data)),
|
||||
oss.ContentType(contentType),
|
||||
oss.ObjectACL(oss.ACLPublicRead),
|
||||
oss.ContentDisposition("inline"),
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取OSS返回的文件URL
|
||||
fileURL, err := s.GetFileURL(objectKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
|
||||
}
|
||||
return fileURL, nil
|
||||
}
|
||||
|
||||
// UploadFromReader 从Reader上传文件到OSS
|
||||
func (s *OSSStorage) UploadFromReader(reader io.Reader, filename string, objectPath string) (string, error) {
|
||||
// 生成OSS对象路径
|
||||
objectKey := s.generateObjectKey(objectPath, filename)
|
||||
|
||||
// 获取文件MIME类型
|
||||
contentType := getContentType(filename)
|
||||
|
||||
// 打印详细上传信息
|
||||
fmt.Printf("\n=== OSS上传请求 ===\n")
|
||||
fmt.Printf("ObjectKey: [%s]\n", objectKey)
|
||||
fmt.Printf("ContentType: [%s]\n", contentType)
|
||||
fmt.Printf("Endpoint: [%s]\n", s.config.Endpoint)
|
||||
fmt.Printf("BucketName: [%s]\n", s.config.BucketName)
|
||||
fmt.Printf("AccessKeyID: [%s]\n", s.config.AccessKeyID)
|
||||
fmt.Printf("AccessKeySecret: [%s]\n", s.config.AccessKeySecret)
|
||||
fmt.Printf("==================\n\n")
|
||||
|
||||
// 上传文件到OSS,设置Content-Type和其他元数据
|
||||
err := s.bucket.PutObject(objectKey, reader,
|
||||
oss.ContentType(contentType),
|
||||
oss.ObjectACL(oss.ACLPublicRead),
|
||||
oss.ContentDisposition("inline"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("\n!!! OSS上传失败 !!!\n")
|
||||
fmt.Printf("错误详情: %v\n", err)
|
||||
fmt.Printf("错误类型: %T\n", err)
|
||||
fmt.Printf("==================\n\n")
|
||||
return "", fmt.Errorf("上传文件到OSS失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取OSS返回的文件URL
|
||||
fileURL, err := s.GetFileURL(objectKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取OSS文件URL失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ OSS上传成功: %s\n\n", fileURL)
|
||||
return fileURL, nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除OSS中的文件
|
||||
func (s *OSSStorage) DeleteFile(objectKey string) error {
|
||||
err := s.bucket.DeleteObject(objectKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除OSS文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsObjectExist 检查对象是否存在
|
||||
func (s *OSSStorage) IsObjectExist(objectKey string) (bool, error) {
|
||||
exist, err := s.bucket.IsObjectExist(objectKey)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("检查OSS对象是否存在失败: %w", err)
|
||||
}
|
||||
return exist, nil
|
||||
}
|
||||
|
||||
// GetFileURL 获取文件访问URL(使用OSS SDK标准方法)
|
||||
func (s *OSSStorage) GetFileURL(objectKey string) (string, error) {
|
||||
// 如果配置了自定义域名,使用自定义域名
|
||||
if s.config.Domain != "" {
|
||||
// 确保 Domain 以 https:// 开头
|
||||
domain := s.config.Domain
|
||||
if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") {
|
||||
domain = "https://" + domain
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s", strings.TrimRight(domain, "/"), objectKey)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// 使用OSS SDK获取对象的公共URL
|
||||
// 正确格式:https://bucket-name.endpoint/objectKey
|
||||
// Endpoint 不应该包含 https://
|
||||
endpoint := s.config.Endpoint
|
||||
// 移除 endpoint 中可能存在的协议前缀
|
||||
endpoint = strings.TrimPrefix(endpoint, "https://")
|
||||
endpoint = strings.TrimPrefix(endpoint, "http://")
|
||||
endpoint = strings.TrimRight(endpoint, "/")
|
||||
|
||||
// 移除 objectKey 开头的斜杠
|
||||
objectKey = strings.TrimLeft(objectKey, "/")
|
||||
|
||||
url := fmt.Sprintf("https://%s.%s/%s", s.config.BucketName, endpoint, objectKey)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// GeneratePresignedURL 生成临时访问URL(带签名)
|
||||
func (s *OSSStorage) GeneratePresignedURL(objectKey string, expireSeconds int64) (string, error) {
|
||||
signedURL, err := s.bucket.SignURL(objectKey, oss.HTTPGet, expireSeconds)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("生成预签名URL失败: %w", err)
|
||||
}
|
||||
return signedURL, nil
|
||||
}
|
||||
|
||||
// generateObjectKey 生成OSS对象键名
|
||||
func (s *OSSStorage) generateObjectKey(objectPath string, filename string) string {
|
||||
// 组合基础路径和对象路径
|
||||
basePath := strings.TrimRight(s.config.BasePath, "/")
|
||||
objectPath = strings.TrimLeft(objectPath, "/")
|
||||
|
||||
// OSS 路径统一使用斜杠,避免 Windows 下使用反斜杠
|
||||
if basePath == "" {
|
||||
return strings.ReplaceAll(filepath.ToSlash(filepath.Join(objectPath, filename)), "\\", "/")
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(filepath.ToSlash(filepath.Join(basePath, objectPath, filename)), "\\", "/")
|
||||
}
|
||||
|
||||
// UploadToOSS 上传文件到OSS(兼容旧接口)
|
||||
func UploadToOSS(reader io.Reader, originalFilename string) (string, error) {
|
||||
if ossStorage == nil {
|
||||
return "", fmt.Errorf("OSS未初始化,请先调用InitOSS()")
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
filename := GenerateFilename(originalFilename)
|
||||
|
||||
// 使用日期目录作为路径
|
||||
objectPath := time.Now().Format("20060102")
|
||||
|
||||
return ossStorage.UploadFromReader(reader, filename, objectPath)
|
||||
}
|
||||
|
||||
// DeleteFromOSS 从OSS删除文件(兼容旧接口)
|
||||
func DeleteFromOSS(fileURL string) error {
|
||||
if ossStorage == nil {
|
||||
return fmt.Errorf("OSS未初始化")
|
||||
}
|
||||
|
||||
cfg := ossStorage.config
|
||||
|
||||
// 从URL中提取ObjectKey
|
||||
var objectKey string
|
||||
if cfg.Domain != "" {
|
||||
// 自定义域名格式: https://domain.com/path/file.jpg
|
||||
domain := cfg.Domain
|
||||
if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") {
|
||||
domain = "https://" + domain
|
||||
}
|
||||
objectKey = strings.TrimPrefix(fileURL, fmt.Sprintf("%s/", strings.TrimRight(domain, "/")))
|
||||
} else {
|
||||
// 默认域名格式: https://bucket.endpoint/path/file.jpg
|
||||
endpoint := strings.TrimPrefix(cfg.Endpoint, "https://")
|
||||
endpoint = strings.TrimPrefix(endpoint, "http://")
|
||||
objectKey = strings.TrimPrefix(fileURL, fmt.Sprintf("https://%s.%s/", cfg.BucketName, endpoint))
|
||||
}
|
||||
|
||||
return ossStorage.DeleteFile(objectKey)
|
||||
}
|
||||
|
||||
// GenerateFilename 生成唯一文件名
|
||||
func GenerateFilename(originalFilename string) string {
|
||||
ext := filepath.Ext(originalFilename)
|
||||
name := strings.TrimSuffix(originalFilename, ext)
|
||||
|
||||
// 生成时间戳和UUID
|
||||
timestamp := time.Now().Format("20060102150405")
|
||||
uuidStr := uuid.New().String()[:8]
|
||||
|
||||
// 清理文件名,移除特殊字符
|
||||
name = strings.ReplaceAll(name, " ", "_")
|
||||
name = strings.ReplaceAll(name, "(", "")
|
||||
name = strings.ReplaceAll(name, ")", "")
|
||||
|
||||
return fmt.Sprintf("%s_%s_%s%s", name, timestamp, uuidStr, ext)
|
||||
}
|
||||
|
||||
// IsValidImageType 验证图片文件类型
|
||||
func IsValidImageType(filename string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"}
|
||||
|
||||
for _, validExt := range validExts {
|
||||
if ext == validExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getContentType 根据文件名获取MIME类型
|
||||
func getContentType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
// 常见图片类型
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
case ".bmp":
|
||||
return "image/bmp"
|
||||
case ".ico":
|
||||
return "image/x-icon"
|
||||
// 常见文档类型
|
||||
case ".pdf":
|
||||
return "application/pdf"
|
||||
case ".doc":
|
||||
return "application/msword"
|
||||
case ".docx":
|
||||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
case ".xls":
|
||||
return "application/vnd.ms-excel"
|
||||
case ".xlsx":
|
||||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
case ".txt":
|
||||
return "text/plain; charset=utf-8"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".xml":
|
||||
return "application/xml"
|
||||
default:
|
||||
// 使用Go标准库自动检测
|
||||
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
|
||||
return mimeType
|
||||
}
|
||||
// 默认类型
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
19
go_backend/utils/password.go
Normal file
19
go_backend/utils/password.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// HashPassword 密码加密(使用SHA256,与Python版本保持一致)
|
||||
func HashPassword(password string) string {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// VerifyPassword 验证密码
|
||||
func VerifyPassword(password, hashedPassword string) bool {
|
||||
fmt.Printf(HashPassword(password))
|
||||
return HashPassword(password) == hashedPassword
|
||||
}
|
||||
Reference in New Issue
Block a user