mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-12 06:04:26 +08:00
插件配置已完成
This commit is contained in:
@@ -25,55 +25,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthConfig 定义授权配置
|
||||
type AuthConfig struct {
|
||||
ExactPaths map[string]bool // 精确匹配的路径
|
||||
PrefixPaths map[string]bool // 前缀匹配的路径
|
||||
}
|
||||
|
||||
var authConfig = &AuthConfig{
|
||||
ExactPaths: map[string]bool{
|
||||
"/api/user/login": false,
|
||||
"/api/user/logout": false,
|
||||
"/api/user/resetPass": false,
|
||||
"/api/user/register": false,
|
||||
"/api/user/clogin": false,
|
||||
"/api/user/clogin/callback": false,
|
||||
"/api/user/signin": false,
|
||||
"/api/admin/login": false,
|
||||
"/api/admin/logout": false,
|
||||
"/api/admin/login/captcha": false,
|
||||
"/api/app/list": false,
|
||||
"/api/app/type/list": false,
|
||||
"/api/app/list/user": false,
|
||||
"/api/model/list": false,
|
||||
"/api/mj/imgWall": false,
|
||||
"/api/mj/notify": false,
|
||||
"/api/invite/hits": false,
|
||||
"/api/sd/imgWall": false,
|
||||
"/api/dall/imgWall": false,
|
||||
"/api/product/list": false,
|
||||
"/api/menu/list": false,
|
||||
"/api/markMap/client": false,
|
||||
"/api/payment/doPay": false,
|
||||
"/api/payment/payWays": false,
|
||||
"/api/download": false,
|
||||
"/api/dall/models": false,
|
||||
"/api/chat/message": false, // 聊天接口需要特殊处理
|
||||
"/api/realtime": false, // 实时通信接口需要特殊处理
|
||||
"/api/realtime/voice": false, // 语音聊天接口需要特殊处理
|
||||
},
|
||||
PrefixPaths: map[string]bool{
|
||||
"/api/test/": false,
|
||||
"/api/payment/notify/": false,
|
||||
"/api/config/": false,
|
||||
"/api/function/": false,
|
||||
"/api/sms/": false,
|
||||
"/api/captcha/": false,
|
||||
"/static/": false,
|
||||
},
|
||||
}
|
||||
|
||||
type AppServer struct {
|
||||
Config *types.AppConfig
|
||||
Engine *gin.Engine
|
||||
@@ -94,10 +45,10 @@ func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types
|
||||
|
||||
func (s *AppServer) Init(client *redis.Client) {
|
||||
s.Engine.Use(middleware.ParameterHandlerMiddleware())
|
||||
s.Engine.Use(middleware.StaticMiddleware())
|
||||
s.Engine.Use(errorHandler)
|
||||
// 添加静态资源访问
|
||||
s.Engine.Static("/static", s.Config.StaticDir)
|
||||
s.Engine.Use(middleware.StaticMiddleware())
|
||||
}
|
||||
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
|
||||
@@ -20,7 +20,6 @@ type MiniOssConfig struct {
|
||||
AccessKey string `json:"access_key"`
|
||||
AccessSecret string `json:"access_secret"`
|
||||
Bucket string `json:"bucket"`
|
||||
SubDir string `json:"sub_dir"`
|
||||
UseSSL bool `json:"use_ssl"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
@@ -30,7 +29,6 @@ type QiNiuOssConfig struct {
|
||||
AccessKey string `json:"access_key"`
|
||||
AccessSecret string `json:"access_secret"`
|
||||
Bucket string `json:"bucket"`
|
||||
SubDir string `json:"sub_dir"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ package types
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type SMSConfig struct {
|
||||
Active string
|
||||
Ali SmsConfigAli
|
||||
Bao SmsConfigBao
|
||||
Active string `json:"active"`
|
||||
Ali SmsConfigAli `json:"aliyun"`
|
||||
Bao SmsConfigBao `json:"bao"`
|
||||
}
|
||||
|
||||
// SmsConfigAli 阿里云短信平台配置
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/middleware"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
@@ -60,12 +59,9 @@ func (h *FunctionHandler) RegisterRoutes() {
|
||||
group.GET("list", h.List)
|
||||
|
||||
// 需要用户授权的接口
|
||||
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
|
||||
{
|
||||
group.POST("weibo", h.WeiBo)
|
||||
group.POST("zaobao", h.ZaoBao)
|
||||
group.POST("dalle3", h.Dall3)
|
||||
}
|
||||
group.POST("weibo", h.WeiBo)
|
||||
group.POST("zaobao", h.ZaoBao)
|
||||
group.POST("dalle3", h.Dall3)
|
||||
}
|
||||
|
||||
type resVo struct {
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
package crawler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/logger"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
)
|
||||
|
||||
// Service 网络爬虫服务
|
||||
type Service struct {
|
||||
browser *rod.Browser
|
||||
}
|
||||
|
||||
// NewService 创建一个新的爬虫服务
|
||||
func NewService() (*Service, error) {
|
||||
// 启动浏览器
|
||||
path, _ := launcher.LookPath()
|
||||
u := launcher.New().Bin(path).
|
||||
Headless(true). // 无头模式
|
||||
Set("disable-web-security", ""). // 禁用网络安全限制
|
||||
Set("disable-gpu", ""). // 禁用 GPU 加速
|
||||
Set("no-sandbox", ""). // 禁用沙箱模式
|
||||
Set("disable-setuid-sandbox", ""). // 禁用 setuid 沙箱
|
||||
MustLaunch()
|
||||
|
||||
browser := rod.New().ControlURL(u).MustConnect()
|
||||
|
||||
return &Service{
|
||||
browser: browser,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchResult 搜索结果
|
||||
type SearchResult struct {
|
||||
Title string `json:"title"` // 标题
|
||||
URL string `json:"url"` // 链接
|
||||
Content string `json:"content"` // 内容摘要
|
||||
}
|
||||
|
||||
// WebSearch 网络搜索
|
||||
func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error) {
|
||||
if keyword == "" {
|
||||
return nil, errors.New("搜索关键词不能为空")
|
||||
}
|
||||
|
||||
if maxPages <= 0 {
|
||||
maxPages = 1
|
||||
}
|
||||
if maxPages > 10 {
|
||||
maxPages = 10 // 最多搜索 10 页
|
||||
}
|
||||
|
||||
results := make([]SearchResult, 0)
|
||||
|
||||
// 使用百度搜索
|
||||
searchURL := fmt.Sprintf("https://www.baidu.com/s?wd=%s", url.QueryEscape(keyword))
|
||||
|
||||
// 设置页面超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建页面
|
||||
page := s.browser.MustPage()
|
||||
defer page.MustClose()
|
||||
|
||||
// 设置视口大小
|
||||
err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("设置视口失败: %v", err)
|
||||
}
|
||||
|
||||
// 导航到搜索页面
|
||||
err = page.Context(ctx).Navigate(searchURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("导航到搜索页面失败: %v", err)
|
||||
}
|
||||
|
||||
// 等待搜索结果加载完成
|
||||
err = page.WaitLoad()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("等待页面加载完成失败: %v", err)
|
||||
}
|
||||
|
||||
// 分析当前页面的搜索结果
|
||||
for i := 0; i < maxPages; i++ {
|
||||
if i > 0 {
|
||||
// 点击下一页按钮
|
||||
nextPage, err := page.Element("a.n")
|
||||
if err != nil || nextPage == nil {
|
||||
break // 没有下一页
|
||||
}
|
||||
|
||||
err = nextPage.Click(proto.InputMouseButtonLeft, 1)
|
||||
if err != nil {
|
||||
break // 点击下一页失败
|
||||
}
|
||||
|
||||
// 等待新页面加载
|
||||
err = page.WaitLoad()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 提取搜索结果
|
||||
resultElements, err := page.Elements(".result, .c-container")
|
||||
if err != nil || resultElements == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, result := range resultElements {
|
||||
// 获取标题
|
||||
titleElement, err := result.Element("h3, .t")
|
||||
if err != nil || titleElement == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
title, err := titleElement.Text()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取 URL
|
||||
linkElement, err := titleElement.Element("a")
|
||||
if err != nil || linkElement == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
href, err := linkElement.Attribute("href")
|
||||
if err != nil || href == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取内容摘要 - 尝试多个可能的选择器
|
||||
var contentElement *rod.Element
|
||||
var content string
|
||||
|
||||
// 尝试多个可能的选择器来适应不同版本的百度搜索结果
|
||||
selectors := []string{".content-right_8Zs40", ".c-abstract", ".content_LJ0WN", ".content"}
|
||||
for _, selector := range selectors {
|
||||
contentElement, err = result.Element(selector)
|
||||
if err == nil && contentElement != nil {
|
||||
content, _ = contentElement.Text()
|
||||
if content != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有选择器都失败,尝试直接从结果块中提取文本
|
||||
if content == "" {
|
||||
// 获取结果元素的所有文本
|
||||
fullText, err := result.Text()
|
||||
if err == nil && fullText != "" {
|
||||
// 简单处理:从全文中移除标题,剩下的可能是摘要
|
||||
fullText = strings.Replace(fullText, title, "", 1)
|
||||
// 清理文本
|
||||
content = strings.TrimSpace(fullText)
|
||||
// 限制内容长度
|
||||
if len(content) > 200 {
|
||||
content = content[:200] + "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到结果集
|
||||
results = append(results, SearchResult{
|
||||
Title: title,
|
||||
URL: *href,
|
||||
Content: content,
|
||||
})
|
||||
|
||||
// 限制结果数量,每页最多 10 条
|
||||
if len(results) >= 10*maxPages {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取真实 URL(百度搜索结果中的 URL 是短链接,需要跳转获取真实 URL)
|
||||
for i, result := range results {
|
||||
realURL, err := s.getRedirectURL(result.URL)
|
||||
if err == nil && realURL != "" {
|
||||
results[i].URL = realURL
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// 获取真实 URL
|
||||
func (s *Service) getRedirectURL(shortURL string) (string, error) {
|
||||
// 创建页面
|
||||
page, err := s.browser.Page(proto.TargetCreateTarget{URL: ""})
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
defer func() {
|
||||
_ = page.Close()
|
||||
}()
|
||||
|
||||
// 导航到短链接
|
||||
err = page.Navigate(shortURL)
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
|
||||
// 等待重定向完成
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 获取当前 URL
|
||||
info, err := page.Info()
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
|
||||
return info.URL, nil
|
||||
}
|
||||
|
||||
// Close 关闭浏览器
|
||||
func (s *Service) Close() error {
|
||||
if s.browser != nil {
|
||||
err := s.browser.Close()
|
||||
s.browser = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchWeb 封装的搜索方法
|
||||
func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
// 添加panic恢复机制
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log := logger.GetLogger()
|
||||
log.Errorf("爬虫服务崩溃: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
service, err := NewService()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建爬虫服务失败: %v", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
// 设置超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 使用goroutine和通道来处理超时
|
||||
resultChan := make(chan []SearchResult, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
results, err := service.WebSearch(keyword, maxPages)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
resultChan <- results
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", fmt.Errorf("搜索超时: %v", ctx.Err())
|
||||
case err := <-errChan:
|
||||
return "", fmt.Errorf("搜索失败: %v", err)
|
||||
case results := <-resultChan:
|
||||
if len(results) == 0 {
|
||||
return "未找到关于 \"" + keyword + "\" 的相关搜索结果", nil
|
||||
}
|
||||
|
||||
// 格式化结果
|
||||
var builder strings.Builder
|
||||
builder.WriteString(fmt.Sprintf("为您找到关于 \"%s\" 的 %d 条搜索结果:\n\n", keyword, len(results)))
|
||||
|
||||
for i, result := range results {
|
||||
// // 尝试打开链接获取实际内容
|
||||
// page := service.browser.MustPage()
|
||||
// defer page.MustClose()
|
||||
|
||||
// // 设置页面超时
|
||||
// pageCtx, pageCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
// defer pageCancel()
|
||||
|
||||
// // 导航到目标页面
|
||||
// err := page.Context(pageCtx).Navigate(result.URL)
|
||||
// if err == nil {
|
||||
// // 等待页面加载
|
||||
// _ = page.WaitLoad()
|
||||
|
||||
// // 获取页面标题
|
||||
// title, err := page.Eval("() => document.title")
|
||||
// if err == nil && title.Value.String() != "" {
|
||||
// result.Title = title.Value.String()
|
||||
// }
|
||||
|
||||
// // 获取页面主要内容
|
||||
// if content, err := page.Element("body"); err == nil {
|
||||
// if text, err := content.Text(); err == nil {
|
||||
// // 清理并截取内容
|
||||
// text = strings.TrimSpace(text)
|
||||
// if len(text) > 200 {
|
||||
// text = text[:200] + "..."
|
||||
// }
|
||||
// result.Prompt = text
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, result.Title))
|
||||
builder.WriteString(fmt.Sprintf(" 链接: %s\n", result.URL))
|
||||
if result.Content != "" {
|
||||
builder.WriteString(fmt.Sprintf(" 摘要: %s\n", result.Content))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
}
|
||||
@@ -122,15 +122,6 @@ type ErrRes struct {
|
||||
|
||||
func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
logger.Debugf("绘画参数:%+v", task)
|
||||
prompt := task.Prompt
|
||||
// translate prompt
|
||||
if utils.HasChinese(prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, prompt), task.TranslateModelId)
|
||||
if err == nil {
|
||||
prompt = content
|
||||
logger.Debugf("重写后提示词:%s", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
var chatModel model.ChatModel
|
||||
if task.ModelId > 0 {
|
||||
@@ -160,7 +151,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
|
||||
reqBody := imgReq{
|
||||
Model: chatModel.Value,
|
||||
Prompt: prompt,
|
||||
Prompt: task.Prompt,
|
||||
N: 1,
|
||||
Size: task.Size,
|
||||
Style: task.Style,
|
||||
@@ -188,7 +179,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
var imgURL string
|
||||
var data = map[string]interface{}{
|
||||
"progress": 100,
|
||||
"prompt": prompt,
|
||||
"prompt": task.Prompt,
|
||||
}
|
||||
// 如果返回的是base64,则需要上传到oss
|
||||
if res.Data[0].B64Json != "" {
|
||||
@@ -214,7 +205,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n\n", prompt, imgURL)
|
||||
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n\n", task.Prompt, imgURL)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"geekai/core/types"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
@@ -191,8 +192,8 @@ func (s *MigrationService) migrateCommunicationConfig(config *types.AppConfig) e
|
||||
|
||||
// 短信配置
|
||||
smsConfig := map[string]any{
|
||||
"active": config.SMS.Active,
|
||||
"ali": map[string]any{
|
||||
"active": strings.ToLower(config.SMS.Active),
|
||||
"aliyun": map[string]any{
|
||||
"access_key": config.SMS.Ali.AccessKey,
|
||||
"access_secret": config.SMS.Ali.AccessSecret,
|
||||
"sign": config.SMS.Ali.Sign,
|
||||
|
||||
@@ -67,25 +67,6 @@ func (s *Service) Run() {
|
||||
continue
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId)
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
// translate negative prompt
|
||||
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt), task.TranslateModelId)
|
||||
if err == nil {
|
||||
task.NegPrompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// use fast mode as default
|
||||
if task.Mode == "" {
|
||||
task.Mode = "fast"
|
||||
|
||||
@@ -72,7 +72,7 @@ func (s MiniOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string,
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(parse.Path)
|
||||
}
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), ext)
|
||||
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), ext)
|
||||
info, err := s.client.PutObject(
|
||||
context.Background(),
|
||||
s.config.Bucket,
|
||||
@@ -99,7 +99,7 @@ func (s MiniOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
defer fileReader.Close()
|
||||
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||
info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
|
||||
ContentType: file.Header.Get("Body-Type"),
|
||||
})
|
||||
@@ -121,7 +121,7 @@ func (s MiniOss) PutBase64(base64Img string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding base64:%v", err)
|
||||
}
|
||||
objectKey := fmt.Sprintf("%s/%d.png", s.config.SubDir, time.Now().UnixMicro())
|
||||
objectKey := fmt.Sprintf("%d.png", time.Now().UnixMicro())
|
||||
info, err := s.client.PutObject(
|
||||
context.Background(),
|
||||
s.config.Bucket,
|
||||
@@ -138,8 +138,7 @@ func (s MiniOss) PutBase64(base64Img string) (string, error) {
|
||||
func (s MiniOss) Delete(fileURL string) error {
|
||||
var objectKey string
|
||||
if strings.HasPrefix(fileURL, "http") {
|
||||
filename := filepath.Base(fileURL)
|
||||
objectKey = fmt.Sprintf("%s/%s", s.config.SubDir, filename)
|
||||
objectKey = filepath.Base(fileURL)
|
||||
} else {
|
||||
objectKey = fileURL
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (s QiNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
defer src.Close()
|
||||
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
key := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||
// 上传文件
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
@@ -112,7 +112,7 @@ func (s QiNiuOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string,
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(parse.Path)
|
||||
}
|
||||
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), ext)
|
||||
key := fmt.Sprintf("%d%s", time.Now().UnixMicro(), ext)
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
// 上传文件字节数据
|
||||
@@ -128,7 +128,7 @@ func (s QiNiuOss) PutBase64(base64Img string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding base64:%v", err)
|
||||
}
|
||||
objectKey := fmt.Sprintf("%s/%d.png", s.config.SubDir, time.Now().UnixMicro())
|
||||
objectKey := fmt.Sprintf("%d.png", time.Now().UnixMicro())
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
// 上传文件字节数据
|
||||
@@ -142,8 +142,7 @@ func (s QiNiuOss) PutBase64(base64Img string) (string, error) {
|
||||
func (s QiNiuOss) Delete(fileURL string) error {
|
||||
var objectKey string
|
||||
if strings.HasPrefix(fileURL, "http") {
|
||||
filename := filepath.Base(fileURL)
|
||||
objectKey = fmt.Sprintf("%s/%s", s.config.SubDir, filename)
|
||||
objectKey = filepath.Base(fileURL)
|
||||
} else {
|
||||
objectKey = fileURL
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ package resp
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SUCCESS(c *gin.Context, values ...interface{}) {
|
||||
|
||||
@@ -223,14 +223,19 @@ const items = [
|
||||
title: '存储配置',
|
||||
},
|
||||
{
|
||||
icon: 'log',
|
||||
index: '/admin/config/communication',
|
||||
title: '通信配置',
|
||||
icon: 'sms',
|
||||
index: '/admin/config/sms',
|
||||
title: '短信配置',
|
||||
},
|
||||
{
|
||||
icon: 'api-key',
|
||||
index: '/admin/config/api',
|
||||
title: 'API配置',
|
||||
icon: 'email',
|
||||
index: '/admin/config/smtp',
|
||||
title: '邮件配置',
|
||||
},
|
||||
{
|
||||
icon: 'plugin',
|
||||
index: '/admin/config/plugin',
|
||||
title: '插件配置',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
47
web/src/components/ui/Alert.vue
Normal file
47
web/src/components/ui/Alert.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="rounded-md p-3 border-2 text-base mb-4 flex items-center" :class="typeClass">
|
||||
<div :class="textColor">
|
||||
<slot name="icon">
|
||||
<i class="iconfont !text-xl" :class="typeIcon"></i>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="ml-3 text-sm leading-relaxed">
|
||||
<slot>{{ message }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
},
|
||||
})
|
||||
|
||||
const typeClass = computed(() => {
|
||||
return {
|
||||
info: 'bg-blue-100 text-blue-500 border-blue-500',
|
||||
success: 'bg-green-100 text-green-500 border-green-500',
|
||||
warning: 'bg-yellow-100 text-yellow-500 border-yellow-500',
|
||||
}[props.type]
|
||||
})
|
||||
const typeIcon = computed(() => {
|
||||
return {
|
||||
info: 'icon-info',
|
||||
success: 'icon-success',
|
||||
warning: 'icon-warning',
|
||||
}[props.type]
|
||||
})
|
||||
const textColor = computed(() => {
|
||||
return {
|
||||
info: 'text-blue-500',
|
||||
success: 'text-green-500',
|
||||
warning: 'text-yellow-500',
|
||||
}[props.type]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -192,16 +192,22 @@ const routes = [
|
||||
component: () => import('@/views/admin/settings/StorageConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/config/communication',
|
||||
name: 'admin-config-communication',
|
||||
meta: { title: '通信配置' },
|
||||
component: () => import('@/views/admin/settings/CommunicationConfig.vue'),
|
||||
path: '/admin/config/sms',
|
||||
name: 'admin-config-sms',
|
||||
meta: { title: '短信配置' },
|
||||
component: () => import('@/views/admin/settings/SmsConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/config/api',
|
||||
name: 'admin-config-api',
|
||||
meta: { title: 'API配置' },
|
||||
component: () => import('@/views/admin/settings/ApiConfig.vue'),
|
||||
path: '/admin/config/smtp',
|
||||
name: 'admin-config-smtp',
|
||||
meta: { title: '邮件配置' },
|
||||
component: () => import('@/views/admin/settings/SmtpConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/config/plugin',
|
||||
name: 'admin-config-plugin',
|
||||
meta: { title: '插件配置' },
|
||||
component: () => import('@/views/admin/settings/PluginConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/config/markmap',
|
||||
|
||||
@@ -5,8 +5,32 @@
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import axios from 'axios'
|
||||
import { getAdminToken, getUserToken, removeAdminToken, removeUserToken } from '@/store/session'
|
||||
import axios from 'axios'
|
||||
|
||||
// Blob 数据读取和解析的辅助函数
|
||||
export async function parseBlobResponse(blob) {
|
||||
try {
|
||||
// 检查 Blob 的类型
|
||||
if (blob.type && blob.type.includes('application/json')) {
|
||||
// 如果是 JSON 类型,直接解析
|
||||
const text = await blob.text()
|
||||
return JSON.parse(text)
|
||||
} else {
|
||||
// 如果不是 JSON 类型,尝试解析为文本
|
||||
const text = await blob.text()
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (e) {
|
||||
// 如果解析 JSON 失败,返回文本内容
|
||||
return { message: text, rawData: text }
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析 Blob 响应失败:', error)
|
||||
return { message: '解析响应数据失败', error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
axios.defaults.timeout = 180000
|
||||
// axios.defaults.baseURL = process.env.VUE_APP_API_HOST
|
||||
@@ -29,7 +53,7 @@ axios.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
async (error) => {
|
||||
if (error.response.status === 401) {
|
||||
if (error.response.request.responseURL.indexOf('/api/admin') !== -1) {
|
||||
removeAdminToken()
|
||||
@@ -39,8 +63,14 @@ axios.interceptors.response.use(
|
||||
error.response.data.message = '请先登录'
|
||||
return Promise.reject(error.response.data)
|
||||
}
|
||||
|
||||
if (error.response.status === 400) {
|
||||
return Promise.reject(new Error(error.response.data.message))
|
||||
let errorMessage = error.response.data.message
|
||||
if (!errorMessage) {
|
||||
const parsedData = await parseBlobResponse(error.response.data)
|
||||
errorMessage = parsedData.message
|
||||
}
|
||||
return Promise.reject(new Error(errorMessage))
|
||||
} else {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<div class="reset-pass"></div>
|
||||
<div class="flex-center loginPage">
|
||||
<div class="left">
|
||||
<div class="login-box">
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<div class="form" v-loading="loading">
|
||||
<el-tabs v-model="active">
|
||||
<el-tab-pane label="SMTP 邮件" name="smtp">
|
||||
<el-form :model="smtp" label-width="140px">
|
||||
<el-form-item label="启用TLS"><el-switch v-model="smtp.use_tls" /></el-form-item>
|
||||
<el-form-item label="SMTP服务器"><el-input v-model="smtp.host" /></el-form-item>
|
||||
<el-form-item label="端口"><el-input-number v-model="smtp.port" :min="1" /></el-form-item>
|
||||
<el-form-item label="应用名称"><el-input v-model="smtp.app_name" /></el-form-item>
|
||||
<el-form-item label="发件人地址"><el-input v-model="smtp.from" /></el-form-item>
|
||||
<el-form-item label="发件人密码"
|
||||
><el-input v-model="smtp.password" type="password"
|
||||
/></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="save('smtp')">保存</el-button>
|
||||
<el-button @click="test('smtp')">测试</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="短信服务" name="sms">
|
||||
<el-form :model="sms" label-width="140px">
|
||||
<el-form-item label="服务商">
|
||||
<el-select v-model="sms.Active" style="width: 200px">
|
||||
<el-option label="阿里云" value="Ali" />
|
||||
<el-option label="短信宝" value="Bao" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="sms.Active === 'Ali'">
|
||||
<el-form-item label="AccessKey"><el-input v-model="sms.Ali.AccessKey" /></el-form-item>
|
||||
<el-form-item label="AccessSecret"
|
||||
><el-input v-model="sms.Ali.AccessSecret"
|
||||
/></el-form-item>
|
||||
<el-form-item label="签名"><el-input v-model="sms.Ali.Sign" /></el-form-item>
|
||||
<el-form-item label="模板ID"><el-input v-model="sms.Ali.CodeTempId" /></el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item label="用户名"><el-input v-model="sms.Bao.Username" /></el-form-item>
|
||||
<el-form-item label="密码"
|
||||
><el-input v-model="sms.Bao.Password" type="password"
|
||||
/></el-form-item>
|
||||
<el-form-item label="签名"><el-input v-model="sms.Bao.Sign" /></el-form-item>
|
||||
<el-form-item label="模板"
|
||||
><el-input v-model="sms.Bao.CodeTemplate" type="textarea" :rows="2"
|
||||
/></el-form-item>
|
||||
</template>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="save('sms')">保存</el-button>
|
||||
<el-button @click="test('sms')">测试</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const active = ref('smtp')
|
||||
const smtp = ref({ use_tls: false, host: '', port: 25, app_name: '', from: '', password: '' })
|
||||
const sms = ref({
|
||||
Active: 'Ali',
|
||||
Ali: { AccessKey: '', AccessSecret: '', Sign: '', CodeTempId: '' },
|
||||
Bao: { Username: '', Password: '', Sign: '', CodeTemplate: '' },
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
Promise.all([httpGet('/api/admin/config/get?key=smtp'), httpGet('/api/admin/config/get?key=sms')])
|
||||
.then(([s1, s2]) => {
|
||||
const smtpData = s1?.data || {}
|
||||
smtp.value = { ...smtp.value, ...smtpData }
|
||||
|
||||
const smsData = s2?.data || {}
|
||||
sms.value = {
|
||||
...sms.value,
|
||||
...smsData,
|
||||
Ali: { ...sms.value.Ali, ...(smsData.Ali || smsData.ali || {}) },
|
||||
Bao: { ...sms.value.Bao, ...(smsData.Bao || smsData.bao || {}) },
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => (loading.value = false))
|
||||
})
|
||||
|
||||
const save = (key) => {
|
||||
if (key === 'smtp') {
|
||||
httpPost('/api/admin/config/update/smtp', smtp.value)
|
||||
.then(() => ElMessage.success('保存成功'))
|
||||
.catch((e) => ElMessage.error(e.message))
|
||||
} else if (key === 'sms') {
|
||||
httpPost('/api/admin/config/update/sms', sms.value)
|
||||
.then(() => ElMessage.success('保存成功'))
|
||||
.catch((e) => ElMessage.error(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
const test = (key) => {
|
||||
ElMessage.info('请在对应服务侧进行验证')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form {
|
||||
padding: 10px 20px 40px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -8,14 +8,14 @@
|
||||
<span class="ms-2">支付宝</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/payment.html#%E6%94%AF%E4%BB%98%E5%AE%9D%E9%85%8D%E7%BD%AE"
|
||||
target="_blank"
|
||||
>支付宝配置</a
|
||||
>。
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<el-form :model="alipay" class="mt-4" label-position="top">
|
||||
<el-form-item label="商户ID"><el-input v-model="alipay.app_id" /></el-form-item>
|
||||
@@ -52,14 +52,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/payment.html#%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E9%85%8D%E7%BD%AE"
|
||||
target="_blank"
|
||||
>微信支付配置</a
|
||||
>。
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<el-form :model="wxpay" class="mt-4" label-position="top">
|
||||
<el-form-item label="AppID"><el-input v-model="wxpay.app_id" /></el-form-item>
|
||||
@@ -89,14 +89,14 @@
|
||||
<span class="ms-2">易支付</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/payment.html#%E6%98%93%E6%94%AF%E4%BB%98%E5%BC%80%E9%80%9A"
|
||||
target="_blank"
|
||||
>易支付配置</a
|
||||
>。
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<el-form :model="epay" class="mt-4" label-position="top">
|
||||
<el-form-item label="商户ID"><el-input v-model="epay.app_id" /></el-form-item>
|
||||
@@ -114,6 +114,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Alert from '@/components/ui/Alert.vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
159
web/src/views/admin/settings/PluginConfig.vue
Normal file
159
web/src/views/admin/settings/PluginConfig.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="settings container p-5">
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<el-tab-pane name="captcha">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<i class="iconfont icon-yanzm"></i>
|
||||
<span class="ml-2">行为验证码</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Alert type="info">
|
||||
行为验证码服务,开启后用户登录的时候需要进行行为验证,可以有效防止恶意登录。<br />
|
||||
请联系作者获取令牌填入下面输入框开通验证服务。
|
||||
</Alert>
|
||||
|
||||
<el-form :model="captchaConfig" label-position="top">
|
||||
<el-form-item label="服务令牌">
|
||||
<el-input v-model="captchaConfig.api_key" placeholder="请输入服务令牌" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="验证码类型">
|
||||
<el-radio-group v-model="captchaConfig.type" size="large">
|
||||
<el-radio value="dot" border>点选验证码</el-radio>
|
||||
<el-radio value="slide" border>滑动验证码</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用验证码">
|
||||
<el-switch size="large" v-model="captchaConfig.enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<el-button type="primary" @click="saveCaptchaConfig" :loading="loading"
|
||||
>提交保存</el-button
|
||||
>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="wxlogin">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<i class="iconfont icon-wechat"></i>
|
||||
<span class="ml-2">微信登录</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Alert type="info">
|
||||
微信登录服务,开启后用户可以使用微信扫码登录。<br />
|
||||
请联系作者获取令牌填入下面输入框开通验证服务。
|
||||
</Alert>
|
||||
|
||||
<el-form :model="wxLoginConfig" label-position="top">
|
||||
<el-form-item label="服务令牌">
|
||||
<el-input v-model="wxLoginConfig.api_key" placeholder="请输入服务令牌" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录成功回调URL">
|
||||
<el-input v-model="wxLoginConfig.notify_url" placeholder="请输入回调URL" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用微信登录">
|
||||
<el-switch size="large" v-model="wxLoginConfig.enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<el-button type="primary" @click="saveWxloginConfig" :loading="loading"
|
||||
>提交保存</el-button
|
||||
>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Alert from '@/components/ui/Alert.vue'
|
||||
import { httpGet, httpPost } from '@/utils/http.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const activeTab = ref('captcha')
|
||||
const loading = ref(false)
|
||||
|
||||
// 行为验证码配置
|
||||
const captchaConfig = ref({
|
||||
api_key: '',
|
||||
type: 'slide',
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
// 微信登录配置
|
||||
const wxLoginConfig = ref({
|
||||
api_key: '',
|
||||
notify_url: '',
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchCaptchaConfig()
|
||||
fetchWxloginConfig()
|
||||
})
|
||||
|
||||
const fetchCaptchaConfig = async () => {
|
||||
try {
|
||||
const res = await httpGet('/api/admin/config/get?key=captcha')
|
||||
captchaConfig.value = Object.assign(captchaConfig.value, res.data)
|
||||
} catch (e) {
|
||||
// 使用默认值
|
||||
}
|
||||
}
|
||||
|
||||
const fetchWxloginConfig = async () => {
|
||||
try {
|
||||
const res = await httpGet('/api/admin/config/get?key=wx_login')
|
||||
wxLoginConfig.value = Object.assign(wxLoginConfig.value, res.data)
|
||||
wxLoginConfig.value.notify_url =
|
||||
wxLoginConfig.value.notify_url || window.location.origin + '/api/user/login/callback'
|
||||
} catch (e) {
|
||||
// 使用默认值
|
||||
}
|
||||
}
|
||||
|
||||
// 保存行为验证码配置
|
||||
const saveCaptchaConfig = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await httpPost('/api/admin/config/update/captcha', captchaConfig.value)
|
||||
ElMessage.success('保存成功')
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败:' + (e.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存微信登录配置
|
||||
const saveWxloginConfig = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await httpPost('/api/admin/config/update/wx_login', wxLoginConfig.value)
|
||||
ElMessage.success('保存成功')
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败:' + (e.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settings {
|
||||
.el-form-item__label {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
web/src/views/admin/settings/SmsConfig.vue
Normal file
143
web/src/views/admin/settings/SmsConfig.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="settings container p-5">
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<el-tab-pane label="阿里云短信" name="aliyun">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<i class="iconfont icon-aliyun"></i>
|
||||
<span class="ml-2">阿里云短信</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/sms.html#%E9%98%BF%E9%87%8C%E4%BA%91"
|
||||
target="_blank"
|
||||
>阿里云短信配置</a
|
||||
>。
|
||||
</Alert>
|
||||
|
||||
<el-form :model="configs.aliyun" label-position="top">
|
||||
<el-form-item label="AccessKey">
|
||||
<el-input v-model="configs.aliyun.access_key" placeholder="请输入AccessKey" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AccessSecret">
|
||||
<el-input v-model="configs.aliyun.access_secret" placeholder="请输入AccessSecret" />
|
||||
</el-form-item>
|
||||
<el-form-item label="短信签名">
|
||||
<el-input v-model="configs.aliyun.sign" placeholder="请输入短信签名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码模板ID">
|
||||
<el-input v-model="configs.aliyun.code_temp_id" placeholder="请输入验证码模板ID" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="短信宝" name="bao">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<i class="iconfont icon-sms"></i>
|
||||
<span class="ml-2">短信宝</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/sms.html#%E7%9F%AD%E4%BF%A1%E5%AE%9D"
|
||||
target="_blank"
|
||||
>短信宝配置</a
|
||||
>。
|
||||
</Alert>
|
||||
|
||||
<el-form :model="configs.bao" label-position="top">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="configs.bao.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="configs.bao.password" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="短信签名">
|
||||
<el-input v-model="configs.bao.sign" placeholder="请输入短信签名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码模板">
|
||||
<el-input v-model="configs.bao.code_template" placeholder="请输入验证码模板" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label mr-2">默认使用</label>
|
||||
<el-radio-group v-model="configs.active" size="large">
|
||||
<el-radio value="aliyun" border>阿里云</el-radio>
|
||||
<el-radio value="bao" border>短信宝</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<el-button type="primary" @click="saveSmsConfig" :loading="loading">提交保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Alert from '@/components/ui/Alert.vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('aliyun')
|
||||
const configs = ref({
|
||||
active: 'aliyun',
|
||||
aliyun: {
|
||||
access_key: '',
|
||||
access_secret: '',
|
||||
sign: '',
|
||||
code_temp_id: '',
|
||||
},
|
||||
bao: {
|
||||
username: '',
|
||||
password: '',
|
||||
sign: '',
|
||||
code_template: '',
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await httpGet('/api/admin/config/get?key=sms')
|
||||
configs.value = Object.assign(configs.value, res.data)
|
||||
} catch (e) {
|
||||
// 使用默认值
|
||||
}
|
||||
})
|
||||
|
||||
const saveSmsConfig = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await httpPost('/api/admin/config/update/sms', configs.value)
|
||||
ElMessage.success('保存成功')
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败:' + (e.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settings {
|
||||
a {
|
||||
color: #409eff;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.el-form-item__label {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
140
web/src/views/admin/settings/SmtpConfig.vue
Normal file
140
web/src/views/admin/settings/SmtpConfig.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="settings container p-5">
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<el-tab-pane label="邮件服务设置" name="smtp">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/sms.html#%E9%82%AE%E4%BB%B6%E6%9C%8D%E5%8A%A1%E9%85%8D%E7%BD%AE"
|
||||
target="_blank"
|
||||
>邮件服务配置</a
|
||||
>。
|
||||
</Alert>
|
||||
|
||||
<el-form :model="smtpConfig" label-position="top">
|
||||
<el-form-item>
|
||||
<label class="form-label"
|
||||
>邮件服务器地址
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
推荐使用网易邮箱,<br />
|
||||
国外邮箱推荐使用 Google 邮箱
|
||||
</template>
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</label>
|
||||
<el-input
|
||||
v-model="smtpConfig.host"
|
||||
placeholder="请输入邮件服务器地址,推荐使用网易邮箱"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<label class="form-label"
|
||||
>邮件服务器端口
|
||||
<el-tooltip placement="top">
|
||||
<template #content> 线上推荐使用465端口,<br />本地测试推荐使用25端口 </template>
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</label>
|
||||
<el-input v-model="smtpConfig.port" type="number" placeholder="请输入端口号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否使用TLS"
|
||||
><el-switch v-model="smtpConfig.use_tls"
|
||||
/></el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<label class="form-label"
|
||||
>应用名称
|
||||
<el-tooltip placement="top">
|
||||
<template #content> 应用名称会显示在邮件的抬头 </template>
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</label>
|
||||
<el-input v-model="smtpConfig.app_name" placeholder="请输入应用名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<label class="form-label">发件人邮箱地址</label>
|
||||
<el-input v-model="smtpConfig.from" type="email" placeholder="请输入发件人邮箱地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<label class="form-label"
|
||||
>发件人邮箱密码
|
||||
<el-tooltip placement="top">
|
||||
<template #content> 如果使用授权码,请输入授权码 </template>
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</label>
|
||||
<el-input
|
||||
v-model="smtpConfig.password"
|
||||
type="password"
|
||||
placeholder="请输入邮箱密码或授权码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<el-button type="primary" @click="saveConfig" :loading="loading">提交保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Alert from '@/components/ui/Alert.vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('smtp')
|
||||
const smtpConfig = ref({
|
||||
use_tls: false,
|
||||
host: 'smtp.163.com',
|
||||
port: 465,
|
||||
app_name: 'GeekAI',
|
||||
from: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await httpGet('/api/admin/config/get?key=smtp')
|
||||
smtpConfig.value = Object.assign(smtpConfig.value, res.data || {})
|
||||
} catch (e) {
|
||||
// 使用默认值
|
||||
}
|
||||
})
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await httpPost('/api/admin/config/update/smtp', smtpConfig.value)
|
||||
ElMessage.success('保存成功')
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败:' + (e.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settings {
|
||||
a {
|
||||
color: #409eff;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.el-form-item__label {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -59,14 +59,39 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="七牛云" name="qiniu">
|
||||
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/oss.html#%E4%B8%83%E7%89%9B%E4%BA%91-oss-%E9%85%8D%E7%BD%AE"
|
||||
target="_blank"
|
||||
>七牛云配置</a
|
||||
>。
|
||||
</div>
|
||||
<el-form :model="qiniu" class="mt-4" label-position="top">
|
||||
<el-form-item label="Zone"><el-input v-model="qiniu.zone" /></el-form-item>
|
||||
<el-form-item label="Zone">
|
||||
<template #label>
|
||||
<label
|
||||
>区域(Zone)
|
||||
<el-tooltip
|
||||
placement="right"
|
||||
content="华南:z2,华东:z0,华北:z1,北美:na0,新加坡:as0"
|
||||
>
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</label>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="qiniu.zone"
|
||||
placeholder="华南:z2,华东:z0,华北:z1,北美:na0,新加坡:as0"
|
||||
/></el-form-item>
|
||||
<el-form-item label="AccessKey"><el-input v-model="qiniu.access_key" /></el-form-item>
|
||||
<el-form-item label="AccessSecret"
|
||||
><el-input v-model="qiniu.access_secret"
|
||||
/></el-form-item>
|
||||
<el-form-item label="Bucket"><el-input v-model="qiniu.bucket" /></el-form-item>
|
||||
<el-form-item label="Domain"><el-input v-model="qiniu.domain" /></el-form-item>
|
||||
<el-form-item label="Domain"
|
||||
><el-input v-model="qiniu.domain" placeholder="请输入七牛云Bucket绑定的域名"
|
||||
/></el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -95,7 +120,6 @@
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<el-button type="primary" @click="save" :loading="loading">提交保存</el-button>
|
||||
<el-button class="ml-3" @click="test">连接测试</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -161,14 +185,16 @@ const save = () => {
|
||||
.then(() => ElMessage.success('保存成功'))
|
||||
.catch((e) => ElMessage.error(e.message))
|
||||
}
|
||||
|
||||
const test = () => {
|
||||
ElMessage.info('请在对象存储端验证配置')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settings {
|
||||
a {
|
||||
color: #409eff;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.el-form-item__label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user