Compare commits

...

10 Commits

58 changed files with 2680 additions and 615 deletions

51
.air.toml Normal file
View File

@ -0,0 +1,51 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = "./tmp/main picgo ./deploy/picgo-dev.yml"
include_dir = []
include_ext = ["go", "html", "js", "css", "yml"]
include_file = []
kill_delay = "1s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = true
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

6
.gitignore vendored
View File

@ -171,7 +171,8 @@ dist
.LSOverride
# Icon must end with two \r
Icon
Icon
# Thumbnails
._*
@ -218,3 +219,6 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
.idea
tmp

111
cmd/picgo.go Normal file
View File

@ -0,0 +1,111 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"log"
"net/http"
"os"
"picgo/configs"
"picgo/corelib"
"picgo/model"
"picgo/router"
"strings"
)
var rootCmd = &cobra.Command{
Use: "root",
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
fmt.Println("args:")
fmt.Println(strings.Join(args, " "))
if len(os.Args) < 1 {
log.Panic("请指定配置文件路径")
return
}
configFile := args[0]
fmt.Println("配置文件路径: ", configFile)
// 初始化配置文件
configs.NewConfig(configFile)
// 初始化日志
corelib.NewLogger()
// 初始化redis
corelib.NewRedis()
// 初始化mysql
corelib.NewMysql()
// 初始化session store
corelib.NewSessionStore()
return
},
}
var picgoCmd = &cobra.Command{
Use: "picgo",
Run: func(cmd *cobra.Command, args []string) {
// 创建路由
router.InitRouter()
// 启动HTTP服务器
corelib.Logger.Infoln("Serving on http://", fmt.Sprintf("%s:%d", configs.Settings.Server.Host, configs.Settings.Server.Port))
err := http.ListenAndServe(fmt.Sprintf("%s:%d", configs.Settings.Server.Host, configs.Settings.Server.Port), nil)
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
},
}
var MigrateCmd = &cobra.Command{
Use: "migrate",
Run: func(cmd *cobra.Command, args []string) {
var err error
// 自动迁移数据库
if err = corelib.DbMysql.AutoMigrate(&model.SysUser{}); err != nil {
panic("SysUser 迁移数据库失败")
}
if err = corelib.DbMysql.AutoMigrate(&model.Domain{}); err != nil {
panic("Domain 迁移数据库失败")
}
if err = corelib.DbMysql.AutoMigrate(&model.Upload{}); err != nil {
panic("Upload 迁移数据库失败")
}
// 初始化管理员用户
err = initSysUser()
if err != nil {
fmt.Println("初始化管理员用户失败", err)
}
},
}
func init() {
rootCmd.AddCommand(picgoCmd)
rootCmd.AddCommand(MigrateCmd)
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
return
}
}
// 初始化超级管理员
func initSysUser() error {
var (
hashPassword string
err error
)
userName := "admin001"
password := "Admin#123456"
salt := corelib.GenerateSalt()
if hashPassword, err = corelib.HashPassword(password, salt); err != nil {
corelib.Logger.Errorln("initSystem 生成密码失败: ", err)
return err
}
user := model.SysUser{Username: userName, Password: hashPassword, Salt: salt, IsSuper: 1}
if err = corelib.DbMysql.Create(&user).Error; err != nil {
corelib.Logger.Errorln("initSystem 初始化管理员用户失败: ", err)
return err
}
return nil
}

8
configs/captcha.go Normal file
View File

@ -0,0 +1,8 @@
package configs
type Captcha struct {
Expire int `yaml:"expire"`
Width int `yaml:"width"`
Hight int `yaml:"hight"`
Length int `yaml:"length"`
}

40
configs/configs.go Normal file
View File

@ -0,0 +1,40 @@
package configs
import (
"gopkg.in/yaml.v3"
"log"
"os"
)
type Configs struct {
Server `yaml:"server"`
Redis `yaml:"redis"`
Mysql `yaml:"mysql"`
Captcha `json:"captcha"`
}
// Settings 定义配置结构体
var Settings Configs
func readYamlFile(configFilePath string) (yamlFile []byte, err error) {
// 读取YAML文件
if yamlFile, err = os.ReadFile(configFilePath); err != nil {
return nil, err
}
return yamlFile, err
}
func NewConfig(configFilePath string) {
var (
yamlFile []byte
err error
)
// 读取配置文件
if yamlFile, err = readYamlFile(configFilePath); err != nil {
log.Panic("读取配置文件失败: ", err)
}
// 解析YAML数据到配置结构体
if err = yaml.Unmarshal(yamlFile, &Settings); err != nil {
log.Panic("解析配置文件失败: ", err)
}
}

7
configs/mysql.go Normal file
View File

@ -0,0 +1,7 @@
package configs
type Mysql struct {
MysqlDNS string `yaml:"mysql_dns"`
MaxIdleConns int `yaml:"max_idle_conns"`
MaxOpenConns int `yaml:"max_open_conns"`
}

9
configs/redis.go Normal file
View File

@ -0,0 +1,9 @@
package configs
type Redis struct {
Addr string `yaml:"addr"`
Password string `yaml:"password"`
DB int `yaml:"db"`
PoolSize int `yaml:"pool_size"`
MinidleConns int `yaml:"minidle_conns"`
}

14
configs/server.go Normal file
View File

@ -0,0 +1,14 @@
package configs
type Server struct {
Port int64 `yaml:"port"`
Host string `yaml:"host"`
NodeId int64 `yaml:"node_id"`
LogPath string `yaml:"log_path"`
Environment string `yaml:"environment"`
StaticPath string `yaml:"static_path"`
UploadPath string `yaml:"upload_path"`
UploadMaxSize int64 `yaml:"upload_max_size"`
SessionsKey string `yaml:"sessions_key"`
SessionName string `yaml:"session_name"`
}

View File

@ -0,0 +1,68 @@
package captcha
import (
"image"
"os"
"path/filepath"
"picgo/configs"
"picgo/corelib"
"strings"
)
// GenerateCaptcha 图形验证码
func GenerateCaptcha(cid string) (img *image.RGBA, err error) {
// 字体路径
dir, err := os.Getwd()
if err != nil {
return
}
var fontPath = filepath.Join(dir, "corelib", "captcha", "fonts")
// 随机字体
var fontNames = []string{"DENNEthree-dee", "3Dumb"}
fontName := RandChooseOne(fontNames)
// 随机模式
var modes = []int{0, 1}
mode := RandChooseOne(modes)
// 生成图片验证码
captchaLen := configs.Settings.Captcha.Length
w := configs.Settings.Captcha.Width
h := configs.Settings.Captcha.Hight
cp := NewCaptcha(w, h, captchaLen)
cp.SetFontPath(fontPath) //指定字体目录
cp.SetFontName(fontName) //指定字体名字
cp.SetMode(mode) //1设置为简单的数学算术运算公式 其他为普通字符串
code, img := cp.OutPut()
// 备注code 存储到redis并在使用时取出验证
corelib.Logger.Infoln("图片验证码: ", code)
// 配置文件中获取过期时间
exp := configs.Settings.Captcha.Expire
// 缓存生成的验证码
if err = SetCode(cid, code, int64(exp)); err != nil {
return
}
//core.Logger.Infoln("验证结果:", Verify(cid, code))
return
}
// Verify 验证
func Verify(id string, code string) (ok bool) {
ok = false
var (
err error
redisCode string
)
// 验证字符串是否合法
if code == "" || !corelib.IsCaptcha(code) {
return
}
code = strings.ToLower(code)
// redis取验证码
if redisCode, err = GetCode(id); err != nil {
return
}
// redis存储验证码和用户输入验证码对比
if strings.ToLower(redisCode) == strings.ToLower(code) {
return true
}
return
}

Binary file not shown.

Binary file not shown.

339
corelib/captcha/image.go Normal file
View File

@ -0,0 +1,339 @@
package captcha
import (
"crypto/rand"
"github.com/golang/freetype"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"
"image"
"image/color"
"log"
"math"
"math/big"
"os"
"path/filepath"
"strconv"
)
const (
chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
operator = "+-*/"
defaultLen = 4
defaultFontSize = 20
defaultDpi = 72
)
// Captcha 图形验证码 使用字体默认ttf格式
// w 图片宽度, h图片高度CodeLen验证码的个数
// FontSize 字体大小, Dpi 清晰度FontPath 字体目录, FontName 字体名字
// mode 验证模式 0普通字符串110以内简单数学公式
type Captcha struct {
W, H, CodeLen int
FontSize float64
Dpi int
FontPath, FontName string
mode int
}
// NewCaptcha 实例化验证码
func NewCaptcha(w, h, CodeLen int) *Captcha {
return &Captcha{W: w, H: h, CodeLen: CodeLen}
}
// OutPut 输出
func (captcha *Captcha) OutPut() (string, *image.RGBA) {
img := captcha.initCanvas()
return captcha.doImage(img)
}
// RangeRand 获取区间[-m, n]的随机数
func (captcha *Captcha) RangeRand(min, max int64) int64 {
if min > max {
panic("the min is greater than max!")
}
if min < 0 {
f64Min := math.Abs(float64(min))
i64Min := int64(f64Min)
result, _ := rand.Int(rand.Reader, big.NewInt(max+1+i64Min))
return result.Int64() - i64Min
} else {
result, _ := rand.Int(rand.Reader, big.NewInt(max-min+1))
return min + result.Int64()
}
}
// 随机字符串
func (captcha *Captcha) getRandCode() string {
if captcha.CodeLen <= 0 {
captcha.CodeLen = defaultLen
}
var code = ""
for l := 0; l < captcha.CodeLen; l++ {
charsPos := captcha.RangeRand(0, int64(len(chars)-1))
code += string(chars[charsPos])
}
return code
}
// 获取算术运算公式
func (captcha *Captcha) getFormulaMixData() (string, []string) {
num1 := int(captcha.RangeRand(11, 20))
num2 := int(captcha.RangeRand(1, 10))
opArr := []rune(operator)
opRand := opArr[captcha.RangeRand(0, 3)]
strNum1 := strconv.Itoa(num1)
strNum2 := strconv.Itoa(num2)
var ret int
var opRet string
switch string(opRand) {
case "+":
ret = num1 + num2
opRet = "+"
case "-":
if num1-num2 > 0 {
ret = num1 - num2
opRet = "-"
} else {
ret = num1 + num2
opRet = "+"
}
case "*":
if num1*num2 < 100 {
ret = num1 * num2
//opRet = "×"
opRet = "×"
} else {
ret = num1 + num2
opRet = "+"
}
case "/":
if num1%num2 == 0 {
ret = num1 / num2
opRet = "÷"
} else {
ret = num1 + num2
opRet = "+"
}
}
return strconv.Itoa(ret), []string{strNum1, opRet, strNum2, "=", "?"}
}
// 初始化画布
func (captcha *Captcha) initCanvas() *image.RGBA {
dest := image.NewRGBA(image.Rect(0, 0, captcha.W, captcha.H))
// 随机色
r := uint8(255) // uint8(captcha.RangeRand(50, 250))
g := uint8(255) // uint8(captcha.RangeRand(50, 250))
b := uint8(255) // uint8(captcha.RangeRand(50, 250))
// 填充背景色
for x := 0; x < captcha.W; x++ {
for y := 0; y < captcha.H; y++ {
dest.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255}) //设定alpha图片的透明度
}
}
return dest
}
// 处理图像
func (captcha *Captcha) doImage(dest *image.RGBA) (string, *image.RGBA) {
gc := draw2dimg.NewGraphicContext(dest)
defer gc.Close()
defer gc.FillStroke()
captcha.setFont(gc)
captcha.doPoint(gc)
captcha.doLine(gc)
captcha.doSinLine(gc)
var codeStr string
if captcha.mode == 1 {
ret, formula := captcha.getFormulaMixData()
// log.Println("算数验证码: ", formula)
codeStr = ret
captcha.doFormula(gc, formula)
} else {
codeStr = captcha.getRandCode()
captcha.doCode(gc, codeStr)
}
return codeStr, dest
}
// 验证码字符设置到图像上
func (captcha *Captcha) doCode(gc *draw2dimg.GraphicContext, code string) {
for l := 0; l < len(code); l++ {
y := captcha.RangeRand(int64(captcha.FontSize)-1, int64(captcha.H)+6)
x := captcha.RangeRand(1, 20)
// 随机色
r := uint8(captcha.RangeRand(0, 200))
g := uint8(captcha.RangeRand(0, 200))
b := uint8(captcha.RangeRand(0, 200))
gc.SetFillColor(color.RGBA{R: r, G: g, B: b, A: 255})
gc.FillStringAt(string(code[l]), float64(x)+captcha.FontSize*float64(l), float64(int64(captcha.H)-y)+captcha.FontSize)
gc.Stroke()
}
}
// 验证码字符设置到图像上
func (captcha *Captcha) doFormula(gc *draw2dimg.GraphicContext, formulaArr []string) {
for l := 0; l < len(formulaArr); l++ {
y := captcha.RangeRand(0, 10)
x := captcha.RangeRand(5, 10)
// 随机色
r := uint8(captcha.RangeRand(10, 200))
g := uint8(captcha.RangeRand(10, 200))
b := uint8(captcha.RangeRand(10, 200))
gc.SetFillColor(color.RGBA{R: r, G: g, B: b, A: 255})
gc.FillStringAt(formulaArr[l], float64(x)+captcha.FontSize*float64(l), captcha.FontSize+float64(y))
gc.Stroke()
}
}
// 增加干扰线
func (captcha *Captcha) doLine(gc *draw2dimg.GraphicContext) {
// 设置干扰线
for n := 0; n < 3; n++ {
gc.SetLineWidth(float64(captcha.RangeRand(1, 2)))
//gc.SetLineWidth(1)
// 随机背景色
r := uint8(captcha.RangeRand(0, 255))
g := uint8(captcha.RangeRand(0, 255))
b := uint8(captcha.RangeRand(0, 255))
gc.SetStrokeColor(color.RGBA{R: r, G: g, B: b, A: 255})
// 初始化位置
gc.MoveTo(float64(captcha.RangeRand(0, int64(captcha.W)+10)), float64(captcha.RangeRand(0, int64(captcha.H)+5)))
gc.LineTo(float64(captcha.RangeRand(0, int64(captcha.W)+10)), float64(captcha.RangeRand(0, int64(captcha.H)+5)))
gc.Stroke()
}
}
// 增加干扰点
func (captcha *Captcha) doPoint(gc *draw2dimg.GraphicContext) {
for n := 0; n < 10; n++ {
gc.SetLineWidth(float64(captcha.RangeRand(1, 3)))
// 随机色
r := uint8(captcha.RangeRand(0, 255))
g := uint8(captcha.RangeRand(0, 255))
b := uint8(captcha.RangeRand(0, 255))
gc.SetStrokeColor(color.RGBA{R: r, G: g, B: b, A: 255})
x := captcha.RangeRand(0, int64(captcha.W)+10) + 1
y := captcha.RangeRand(0, int64(captcha.H)+5) + 1
gc.MoveTo(float64(x), float64(y))
gc.LineTo(float64(x+captcha.RangeRand(1, 2)), float64(y+captcha.RangeRand(1, 2)))
gc.Stroke()
}
}
// 增加正弦干扰线
func (captcha *Captcha) doSinLine(gc *draw2dimg.GraphicContext) {
h1 := captcha.RangeRand(-12, 12)
h2 := captcha.RangeRand(-1, 1)
w2 := captcha.RangeRand(5, 20)
h3 := captcha.RangeRand(5, 10)
h := float64(captcha.H)
w := float64(captcha.W)
// 随机色
r := uint8(captcha.RangeRand(128, 255))
g := uint8(captcha.RangeRand(128, 255))
b := uint8(captcha.RangeRand(128, 255))
gc.SetStrokeColor(color.RGBA{R: r, G: g, B: b, A: 255})
gc.SetLineWidth(float64(captcha.RangeRand(1, 2)))
var i float64
for i = -w / 2; i < w/2; i = i + 0.1 {
y := h/float64(h3)*math.Sin(i/float64(w2)) + h/2 + float64(h1)
gc.LineTo(i+w/2, y)
if h2 == 0 {
gc.LineTo(i+w/2, y+float64(h2))
}
}
gc.Stroke()
}
// SetMode 设置模式
func (captcha *Captcha) SetMode(mode int) {
captcha.mode = mode
}
// SetFontPath 设置字体路径
func (captcha *Captcha) SetFontPath(FontPath string) {
captcha.FontPath = FontPath
}
// SetFontName 设置字体名称
func (captcha *Captcha) SetFontName(FontName string) {
captcha.FontName = FontName
}
// SetFontSize 设置字体大小
func (captcha *Captcha) SetFontSize(fontSize float64) {
captcha.FontSize = fontSize
}
// 设置相关字体
func (captcha *Captcha) setFont(gc *draw2dimg.GraphicContext) {
if captcha.FontPath == "" {
panic("the font path is empty!")
}
if captcha.FontName == "" {
panic("the font name is empty!")
}
// 字体文件
fontFile := filepath.Join(captcha.FontPath, captcha.FontName+".ttf")
fontBytes, err := os.ReadFile(fontFile)
if err != nil {
log.Println(err)
return
}
font, err := freetype.ParseFont(fontBytes)
if err != nil {
log.Println(err)
return
}
// 设置自定义字体相关信息
gc.FontCache = draw2d.NewSyncFolderFontCache(captcha.FontPath)
gc.FontCache.Store(draw2d.FontData{Name: captcha.FontName, Family: 0, Style: draw2d.FontStyleNormal}, font)
gc.SetFontData(draw2d.FontData{Name: captcha.FontName, Style: draw2d.FontStyleNormal})
//设置清晰度
if captcha.Dpi <= 0 {
captcha.Dpi = defaultDpi
}
gc.SetDPI(captcha.Dpi)
// 设置字体大小
if captcha.FontSize <= 0 {
captcha.FontSize = defaultFontSize
}
gc.SetFontSize(captcha.FontSize)
}

24
corelib/captcha/random.go Normal file
View File

@ -0,0 +1,24 @@
package captcha
import (
"math/rand"
"time"
)
// RandDigit 随机数
func RandDigit(max int) int {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
return r.Intn(max)
}
// RandDigitRange 随机数范围
func RandDigitRange(max int, min int) int {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
return r.Intn(max-min) + min
}
// RandChooseOne 数组随机选一个元素
func RandChooseOne[T any](reasons []T) T {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
return reasons[r.Intn(len(reasons))]
}

46
corelib/captcha/store.go Normal file
View File

@ -0,0 +1,46 @@
package captcha
import (
"context"
"picgo/corelib"
"time"
)
// Store 存储验证码接口
type Store interface {
// SetCode cid 当前id
// oid 旧id
// code 验证码
// expired 过期时间(毫秒)
// return nil/error
SetCode(cid string, oid string, code string, expired int)
// GetCode cid 当前id
// return code
GetCode(cid string) string
}
// SetCode 验证码保存到redis
func SetCode(cid string, code string, expired int64) (err error) {
ctx := context.Background()
cid = genRedisKey(cid)
if err = corelib.RdbClient.Set(ctx, cid, code, time.Duration(expired)*time.Second).Err(); err != nil {
return
}
return
}
// GetCode 从redis中查询验证码查询之后删除
func GetCode(cid string) (code string, err error) {
ctx := context.Background()
cid = genRedisKey(cid)
if code, err = corelib.RdbClient.Get(ctx, cid).Result(); err != nil {
return
}
//rdb.Del(ctx, cid)
return
}
// captcha缓存key拼接
func genRedisKey(id string) (res string) {
return corelib.CaptchaKey + id
}

34
corelib/encryption.go Normal file
View File

@ -0,0 +1,34 @@
package corelib
import (
"crypto/md5"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
// Md5Hash md5加密
func Md5Hash(data string) string {
md5Hash := md5.New()
md5Hash.Write([]byte(data))
return hex.EncodeToString(md5Hash.Sum(nil))
}
// HashPassword 使用Bcrypt哈希密码和盐值
func HashPassword(password, salt string) (string, error) {
var (
err error
hashedPassword []byte
)
passwordWithSalt := append([]byte(password), []byte(salt)...)
if hashedPassword, err = bcrypt.GenerateFromPassword(passwordWithSalt, bcrypt.DefaultCost); err != nil {
return "", err
}
return string(hashedPassword), nil
}
// ComparePasswords 验证密码是否匹配哈希值和盐值
func ComparePasswords(hashedPassword, userInput, salt string) bool {
passwordWithSalt := append([]byte(userInput), []byte(salt)...)
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), passwordWithSalt)
return err == nil
}

13
corelib/json.go Normal file
View File

@ -0,0 +1,13 @@
package corelib
import "encoding/json"
// JsonMarshal JSON封装
func JsonMarshal(src interface{}) (res []byte, err error) {
return json.Marshal(src)
}
// JsonUnmarshal JSON解封
func JsonUnmarshal(data []byte, res interface{}) (err error) {
return json.Unmarshal(data, res)
}

103
corelib/log.go Normal file
View File

@ -0,0 +1,103 @@
package corelib
//import (
// "go.uber.org/zap"
// "go.uber.org/zap/zapcore"
//)
//var Logger *zap.SugaredLogger
//
//func NewLogger() {
// writeSyncer := getLogWriter()
// encoder := getEncoder()
// core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
// logger := zap.New(core, zap.AddCaller())
// Logger = logger.Sugar()
//}
//
//func getEncoder() zapcore.Encoder {
// encoderConfig := zap.NewProductionEncoderConfig()
// encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// return zapcore.NewConsoleEncoder(encoderConfig)
//}
//
//func getLogWriter() zapcore.WriteSyncer {
// var (
// err error
// currentDir string
// )
// // 获取当前工作目录
// if currentDir, err = os.Getwd(); err != nil {
// log.Panic("获取当前工作目录失败: ", err)
// }
// logPath := path.Join(currentDir, configs.Settings.Server.LogPath)
// // Filename: 日志文件的位置
// // MaxSize在进行切割之前日志文件的最大大小以 MB 为单位)
// // MaxBackups保留旧文件的最大个数
// // MaxAges保留旧文件的最大天数
// // Compress是否压缩 / 归档旧文件
// lumberJackLogger := &lumberjack.Logger{
// Filename: logPath,
// MaxSize: 50,
// MaxBackups: 10,
// MaxAge: 1,
// Compress: false,
// }
// return zapcore.AddSync(lumberJackLogger)
//}
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Logger *zap.SugaredLogger
func NewLogger() {
config := zap.NewProductionConfig()
config.OutputPaths = []string{
"stdout",
"tmp/picgo-info.log",
}
config.ErrorOutputPaths = []string{
"stderr",
"tmp/picgo-error.log",
}
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
logger, err := config.Build()
if err != nil {
panic(err)
}
Logger = logger.Sugar()
}
func Panic(args ...interface{}) {
Logger.Panic(args...)
}
func Debug(args ...interface{}) {
Logger.Debug(args...)
}
func Info(args ...interface{}) {
Logger.Info(args...)
}
func Warn(args ...interface{}) {
Logger.Warn(args...)
}
func Error(args ...interface{}) {
Logger.Error(args...)
}
func Fatal(args ...interface{}) {
Logger.Fatal(args...)
}
func Sync() {
Logger.Sync()
}

111
corelib/msyql.go Normal file
View File

@ -0,0 +1,111 @@
package corelib
import (
"context"
"database/sql"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"picgo/configs"
"time"
)
type GormZapLogger struct {
logger *zap.SugaredLogger
gormLogger.Config
}
func (l *GormZapLogger) LogMode(level gormLogger.LogLevel) gormLogger.Interface {
newLogger := *l
newLogger.LogLevel = level
return &newLogger
}
func (l *GormZapLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLogger.Info {
l.logger.Infof(msg, data...)
}
}
func (l *GormZapLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLogger.Warn {
l.logger.Warnf(msg, data...)
}
}
func (l *GormZapLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLogger.Error {
l.logger.Errorf(msg, data...)
}
}
func (l *GormZapLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if l.LogLevel <= 0 {
return
}
elapsed := time.Since(begin)
switch {
case err != nil && l.LogLevel >= gormLogger.Error:
sql, rows := fc()
l.logger.Errorf("%s [%.2fms] [rows:%v] %s", err, float64(elapsed.Nanoseconds())/1e6, rows, sql)
case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= gormLogger.Warn:
sql, rows := fc()
l.logger.Warnf("SLOW SQL >= %v [%.2fms] [rows:%v] %s", l.SlowThreshold, float64(elapsed.Nanoseconds())/1e6, rows, sql)
case l.LogLevel >= gormLogger.Info:
sql, rows := fc()
l.logger.Infof("[%.2fms] [rows:%v] %s", float64(elapsed.Nanoseconds())/1e6, rows, sql)
}
}
var (
DbMysql *gorm.DB
)
// NewMysql 创建数据库连接
func NewMysql() {
var (
err error
db *gorm.DB
sqlDB *sql.DB
dsn string
)
// 使用 zapgorm2 创建 GORM logger
dsn = configs.Settings.Mysql.MysqlDNS
zapLogger := Logger
newLogger := &GormZapLogger{
logger: zapLogger,
Config: gormLogger.Config{
SlowThreshold: time.Second,
LogLevel: gormLogger.Info,
Colorful: false,
},
}
if db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
// 配置日志级别打印出所有的sql
//Logger: logger.Default.LogMode(logger.Info),
Logger: newLogger,
NamingStrategy: schema.NamingStrategy{
//TablePrefix: configs.Setting.TablePrefix, // 表前缀
SingularTable: true, // 设置全局表名禁用复数
},
}); err != nil {
Logger.Panicln("数据库连接失败: ", err)
}
if sqlDB, err = db.DB(); err != nil {
Logger.Panicln("数据库连接失败: ", err)
}
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(configs.Settings.Mysql.MaxIdleConns)
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(configs.Settings.Mysql.MaxOpenConns)
// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)
DbMysql = db
}

48
corelib/rand.go Normal file
View File

@ -0,0 +1,48 @@
package corelib
import (
"math/rand"
"time"
)
func randomString(n int, allowedChars []rune) string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
b := make([]rune, n)
for i := range b {
b[i] = allowedChars[r.Intn(len(allowedChars))]
}
return string(b)
}
// 生成随机字符串
func generateRandom(randType int, length int) string {
var res string
const special = "~!@#$%^&*()_+=-"
const number = "0123456789"
const capitalLetter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const lowerLetter = "abcdefghijklmnopqrstuvwxyz"
switch {
case randType == 1: // 特俗符号
res = special
case randType == 2: // 数字
res = number
case randType == 3: // 大写字母
res = capitalLetter
case randType == 4: // 小写字母
res = lowerLetter
case randType == 5: // 大写字母+小写字母
res = capitalLetter + lowerLetter
case randType == 6: // 大写字母+小写字母+数字
res = capitalLetter + lowerLetter + number
default: // 大写字母+小写字母+数字+特殊符号
res = capitalLetter + lowerLetter + number + special
}
return randomString(length, []rune(res))
}
// GenerateSalt 生成一个随机盐
func GenerateSalt() string {
const saltSize = 16 // 16字节的随机盐
return generateRandom(0, saltSize)
}

30
corelib/redis.go Normal file
View File

@ -0,0 +1,30 @@
package corelib
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"picgo/configs"
)
var (
RdbClient *redis.Client
CaptchaKey = "picgo:captcha:" // 验证码存储key
UserKey = "picgo:user:"
)
func NewRedis() {
RdbClient = redis.NewClient(&redis.Options{
Addr: configs.Settings.Redis.Addr, // Redis服务器地址
Password: configs.Settings.Redis.Password, // 密码,如果没有密码可以为空
DB: configs.Settings.Redis.DB, // 使用默认的数据库
PoolSize: configs.Settings.Redis.PoolSize, // 连接池大小
MinIdleConns: configs.Settings.Redis.MinidleConns, // 最小空闲连接数
})
// 测试连接
_, err := RdbClient.Ping(context.Background()).Result()
if err != nil {
panic(fmt.Sprintf("Failed to connect to Redis: %v", err))
}
}

38
corelib/regexp.go Normal file
View File

@ -0,0 +1,38 @@
package corelib
import (
"github.com/dlclark/regexp2"
)
// 执行正则
func regexpRun(rx string, txt string) (ok bool) {
var (
err error
compile *regexp2.Regexp
matchString bool
)
if compile, err = regexp2.Compile(rx, 0); err != nil {
Logger.Error("regexp compile error:", err)
return false
}
if matchString, err = compile.MatchString(txt); err != nil {
Logger.Error("regexp compile error:", err)
return false
}
return matchString
}
// IsCaptcha 验证码正则
func IsCaptcha(code string) (ok bool) {
return regexpRun("^[0-9a-zA-Z]{1,5}$", code)
}
// CheckUsername 用户名正则
func CheckUsername(username string) (ok bool) {
return regexpRun("^[a-zA-Z0-9_-]{6,16}$", username)
}
// CheckPassword 用户名正则
func CheckPassword(password string) (ok bool) {
return regexpRun("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[$@$!#%*?&])[A-Za-z\\d$@#$!%*?&]{8,16}", password)
}

View File

@ -15,7 +15,7 @@ type JsonResponse struct {
// WriteJsonResponse 用于写入JSON响应
func WriteJsonResponse(w http.ResponseWriter, status int, message string, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.WriteHeader(http.StatusOK)
response := JsonResponse{
Status: status,
Message: message,

18
corelib/session.go Normal file
View File

@ -0,0 +1,18 @@
package corelib
import (
"github.com/boj/redistore"
"picgo/configs"
)
var SessionStore *redistore.RediStore
func NewSessionStore() {
// Fetch new store.
store, err := redistore.NewRediStore(10, "tcp", configs.Settings.Redis.Addr, configs.Settings.Redis.Password, []byte(configs.Settings.SessionsKey))
if err != nil {
panic(err)
}
SessionStore = store
//defer store.Close()
}

44
corelib/template.go Normal file
View File

@ -0,0 +1,44 @@
package corelib
import (
"html/template"
"net/http"
)
// noRender 是一个自定义模板函数,忽略不渲染传入的内容
func noRender(s string) template.HTML {
return template.HTML(s)
}
func TemplateHandler(w http.ResponseWriter, data any, name string, filenames ...string) {
// 创建一个新的基础模板,并将自定义函数注册到模板中
baseTmpl := template.New("base").Funcs(template.FuncMap{
"noRender": noRender,
})
// 添加一个简单的内容以确保基础模板不是空的
_, err := baseTmpl.Parse(`{{define "base"}}{{end}}`)
if err != nil {
Logger.Errorf("Error creating base template: %v", err)
http.Error(w, "Error executing template", http.StatusInternalServerError)
}
// 解析布局模板及其子模板
_, err = baseTmpl.ParseFiles(filenames...)
if err != nil {
Logger.Errorf("Error parsing templates: %v", err)
http.Error(w, "Error executing template", http.StatusInternalServerError)
}
// 加载所有模板文件
tmpl, err := baseTmpl.Clone()
if err != nil {
Logger.Errorf("error cloning base template: %v", err)
http.Error(w, "Error executing template", http.StatusInternalServerError)
}
err = tmpl.ExecuteTemplate(w, name, data)
if err != nil {
Logger.Errorf("Error executing template: %v", err)
http.Error(w, "Error executing template", http.StatusInternalServerError)
}
}

62
data/user.go Normal file
View File

@ -0,0 +1,62 @@
package data
import (
"context"
"picgo/corelib"
"picgo/model"
"time"
)
// SysUserSelectByUsername 通过用户名查询用户
func SysUserSelectByUsername(userName string) (model.SysUser, error) {
var (
err error
user model.SysUser
)
if user, err = SysUserGetCacheByUsername(userName); err == nil {
corelib.Logger.Infoln("SysUserSelectByUsername 从缓存获取用户信息成功: ", user.Username)
return user, nil
} else {
corelib.Logger.Infoln("SysUserSelectByUsername 从缓存获取用户信息失败: ", err)
}
if err = corelib.DbMysql.Model(model.SysUser{Username: userName}).First(&user).Error; err != nil {
corelib.Logger.Infoln("SysUserSelectByUsername 数据库中查询用户信息失败: ", err)
return user, err
}
if err = SysUserSetCacheByUsername(user); err != nil {
corelib.Logger.Infoln("SysUserSelectByUsername 缓存用户信息失败: ", err)
return user, nil
}
return user, nil
}
func SysUserSetCacheByUsername(user model.SysUser) error {
var (
jsonData []byte
err error
)
if jsonData, err = corelib.JsonMarshal(user); err != nil {
return err
}
key := corelib.UserKey + user.Username
if err = corelib.RdbClient.Set(context.Background(), key, string(jsonData), 5*time.Minute).Err(); err != nil {
return err
}
return nil
}
// SysUserGetCacheByUsername redis获取用户信息
func SysUserGetCacheByUsername(userName string) (model.SysUser, error) {
var (
err error
user model.SysUser
userCache string
)
if userCache, err = corelib.RdbClient.Get(context.Background(), corelib.UserKey+userName).Result(); err != nil {
return model.SysUser{}, err
}
if err = corelib.JsonUnmarshal([]byte(userCache), &user); err != nil {
return model.SysUser{}, err
}
return user, nil
}

52
deploy/picgo-dev.yml Normal file
View File

@ -0,0 +1,52 @@
#------------------------
# ---- 公共配置 start ----
#------------------------
# 服务
server:
port: 8282
host: localhost
node_id: 1
log_path: tmp/picgo.log
environment: dev
static_path: static
upload_path: static/pic
upload_max_size: 3
sessions_key: afE+7AztxjvTc5MmQ/8RuQw1aabgOLMlAKGMKLseRkc=
session_name: picgo
# redis
redis:
addr: "localhost:6379"
password: Develop_123
db: 3
pool_size: 10
minidle_conns: 5
# 数据库
mysql:
mysql_dns: "root:Mysql_123@tcp(localhost:3306)/picgo?charset=utf8mb4&parseTime=True&loc=Local"
max_idle_conns: 10
max_open_conns: 100
#----------------------
# ---- 公共配置 end ----
#----------------------
#-------------------------------
# ---- 管理后台相关配置 start ----
#-------------------------------
# 验证码
captcha:
# 过期时间(s)
expire: 305000
width: 96
hight: 32
length: 4
# 鉴权
#jwt:
# signing_key: eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9
# expires_time: 7d
# issuer: cnnm
# aes_key: 23624e296707b1eb20311c29969bc46e
# aes_iv: 29ca233cd1f42147
#-----------------------------
# ---- 管理后台相关配置 end ----
#-----------------------------

36
go.mod
View File

@ -2,4 +2,38 @@ module picgo
go 1.22
require github.com/gorilla/mux v1.8.1
require (
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff
github.com/bwmarrin/snowflake v0.3.0
github.com/dlclark/regexp2 v1.11.2
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/gorilla/csrf v1.7.2
github.com/gorilla/mux v1.8.1
github.com/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195
github.com/redis/go-redis/v9 v9.5.4
github.com/spf13/cobra v1.8.1
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.25.0
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.11
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/garyburd/redigo v1.6.4 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/text v0.16.0 // indirect
)

82
go.sum
View File

@ -1,2 +1,84 @@
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
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/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/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/dlclark/regexp2 v1.11.2 h1:/u628IuisSTwri5/UKloiIsH8+qF2Pu7xEQX+yIKg68=
github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg=
github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195 h1:Vdz2cBh5Fw2MYHWi3ED2PraDQaWEUhNCr1XFHrP4N5A=
github.com/llgcode/draw2d v0.0.0-20240627062922-0ed1ff131195/go.mod h1:1Vk0LDW6jG5cGc2D9RQUxHaE0vYhTvIwSo9mOL6K4/U=
github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e h1:ZAvbj5hI/G/EbAYAcj4yCXUNiFKefEhH0qfImDDD0/8=
github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e/go.mod h1:1l8ky+Ew27CMX29uG+a2hNOKpeNYEQjjtiALiBlFQbY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.5.4 h1:vOFYDKKVgrI5u++QvnMT7DksSMYg7Aw/Np4vLJLKLwY=
github.com/redis/go-redis/v9 v9.5.4/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b h1:U/Uqd1232+wrnHOvWNaxrNqn/kFnr4yu4blgPtQt0N8=
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b/go.mod h1:fgfIZMlsafAHpspcks2Bul+MWUNw/2dyQmjC2faKjtg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

86
handler/captcha.go Normal file
View File

@ -0,0 +1,86 @@
package handler
import (
"bytes"
"image"
"image/png"
"net"
"net/http"
"picgo/corelib"
"picgo/corelib/captcha"
"strings"
)
func CaptchaHandler(w http.ResponseWriter, r *http.Request) {
var (
res []byte
err error
)
captchaId := getCaptchaId(r)
corelib.Logger.Info("captchaHandler cid: ", captchaId)
if res, err = generateCaptcha(captchaId); err != nil {
corelib.WriteJsonResponse(w, 1030, "生产验证码失败", nil)
return
}
// 设置响应头
w.Header().Set("Content-Type", "image/png")
// 将图片文件内容写入响应
_, err = w.Write(res)
if err != nil {
http.Error(w, "Failed to serve image.", http.StatusInternalServerError)
}
}
// user agent + client ip md5 生成验证码key
func getCaptchaId(r *http.Request) string {
clientIP := GetRealIP(r)
userAgent := r.Header.Get("User-Agent")
return corelib.Md5Hash(clientIP + userAgent)
}
// 生成验证码图片
func generateCaptcha(captchaId string) ([]byte, error) {
var (
b bytes.Buffer
img *image.RGBA
err error
)
if img, err = captcha.GenerateCaptcha(captchaId); err != nil {
corelib.Logger.Fatalln("生成验证码错误", err)
return nil, err
//return nil, corelib.CAPTCHA_GENERATE_ERROR
}
if err = png.Encode(&b, img); err != nil {
corelib.Logger.Fatalln("验证码encode错误", err)
return nil, err
//return nil, corelib.CAPTCHA_GENERATE_ERROR
}
return b.Bytes(), nil
}
// GetRealIP 获取用户的真实IP地址
func GetRealIP(r *http.Request) string {
// 检查 X-Forwarded-For 头
xff := r.Header.Get("X-Forwarded-For")
if xff != "" {
// X-Forwarded-For 头可以包含多个IP地址取第一个
ips := strings.Split(xff, ",")
if len(ips) > 0 {
return strings.TrimSpace(ips[0])
}
}
// 检查 X-Real-IP 头
xri := r.Header.Get("X-Real-IP")
if xri != "" {
return xri
}
// 如果没有这些头,则使用远程地址
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return ip
}

31
handler/domain.go Normal file
View File

@ -0,0 +1,31 @@
package handler
import (
"github.com/gorilla/csrf"
"net/http"
"picgo/corelib"
"picgo/data"
"picgo/model"
)
func DomainHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
var (
err error
user model.SysUser
)
username := r.Context().Value("username").(string)
if user, err = data.SysUserGetCacheByUsername(username); err != nil {
http.Error(w, "IndexHandler SysUserGetCacheByUsername Error", http.StatusInternalServerError)
return
}
tmpData := map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
}
tmpData["Title"] = "域名管理"
tmpData["Active"] = r.URL.Path
tmpData["IsSuper"] = user.IsSuper
w.Header().Add("X-CSRF-Token", csrf.Token(r))
corelib.TemplateHandler(w, tmpData, "layout.html", "view/layout.html", "view/domain.html")
}
}

View File

@ -1 +1,31 @@
package handler
import (
"github.com/gorilla/csrf"
"net/http"
"picgo/corelib"
"picgo/data"
"picgo/model"
)
func IndexHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
var (
err error
user model.SysUser
)
username := r.Context().Value("username").(string)
if user, err = data.SysUserGetCacheByUsername(username); err != nil {
http.Error(w, "IndexHandler SysUserGetCacheByUsername Error", http.StatusInternalServerError)
return
}
tmpData := map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
}
tmpData["Title"] = "Dashboard"
tmpData["Active"] = r.URL.Path
tmpData["IsSuper"] = user.IsSuper
w.Header().Add("X-CSRF-Token", csrf.Token(r))
corelib.TemplateHandler(w, tmpData, "layout.html", "view/layout.html", "view/index.html")
}
}

View File

@ -1 +1,73 @@
package handler
import (
"encoding/json"
"github.com/gorilla/csrf"
"net/http"
"picgo/configs"
"picgo/corelib"
"picgo/corelib/captcha"
"picgo/data"
"picgo/model"
)
func LoginHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
tmpData := map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
}
corelib.TemplateHandler(w, tmpData, "login.html", "view/login.html")
case http.MethodPost:
loginService(w, r)
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
}
func loginService(w http.ResponseWriter, r *http.Request) {
var (
res model.LoginRequest
user model.SysUser
)
err := json.NewDecoder(r.Body).Decode(&res)
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
cid := getCaptchaId(r)
corelib.Logger.Info("LoginRequest: ", res)
corelib.Logger.Info("login cid: ", cid)
if ok := captcha.Verify(cid, res.Captcha); !ok {
corelib.WriteJsonResponse(w, 1040, "验证码错误", nil)
return
}
if user, err = data.SysUserSelectByUsername(res.Username); err != nil {
corelib.WriteJsonResponse(w, 1041, "用户不存在", nil)
return
}
// 验证用户名密码
if !corelib.ComparePasswords(user.Password, res.Password, user.Salt) {
corelib.WriteJsonResponse(w, 1042, "用户名或密码错误", nil)
return
}
session, _ := corelib.SessionStore.Get(r, configs.Settings.Server.SessionName)
session.Values["username"] = user.Username
if err = session.Save(r, w); err != nil {
corelib.Logger.Infoln("session save err:", err)
corelib.WriteJsonResponse(w, 1043, "会话保存失败", nil)
return
}
w.Header().Set("Content-Type", "application/json")
corelib.WriteJsonResponse(w, 200, "登录成功", nil)
}
//// sysUserSelectDataByUsername 通过用户名查询用户
//func sysUserSelectDataByUsername(userName string) (model.SysUser, error) {
// var user model.SysUser
// if err := corelib.DbMysql.Model(model.SysUser{Username: userName}).First(&user).Error; err != nil {
// return user, err
// }
// return user, nil
//}

32
handler/picture.go Normal file
View File

@ -0,0 +1,32 @@
package handler
import (
"github.com/gorilla/csrf"
"net/http"
"picgo/corelib"
"picgo/data"
"picgo/model"
)
func PictureHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
var (
err error
user model.SysUser
)
username := r.Context().Value("username").(string)
if user, err = data.SysUserGetCacheByUsername(username); err != nil {
http.Error(w, "IndexHandler SysUserGetCacheByUsername Error", http.StatusInternalServerError)
return
}
tmpData := map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
}
tmpData["Title"] = "图片管理"
tmpData["Active"] = r.URL.Path
tmpData["IsSuper"] = user.IsSuper
w.Header().Add("X-CSRF-Token", csrf.Token(r))
corelib.TemplateHandler(w, tmpData, "layout.html", "view/layout.html", "view/picture.html")
}
}

View File

@ -4,31 +4,22 @@ import (
"net/http"
"os"
"path/filepath"
"picgo/configs"
)
func StaticHandler(w http.ResponseWriter, r *http.Request) {
func StaticHandler() http.Handler {
// 文件服务器的根目录
root := "./static"
// 自定义文件处理器
fileHandler := http.StripPrefix("/static/", http.FileServer(http.Dir(root)))
// 获取请求的路径
path := filepath.Join(root, r.URL.Path[len("/static/"):])
// 检查路径是否是一个目录
info, err := os.Stat(path)
if err != nil {
// 如果文件不存在返回404
http.NotFound(w, r)
return
}
if info.IsDir() {
// 如果是目录返回404
http.NotFound(w, r)
return
}
// 处理文件
fileHandler.ServeHTTP(w, r)
root := configs.Settings.Server.StaticPath
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 获取文件路径
path := filepath.Join(root, r.URL.Path)
// 检查路径是否为目录
info, err := os.Stat(path)
if err == nil && info.IsDir() {
http.NotFound(w, r) // 如果是目录则返回404
return
}
// 否则提供文件
http.ServeFile(w, r, path)
})
}

View File

@ -6,13 +6,14 @@ import (
"net/http"
"os"
"path/filepath"
"picgo/configs"
"picgo/corelib"
)
func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
// 解析表单数据限制最大内存为32MB
err := r.ParseMultipartForm(32 << 20)
// 解析表单数据限制最大内存为3MB 1024*1024*3
err := r.ParseMultipartForm(configs.Settings.Server.UploadMaxSize << 20)
if err != nil {
corelib.WriteJsonResponse(w, 1020, "文件大于32MB", nil)
return
@ -28,7 +29,7 @@ func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
defer file.Close()
// 保存文件到本地
f, err := os.OpenFile(filepath.Join("./static/pic", handler.Filename), os.O_WRONLY|os.O_CREATE, 0666)
f, err := os.OpenFile(filepath.Join(configs.Settings.Server.UploadPath, handler.Filename), os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println("Error saving file")
fmt.Println(err)
@ -46,7 +47,8 @@ func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
return
}
//fmt.Fprintf(w, "File %s uploaded successfully!", handler.Filename)
corelib.Success(w)
corelib.WriteJsonResponse(w, 200, "文件上传成功", nil)
//corelib.Success(w)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}

32
handler/user.go Normal file
View File

@ -0,0 +1,32 @@
package handler
import (
"github.com/gorilla/csrf"
"net/http"
"picgo/corelib"
"picgo/data"
"picgo/model"
)
func UserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
var (
err error
user model.SysUser
)
username := r.Context().Value("username").(string)
if user, err = data.SysUserGetCacheByUsername(username); err != nil {
http.Error(w, "IndexHandler SysUserGetCacheByUsername Error", http.StatusInternalServerError)
return
}
tmpData := map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(r),
}
tmpData["Title"] = "用户管理"
tmpData["Active"] = r.URL.Path
tmpData["IsSuper"] = user.IsSuper
w.Header().Add("X-CSRF-Token", csrf.Token(r))
corelib.TemplateHandler(w, tmpData, "layout.html", "view/layout.html", "view/user.html")
}
}

15
main.go
View File

@ -1,18 +1,7 @@
package main
import (
"log"
"net/http"
"picgo/router"
)
import "picgo/cmd"
func main() {
// 创建路由
r := router.InitRouter()
// 启动HTTP服务器
log.Println("Serving on http://localhost:8082")
err := http.ListenAndServe(":8082", r)
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
cmd.Execute()
}

37
middleware/auth.go Normal file
View File

@ -0,0 +1,37 @@
package middleware
import (
"context"
"net/http"
"picgo/configs"
"picgo/corelib"
"picgo/data"
"picgo/model"
"strings"
)
// LoginMiddleware 登录 // 添加日志中间件到路由器 使用r.Handle("/", LoginMiddleware(http.HandlerFunc(handler)))
func LoginMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resPath := r.URL.Path
if resPath == "/login" || resPath == "/captcha" || strings.HasPrefix(resPath, "/static") {
next.ServeHTTP(w, r)
}
session, _ := corelib.SessionStore.Get(r, configs.Settings.Server.SessionName)
username, ok := session.Values["username"].(string)
if !ok || username == "" {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
var (
user model.SysUser
err error
)
if user, err = data.SysUserSelectByUsername(username); err != nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
ctx := context.WithValue(r.Context(), "username", user.Username)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

21
middleware/cors.go Normal file
View File

@ -0,0 +1,21 @@
package middleware
import "net/http"
// CorsMiddleware 是处理跨域请求的中间件
func CorsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CSRF-Token")
w.Header().Set("Access-Control-Expose-Headers", "Content-Length, X-CSRF-Token")
// 处理预检请求
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}

26
model/base.go Normal file
View File

@ -0,0 +1,26 @@
package model
import (
"github.com/bwmarrin/snowflake"
"gorm.io/gorm"
"picgo/configs"
"time"
)
type BaseModel struct {
ID int64 `gorm:"primary_key;unique;not null;comment:'主键'" json:"id"`
CreatedAt time.Time `gorm:"not null;type:timestamp;default:CURRENT_TIMESTAMP;comment:'创建时间'"`
UpdatedAt time.Time `gorm:"not null;type:timestamp;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;comment:'更新时间'"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// BeforeCreate 在创建用户之前为其生成分布式ID
func (base *BaseModel) BeforeCreate(tx *gorm.DB) (err error) {
var node *snowflake.Node
nodeId := configs.Settings.Server.NodeId
if node, err = snowflake.NewNode(nodeId); err != nil {
return err
}
base.ID = node.Generate().Int64()
return nil
}

13
model/domin.go Normal file
View File

@ -0,0 +1,13 @@
package model
// Domain cdn访问域名
type Domain struct {
BaseModel
Name string `gorm:"size:64;unique;not null;column:name;comment:'域名'" json:"name"`
Remark string `gorm:"size:64;column:remark;comment:'备注'" json:"remark"`
}
// TableName sets the custom table name for the User model.
func (Domain) TableName() string {
return "domain"
}

7
model/login_request.go Normal file
View File

@ -0,0 +1,7 @@
package model
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Captcha string `json:"captcha"`
}

13
model/upload.go Normal file
View File

@ -0,0 +1,13 @@
package model
// Upload cdn访问域名
type Upload struct {
BaseModel
FileKey string `gorm:"size:64;unique;not null;column:filekey;comment:'文件路径'" json:"fileKey"`
Remark string `gorm:"size:64;column:remark;comment:'备注'" json:"remark"`
}
// TableName sets the custom table name for the User model.
func (Upload) TableName() string {
return "upload"
}

16
model/user.go Normal file
View File

@ -0,0 +1,16 @@
package model
// SysUser represents a user in the database.
type SysUser struct {
BaseModel
Username string `gorm:"size:32;unique;not null;column:username;comment:'用户名'" json:"username"`
Password string `gorm:"size:64;not null;column:password;comment:'密码'" json:"password"`
Salt string `gorm:"size:16;not null;column:salt;comment:'盐'" json:"salt"`
IsSuper int `gorm:"type:TINYINT(1);default:0;column:is_super;comment:'是否是超级管理员-0:否,1:是'" json:"is_super"`
Remark string `gorm:"size:64;column:remark;comment:'备注'" json:"remark"`
}
// TableName sets the custom table name for the User model.
func (SysUser) TableName() string {
return "sys_user"
}

View File

@ -1,15 +1,41 @@
package router
import (
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"net/http"
"picgo/configs"
"picgo/handler"
"picgo/middleware"
)
func InitRouter() *http.ServeMux {
var mux *http.ServeMux
func InitRouter() *mux.Router {
var secure bool
if configs.Settings.Server.Environment == "dev" {
secure = false
} else {
secure = true
}
// 设置CSRF保护
CSRF := csrf.Protect(
[]byte(configs.Settings.Server.SessionsKey),
csrf.Secure(secure), // 在开发环境中禁用HTTPS
csrf.RequestHeader("X-CSRF-Token"),
)
// 创建新的路由器
mux = http.NewServeMux()
mux.HandleFunc("/static/", handler.StaticHandler)
mux.HandleFunc("/api/v1/upload", handler.UploadFileHandler)
return mux
r := mux.NewRouter()
// 不需要鉴权路由
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", handler.StaticHandler()))
r.HandleFunc("/login", handler.LoginHandler).Methods(http.MethodGet, http.MethodPost) // 登录
r.HandleFunc("/captcha", handler.CaptchaHandler).Methods(http.MethodGet) // 验证码接口
// 需要鉴权路由
r.Handle("/", middleware.LoginMiddleware(http.HandlerFunc(handler.IndexHandler))).Methods(http.MethodGet) // 后台首页
r.Handle("/domain", middleware.LoginMiddleware(http.HandlerFunc(handler.DomainHandler))).Methods(http.MethodGet) // 域名管理
r.Handle("/user", middleware.LoginMiddleware(http.HandlerFunc(handler.UserHandler))).Methods(http.MethodGet) // 用户管理
r.Handle("/picture", middleware.LoginMiddleware(http.HandlerFunc(handler.PictureHandler))).Methods(http.MethodGet) // 图片管理
r.Handle("/api/v1/upload", middleware.LoginMiddleware(http.HandlerFunc(handler.UploadFileHandler))).Methods(http.MethodPost) // 图片上传接口
// 应用 CORS CSRF 中间件
http.Handle("/", middleware.CorsMiddleware(CSRF(r)))
return r
}

0
static/css/index.css Normal file
View File

22
static/img/add.svg Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>图标/常规/ic_增加表格</title>
<defs>
<rect id="path-1" x="0" y="0" width="24" height="24"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="图床" transform="translate(-643, -239)">
<g id="数据录入/上传/拖拽/拖拽点击" transform="translate(-61, 184)">
<g id="编组" transform="translate(313, 55)">
<g id="图标/常规/ic_增加表格" transform="translate(391, 0)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="矩形" fill="#4C5362" opacity="0" xlink:href="#path-1"></use>
<path d="M22.5,0 L22.5,10.5 L21,10.5 L21,1.5 L3,1.5 L3,22.5 L9,22.5 L9,24 L1.5,24 L1.5,0 L22.5,0 Z M17.25,12 L17.25,17.25 L22.5,17.25 L22.5,18.75 L17.25,18.75 L17.25,24 L15.75,24 L15.75,18.75 L10.5,18.75 L10.5,17.25 L15.75,17.25 L15.75,12 L17.25,12 Z" id="形状结合" fill="#4C5362" fill-rule="nonzero"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

55
static/js/common.js Normal file
View File

@ -0,0 +1,55 @@
function Common() {
}
// 错误弹窗
Common.prototype.AlertError = function (msg) {
swal('提示', msg, 'error');
}
// 弹窗
Common.prototype.AlertToast = function (msg, type) {
swal({
'title': msg,
'text': '',
'type': type,
'showCancelButton': false,
'showConfirmButton': false,
'timer': 1000
})
}
// 带确认按钮的弹窗
Common.prototype.AlertConfirm = function (params) {
swal({
'title': params['title'] ? params['title'] : '提示',
'showCancelButton': true,
'showConfirmButton': true,
'type': params['type'] ? params['type'] : '',
'confirmButtonText': params['confirmText'] ? params['confirmText'] : '确定',
'cancelButtonText': params['cancelText'] ? params['cancelText'] : '取消',
'text': params['text'] ? params['text'] : ''
}, function (isConfirm) {
if (isConfirm) {
if (params['confirmCallback']) {
params['confirmCallback']()
}
} else {
if (params['cancelCallback']) {
params['cancelCallback']()
}
}
})
}
// 成功弹窗
Common.prototype.AlertSuccessToast = function (msg) {
if (!msg) {
msg = '成功!'
}
this.AlertToast(msg, 'success')
}
var Alert = new Common()

File diff suppressed because one or more lines are too long

View File

@ -1,338 +1,102 @@
function Domain() {
this.tableData = null
this.model = false
this.editIndex = -1
// this.url = "http://127.0.0.1:8300/domain"
this.url = "https://monitor.api.ggbba.top/domain"
this.currentPage = 1
this.pageData = null
this.searchText = ""
this.numPages = 0
}
function Index() {
// 搜索域名
Domain.prototype.searchDomain = function () {
let self = this
$('#search-btn').click(function () {
let search = $('#search-input').val()
self.searchText = search
if (self.searchText) {
self.currentPage = 1
self.initTableData(self.currentPage, self.searchText)
} else {
self.initTableData(1, "")
}
//阻止浏览器默认行。
Index.prototype.HoldbackBorwser = function () {
$(document).on({
dragleave: function (e) { //拖离
e.preventDefault();
},
drop: function (e) { //拖后放
e.preventDefault();
},
dragenter: function (e) { //拖进
e.preventDefault();
},
dragover: function (e) { //拖来拖去
e.preventDefault();
}
})
});
}
// 点击分页按钮
Domain.prototype.domainPageBtn = function () {
//监听拖拽事件
Index.prototype.MonitorDrop = function () {
let self = this
$('.page-btn').click(function () {
self.currentPage = parseInt($(this).attr('data-p'))
self.initTableData(self.currentPage, self.searchText)
})
}
// 分页初始化
Domain.prototype.domainPageInit = function () {
let self = this
// 是否是最后一页
let is_finally = false
// 是否是第一页
let is_first = false
// 总页数
let num_pages = self.pageData['num_pages']
if (self.currentPage === 1) {
is_first = true
}
if (self.currentPage === num_pages) {
is_finally = true
}
self.pageData['is_first'] = is_first
self.pageData['is_finally'] = is_finally
}
// 表单验证
Domain.prototype.verifyDomain = function (domain) {
let patt = /(https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/i
return patt.test(domain)
}
// 错误弹窗
Domain.prototype.alertError = function (msg) {
swal('提示', msg, 'error');
}
// 弹窗
Domain.prototype.alertToast = function (msg, type) {
swal({
'title': msg,
'text': '',
'type': type,
'showCancelButton': false,
'showConfirmButton': false,
'timer': 1000
})
}
// 带确认按钮的弹窗
Domain.prototype.alertConfirm = function (params) {
swal({
'title': params['title'] ? params['title'] : '提示',
'showCancelButton': true,
'showConfirmButton': true,
'type': params['type'] ? params['type'] : '',
'confirmButtonText': params['confirmText'] ? params['confirmText'] : '确定',
'cancelButtonText': params['cancelText'] ? params['cancelText'] : '取消',
'text': params['text'] ? params['text'] : ''
}, function (isConfirm) {
if (isConfirm) {
if (params['confirmCallback']) {
params['confirmCallback']()
}
} else {
if (params['cancelCallback']) {
params['cancelCallback']()
let box = document.getElementById('upload-btn-click'); //拖拽区域
box.addEventListener("drop", function (e) {
e.preventDefault(); //取消默认浏览器拖拽效果
let fileList = e.dataTransfer.files; //获取文件对象
//循环遍历文件对象
for (let i = 0; i < fileList.length; i++) {
//检测文件是不是图片
if (fileList[i].type.indexOf('image') === -1) {
Alert.AlertError("您拖的不是图片!")
return false;
}
//拖拉图片到浏览器,可以实现预览功能
// let img = window.URL.createObjectURL(fileList[i]);
// let filename = fileList[i].name; //图片名称
// let filesize = Math.floor((fileList[0].size) / 1024);
// if (filesize > 2048) {
// Alert.AlertError("上传大小不能超过2M.")
// return false;
// }
//上传
self.UploadFile(fileList[i]);
}
})
}, false);
}
// 成功弹窗
Domain.prototype.alertSuccessToast = function (msg) {
if (!msg) {
msg = '成功!'
}
this.alertToast(msg, 'success')
}
// 渲染模板
Domain.prototype.renderPage = function () {
let self = this;
let html = template('tpl-table-tr', {domainList: self.tableData})
$('#tbody-domin').empty().append(html)
html = template('tpl-page-li', {page_data: self.pageData})
$('#page-li').empty().append(html)
self.editDomain()
self.deleteDomain()
self.domainPageBtn()
}
// 初始化数据
Domain.prototype.initTableData = function (num, query) {
let self = this;
let requestUrl = self.url + '/page/' + num
if (query) {
requestUrl += "?search=" + query
}
$.get(requestUrl, function (data, status) {
if (data['code'] === 200) {
self.currentPage = num
self.tableData = data['data']['domain_data']
self.pageData = data['data']['pagination_data']
self.numPages = self.pageData['num_pages']
self.domainPageInit()
self.renderPage()
}
})
}
// 添加域名
Domain.prototype.addDomain = function () {
let self = this;
$("#add-domain").click(function () {
self.initAddForm()
})
}
// 初始化添加域名表单弹窗
Domain.prototype.initAddForm = function () {
this.model = false
$('#DomainModalLabel').text("添加域名")
$('#cdn-f').prop('checked', true)
$('#boce-f').prop('checked', true)
$('#form-key').val("")
$('#form-domain').val("")
$('#form-remark').val("")
}
// 提交添加域名表单
Domain.prototype.addSubmit = function () {
let self = this
let boceId = $("input[name='options-boce']:checked").attr("id")
let boce = false
if (boceId === "boce-t") {
boce = true
}
let cdnId = $("input[name='options-cdn']:checked").attr("id")
let cdn = false
if (cdnId === "cdn-t") {
cdn = true
}
let domain = $('#form-domain').val()
let autoKey = $('#form-key').val()
let remark = $('#form-remark').val()
let requestData = {'name': domain,'auth_key': auto_Key, 'remark': remark, 'boce': boce, 'category': cdn}
$("#DomainModal").modal("hide")
if (!self.verifyDomain(domain)) {
self.alertError("域名格式不正确")
return
//上传图片
Index.prototype.UploadFile = function (file) {
let filesize = Math.floor((file.size) / 1024);
if (filesize > 2048) {
Alert.AlertError("上传大小不能超过2M.")
return false;
}
let formData = new FormData();
formData.append('file', file);
let csrfToken = document.getElementsByName("gorilla.csrf.Token")[0].value
//开始上传图片
$.ajax({
type: "post",
url: self.url,
data: JSON.stringify(requestData),
contentType: "application/json; charset=utf-8",
dataType: "json",
url: '/upload',
type: 'POST',
data: formData,
contentType: false,
processData: false,
headers: {
"X-CSRF-Token": csrfToken
},
success: function (result) {
self.alertSuccessToast("添加成功!")
self.initTableData(self.numPages, '')
}
})
}
// 编辑域名
Domain.prototype.editDomain = function () {
let self = this;
$(".edit-domain").click(function () {
let index = $(this).attr('data-index')
self.initEditForm(index)
})
}
// 初始化编辑域名表单弹窗
Domain.prototype.initEditForm = function (index) {
this.editIndex = index
this.model = true
$('#DomainModalLabel').text("编辑域名")
let data = this.tableData[index]
if (data['category']) {
$('#cdn-t').prop('checked', true)
} else {
$('#cdn-f').prop('checked', true)
}
if (data['boce']) {
$('#boce-t').prop('checked', true)
} else {
$('#boce-f').prop('checked', true)
}
$('#form-key').val(data['auth_key'])
$('#form-domain').val(data['name'])
$('#form-remark').val(data['remark'])
}
// 编辑域名表单提交
Domain.prototype.editSubmit = function () {
let self = this
if (self.model) {
let data = this.tableData[self.editIndex]
let id = data['id']
let boceId = $("input[name='options-boce']:checked").attr("id")
let boce = false
if (boceId === "boce-t") {
boce = true
}
let cdnId = $("input[name='options-cdn']:checked").attr("id")
let cdn = false
if (cdnId === "cdn-t") {
cdn = true
}
let domain = $('#form-domain').val()
let autoKey = $('#form-key').val()
let remark = $('#form-remark').val()
let requestData = {'id': id, 'name': domain, 'auth_key': autoKey, 'remark': remark, 'boce': boce, 'category': cdn}
$("#DomainModal").modal("hide")
if (!self.verifyDomain(domain)) {
self.alertError("域名格式不正确")
return
}
$.ajax({
type: "put",
url: self.url,
data: JSON.stringify(requestData),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
self.alertSuccessToast("编辑成功!")
self.tableData[self.editIndex] = requestData
self.renderPage()
}
})
}
}
// 表单提交策略
Domain.prototype.submitData = function () {
let self = this;
$('#submit-data').click(function () {
if (self.model) {
self.editSubmit()
} else {
self.addSubmit()
}
})
}
// 删除域名
Domain.prototype.deleteDomain = function () {
let self = this;
$('.delete-domain').click(function () {
let index = $(this).attr('data-index')
let requestData = {'id': self.tableData[index]['id']}
self.alertConfirm({
'text': '您确定要删除' + self.tableData[index]['name'] + '吗?',
'confirmCallback': function () {
$.ajax({
type: "delete",
url: self.url,
data: JSON.stringify(requestData),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
self.alertSuccessToast("删除成功!")
self.initTableData(self.currentPage, self.searchText)
}
})
}
})
})
}
// 入口方法
Domain.prototype.run = function () {
let self = this
self.initTableData(self.currentPage, '')
self.addDomain()
self.editDomain()
self.submitData()
self.deleteDomain()
self.domainPageBtn()
self.searchDomain()
}
// 构造执行入口
$(function () {
// 模板过滤方法
if (window.template) {
template.defaults.imports.domainSubstring = function (dateValue) {
if (dateValue.length > 40 ) {
return dateValue.substring(0, 37) + "..."
if (result.status === 200) {
Alert.AlertSuccessToast("上传成功!")
// alert("上传成功!文件名为:" + result.data);
} else {
return dateValue
Alert.AlertError(result.message)
return false;
}
},
error: function () {
Alert.AlertError("服务器内部错误")
}
}
let domain = new Domain()
domain.run()
})
})
}
Index.prototype.UploadClick = function () {
$("#upload-btn-click").click(function () {
$("#upload-input-file").click()
})
}
Index.prototype.run = function () {
this.HoldbackBorwser()
this.MonitorDrop()
this.UploadClick()
}
$(function () {
let index = new Index()
index.run()
})

338
static/js/index.js.back Normal file
View File

@ -0,0 +1,338 @@
function Domain() {
this.tableData = null
this.model = false
this.editIndex = -1
// this.url = "http://127.0.0.1:8300/domain"
this.url = "https://monitor.api.ggbba.top/domain"
this.currentPage = 1
this.pageData = null
this.searchText = ""
this.numPages = 0
}
// 搜索域名
Domain.prototype.searchDomain = function () {
let self = this
$('#search-btn').click(function () {
let search = $('#search-input').val()
self.searchText = search
if (self.searchText) {
self.currentPage = 1
self.initTableData(self.currentPage, self.searchText)
} else {
self.initTableData(1, "")
}
})
}
// 点击分页按钮
Domain.prototype.domainPageBtn = function () {
let self = this
$('.page-btn').click(function () {
self.currentPage = parseInt($(this).attr('data-p'))
self.initTableData(self.currentPage, self.searchText)
})
}
// 分页初始化
Domain.prototype.domainPageInit = function () {
let self = this
// 是否是最后一页
let is_finally = false
// 是否是第一页
let is_first = false
// 总页数
let num_pages = self.pageData['num_pages']
if (self.currentPage === 1) {
is_first = true
}
if (self.currentPage === num_pages) {
is_finally = true
}
self.pageData['is_first'] = is_first
self.pageData['is_finally'] = is_finally
}
// 表单验证
Domain.prototype.verifyDomain = function (domain) {
let patt = /(https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/i
return patt.test(domain)
}
// 错误弹窗
Domain.prototype.alertError = function (msg) {
swal('提示', msg, 'error');
}
// 弹窗
Domain.prototype.alertToast = function (msg, type) {
swal({
'title': msg,
'text': '',
'type': type,
'showCancelButton': false,
'showConfirmButton': false,
'timer': 1000
})
}
// 带确认按钮的弹窗
Domain.prototype.alertConfirm = function (params) {
swal({
'title': params['title'] ? params['title'] : '提示',
'showCancelButton': true,
'showConfirmButton': true,
'type': params['type'] ? params['type'] : '',
'confirmButtonText': params['confirmText'] ? params['confirmText'] : '确定',
'cancelButtonText': params['cancelText'] ? params['cancelText'] : '取消',
'text': params['text'] ? params['text'] : ''
}, function (isConfirm) {
if (isConfirm) {
if (params['confirmCallback']) {
params['confirmCallback']()
}
} else {
if (params['cancelCallback']) {
params['cancelCallback']()
}
}
})
}
// 成功弹窗
Domain.prototype.alertSuccessToast = function (msg) {
if (!msg) {
msg = '成功!'
}
this.alertToast(msg, 'success')
}
// 渲染模板
Domain.prototype.renderPage = function () {
let self = this;
let html = template('tpl-table-tr', {domainList: self.tableData})
$('#tbody-domin').empty().append(html)
html = template('tpl-page-li', {page_data: self.pageData})
$('#page-li').empty().append(html)
self.editDomain()
self.deleteDomain()
self.domainPageBtn()
}
// 初始化数据
Domain.prototype.initTableData = function (num, query) {
let self = this;
let requestUrl = self.url + '/page/' + num
if (query) {
requestUrl += "?search=" + query
}
$.get(requestUrl, function (data, status) {
if (data['code'] === 200) {
self.currentPage = num
self.tableData = data['data']['domain_data']
self.pageData = data['data']['pagination_data']
self.numPages = self.pageData['num_pages']
self.domainPageInit()
self.renderPage()
}
})
}
// 添加域名
Domain.prototype.addDomain = function () {
let self = this;
$("#add-domain").click(function () {
self.initAddForm()
})
}
// 初始化添加域名表单弹窗
Domain.prototype.initAddForm = function () {
this.model = false
$('#DomainModalLabel').text("添加域名")
$('#cdn-f').prop('checked', true)
$('#boce-f').prop('checked', true)
$('#form-key').val("")
$('#form-domain').val("")
$('#form-remark').val("")
}
// 提交添加域名表单
Domain.prototype.addSubmit = function () {
let self = this
let boceId = $("input[name='options-boce']:checked").attr("id")
let boce = false
if (boceId === "boce-t") {
boce = true
}
let cdnId = $("input[name='options-cdn']:checked").attr("id")
let cdn = false
if (cdnId === "cdn-t") {
cdn = true
}
let domain = $('#form-domain').val()
let autoKey = $('#form-key').val()
let remark = $('#form-remark').val()
let requestData = {'name': domain,'auth_key': auto_Key, 'remark': remark, 'boce': boce, 'category': cdn}
$("#DomainModal").modal("hide")
if (!self.verifyDomain(domain)) {
self.alertError("域名格式不正确")
return
}
$.ajax({
type: "post",
url: self.url,
data: JSON.stringify(requestData),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
self.alertSuccessToast("添加成功!")
self.initTableData(self.numPages, '')
}
})
}
// 编辑域名
Domain.prototype.editDomain = function () {
let self = this;
$(".edit-domain").click(function () {
let index = $(this).attr('data-index')
self.initEditForm(index)
})
}
// 初始化编辑域名表单弹窗
Domain.prototype.initEditForm = function (index) {
this.editIndex = index
this.model = true
$('#DomainModalLabel').text("编辑域名")
let data = this.tableData[index]
if (data['category']) {
$('#cdn-t').prop('checked', true)
} else {
$('#cdn-f').prop('checked', true)
}
if (data['boce']) {
$('#boce-t').prop('checked', true)
} else {
$('#boce-f').prop('checked', true)
}
$('#form-key').val(data['auth_key'])
$('#form-domain').val(data['name'])
$('#form-remark').val(data['remark'])
}
// 编辑域名表单提交
Domain.prototype.editSubmit = function () {
let self = this
if (self.model) {
let data = this.tableData[self.editIndex]
let id = data['id']
let boceId = $("input[name='options-boce']:checked").attr("id")
let boce = false
if (boceId === "boce-t") {
boce = true
}
let cdnId = $("input[name='options-cdn']:checked").attr("id")
let cdn = false
if (cdnId === "cdn-t") {
cdn = true
}
let domain = $('#form-domain').val()
let autoKey = $('#form-key').val()
let remark = $('#form-remark').val()
let requestData = {'id': id, 'name': domain, 'auth_key': autoKey, 'remark': remark, 'boce': boce, 'category': cdn}
$("#DomainModal").modal("hide")
if (!self.verifyDomain(domain)) {
self.alertError("域名格式不正确")
return
}
$.ajax({
type: "put",
url: self.url,
data: JSON.stringify(requestData),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
self.alertSuccessToast("编辑成功!")
self.tableData[self.editIndex] = requestData
self.renderPage()
}
})
}
}
// 表单提交策略
Domain.prototype.submitData = function () {
let self = this;
$('#submit-data').click(function () {
if (self.model) {
self.editSubmit()
} else {
self.addSubmit()
}
})
}
// 删除域名
Domain.prototype.deleteDomain = function () {
let self = this;
$('.delete-domain').click(function () {
let index = $(this).attr('data-index')
let requestData = {'id': self.tableData[index]['id']}
self.alertConfirm({
'text': '您确定要删除' + self.tableData[index]['name'] + '吗?',
'confirmCallback': function () {
$.ajax({
type: "delete",
url: self.url,
data: JSON.stringify(requestData),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
self.alertSuccessToast("删除成功!")
self.initTableData(self.currentPage, self.searchText)
}
})
}
})
})
}
// 入口方法
Domain.prototype.run = function () {
let self = this
self.initTableData(self.currentPage, '')
self.addDomain()
self.editDomain()
self.submitData()
self.deleteDomain()
self.domainPageBtn()
self.searchDomain()
}
// 构造执行入口
$(function () {
// 模板过滤方法
if (window.template) {
template.defaults.imports.domainSubstring = function (dateValue) {
if (dateValue.length > 40 ) {
return dateValue.substring(0, 37) + "..."
} else {
return dateValue
}
}
}
let domain = new Domain()
domain.run()
})

76
static/js/login.js Normal file
View File

@ -0,0 +1,76 @@
function Login() {
this.captchaUrl = "/captcha"
this.userName = ""
this.password = ""
this.captcha = ""
}
Login.prototype.RefreshCaptcha = function () {
let self = this;
$("#captcha-img").click(function () {
let timestamp = new Date().getTime();
self.captchaUrl = "/captcha" + "?t=" + timestamp
$("#captcha-img").attr('src', self.captchaUrl); //显示图片
})
}
//
Login.prototype.LoginClick = function () {
let self = this;
let csrfToken = document.getElementsByName("gorilla.csrf.Token")[0].value
$("#login-btn").click(function (e) {
e.preventDefault();
// 获取参数
let username = $("#login-username").val()
let password = $("#login-password").val()
let captcha = $("#login-captcha").val()
$.ajax({
url: '/login',
method: 'POST',
data: JSON.stringify({
username: username,
password: password,
captcha: captcha
}),
headers: {
'Content-Type': 'application/json',
"X-CSRF-Token": csrfToken
},
success: function (result) {
if (result.status === 200) {
Alert.AlertSuccessToast(result.message)
window.location.href = "/"
} else {
Alert.AlertError(result.message)
}
},
error: function (xhr, status, error) {
Alert.AlertError("服务器内部错误")
console.log('请求失败:', error);
}
});
})
}
Login.prototype.run = function () {
this.RefreshCaptcha()
this.LoginClick()
}
// 构造执行入口
$(function () {
feather.replace()
// 模板过滤方法
// if (window.template) {
// template.defaults.imports.domainSubstring = function (dateValue) {
// if (dateValue.length > 40 ) {
// return dateValue.substring(0, 37) + "..."
// } else {
// return dateValue
// }
// }
// }
let login = new Login()
login.run()
})

45
view/domain.html Normal file
View File

@ -0,0 +1,45 @@
{{define "style"}}
{{end}}
{{define "content"}}
<div class="alert alert-primary form-inline" role="alert">
<button type="button" id="add-domain" class="btn btn-primary" data-toggle="modal"
data-target="#DomainModal"><i
class="bi bi-plus-lg"></i> 添加域名
</button>
<div class="input-group mr-3" style="position: absolute; right: 0">
<input type="text" id="search-input" class="form-control" placeholder="域名" aria-label="域名" aria-describedby="searc-btn">
<div class="input-group-append">
<button class="btn btn-primary" id="search-btn" type="button"><i class="bi bi-search"></i> 搜索
</button>
</div>
</div>
</div>
<table class="table table-bordered">
<thead>
<tr class="table-primary">
<th scope="col">#</th>
<th scope="col">First</th>
<th scope="col">Last</th>
<th scope="col">Handle</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1</th>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
<tr>
<th scope="row">2</th>
<td>Jacob</td>
<td>Thornton</td>
<td>@fat</td>
</tr>
</tbody>
</table>
{{end}}
{{define "script"}}
{{end}}

View File

@ -1,217 +1,60 @@
<!doctype html>
<html lang="zh-CN">
{{define "style"}}
<link href="/static/css/index.css" rel="stylesheet">
{{end}}
{{define "content"}}
<div class="card text-center">
<div class="card-header" style="background-color: #fcf8e3">
<h4>图片上传</h4>
</div>
<div class="card-body" style="border-style:dashed; border-color:#98bf21; display: block">
<input type="file" class="form-control-file" id="upload-input-file" style="display:none;">
<div id="upload-btn-click">
<div style="height: 80px"></div>
<div class="ant-upload-drag-container">
<p class="ant-upload-drag-icon"><img src="/static/img/add.svg"></p>
<p class="ant-upload-text" style="font-size: 14px; margin: 0;">拖拽文件到此区域或<span
style="color: rgb(26, 102, 255);">点此上传</span></p>
<p class="ant-upload-hint" style="font-size: 12px; margin: 4px 0 0;">
只支持上传图片文件最大支持2M上传后支持复制url</p>
</div>
<div style="height: 80px"></div>
</div>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="../static/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../static/css/bootstrap.min.css">
<link rel="stylesheet" href="../static/css/bootstrap-icons.css">
<link rel="stylesheet" href="../static/css/sweetalert.css">
<title>域名监控</title>
</head>
<body>
<div class="container-fluid" style="padding-left: 0; padding-right: 0">
<nav class="navbar justify-content-center navbar-dark bg-dark">
<span class="navbar-brand mb-0 h1">域名监控</span>
</nav>
</div>
<div class="container">
<div class="shadow p-3 bg-white rounded mt-3">
<div class="alert alert-primary form-inline" role="alert">
<button type="button" id="add-domain" class="btn btn-primary" data-toggle="modal"
data-target="#DomainModal"><i
class="bi bi-plus-lg"></i> 添加域名
</button>
<div class="input-group mr-3" style="position: absolute; right: 0">
<input type="text" id="search-input" class="form-control" placeholder="域名" aria-label="域名" aria-describedby="searc-btn">
<div class="input-group-append">
<button class="btn btn-primary" id="search-btn" type="button"><i class="bi bi-search"></i> 搜索
</button>
</div>
<div class="card-footer text-muted" style="background-color: #fcf8e3">
<div class="container">
<div class="row" id="preview">
<div class="col-sm">
<div class="card" style="width: 14rem;">
<img src="http://img.520touxiang.com/uploads/allimg/2016063019/4b1sswvy0mj.jpg" class="card-img-top" alt="">
<div class="card-body">
<p>http://img.520touxiang.com/uploads/allimg/2016063019/4b1sswvy0mj.jpg</p>
<button type="button" class="btn btn-primary img-copy-url-btn">复制链接</button>
</div>
</div>
</div>
<div class="col-sm">
<div class="card" style="width: 14rem;">
<img src="http://img.520touxiang.com/uploads/allimg/2016063019/4b1sswvy0mj.jpg" class="card-img-top" alt="">
<div class="card-body">
<p>http://img.520touxiang.com/uploads/allimg/2016063019/4b1sswvy0mj.jpg</p>
<button type="button" class="btn btn-primary img-copy-url-btn">复制链接</button>
</div>
</div>
</div>
</div>
</div>
<div class="bg-light">
<table class="table table-bordered text-nowrap">
<thead>
<tr class="table-primary">
<th scope="col">编号</th>
<th scope="col">域名</th>
<th scope="col">Boce</th>
<th scope="col">备注</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody id="tbody-domin">
<script id="tpl-table-tr" type="text/html">
{{each domainList value index }}
<tr>
<td>{{index}}</td>
<td>
<span class="d-inline-block" tabindex="0" data-toggle="tooltip" data-placement="top"
title="{{value.name}}">
{{value.name|domainSubstring}}
</span>
</td>
{{if value.boce}}
<td><span class="badge rounded-pill bg-success">True</span></td>
{{else}}
<td><span class="badge rounded-pill bg-secondary">False</span></td>
{{/if}}
<td>{{value.remark}}</td>
<td>
<button type="button" class="btn btn-sm btn-primary"
onclick="window.open('{{value.name}}')"><i
class="bi bi-link-45deg"></i> 打开
</button>
<button type="button" class="btn btn-sm btn-warning edit-domain" data-index="{{index}}"
data-toggle="modal"
data-target="#DomainModal"><i
class="bi bi-pencil-square"></i> 编辑
</button>
<button type="button" class="btn btn-sm btn-danger delete-domain"
data-index="{{index}}"><i
class="bi bi-trash3"></i> 删除
</button>
</td>
</tr>
{{/each}}
</script>
</tbody>
</table>
<!-- 分页 -->
<nav>
<ul class="pagination justify-content-end" id="page-li">
<script id="tpl-page-li" type="text/html">
<!-- 上一页 -->
<li class="page-item{{if page_data.is_first}} disabled{{/if}}">
<a class="page-link page-btn" data-p="{{page_data.current_page-1}}"
href="javascript:void(0)">上一页</a>
</li>
<!-- 当前页左边是否显示 ... -->
{{if page_data.left_has_more }}
<li class="page-item"><a class="page-link page-btn" data-p="1" href="javascript:void(0)">1</a>
</li>
<li class="page-item"><span class="page-link">...</span></li>
{{/if}}
<!-- 当前页左边显示按钮 -->
{{each page_data.left_pages lp}}
<li class="page-item"><a class="page-link page-btn" data-p="{{lp}}" href="javascript:void(0)">{{lp}}</a>
</li>
{{/each}}
<!-- 当前页 -->
<li class="page-item active"><span class="page-link">{{page_data.current_page}}</span></li>
<!-- 当前页右边是否显示 ... -->
{{each page_data.right_pages rp}}
<li class="page-item"><a class="page-link page-btn" data-p="{{rp}}" href="javascript:void(0)">{{rp}}</a>
</li>
{{/each}}
<!-- 当前页右边显示按钮 -->
{{if page_data.right_has_more }}
<li class="page-item"><span class="page-link">...</span></li>
<li class="page-item"><a class="page-link page-btn" data-p="{{page_data.num_pages}}"
href="javascript:void(0)">{{page_data.num_pages}}</a></li>
{{/if}}
<!-- 下一页 -->
<li class="page-item{{if page_data.is_finally}} disabled{{/if}}">
<a class="page-link page-btn" data-p="{{page_data.current_page+1}}"
href="javascript:void(0)">下一页</a>
</li>
</script>
</ul>
</nav>
</div>
{{end}}
{{define "script"}}
{{noRender `<script id="tpl-image" type="text/html">
<div class="card" style="width: 18rem;">
<img src="{{ url }}" class="card-img-top" alt="...">
<div class="card-body">
<button type="button" class="btn btn-primary">点击复制按钮</button>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="DomainModal" tabindex="-1" role="dialog" aria-labelledby="DomainModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="DomainModalLabel" modal-data="0">添加域名</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form class="col-auto">
<div class="form-group row">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text alert-primary">域名:</div>
</div>
<input type="text" id="form-domain" class="form-control" placeholder="域名">
</div>
</div>
<div class="form-group row">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text alert-primary">备注:</div>
</div>
<input type="text" id="form-remark" class="form-control" placeholder="备注">
</div>
</div>
<div class="form-group row">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text alert-primary">秘钥:</div>
</div>
<input type="text" id="form-key" class="form-control" placeholder="cdn鉴权秘钥">
</div>
</div>
<div class="form-group row">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text alert-primary">CDN: </div>
</div>
<div class="btn-group" data-toggle="buttons-cdn">
<label class="btn btn-light" style="margin-bottom: 0">
<input type="radio" name="options-cdn" id="cdn-t" autocomplete="off">True
</label>
<label class="btn btn-light" style="margin-bottom: 0">
<input type="radio" name="options-cdn" id="cdn-f" autocomplete="off" checked>False
</label>
<label style="margin-left:10px;color: red;height: 30px;line-height: 38px;">是否需要鉴权</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text alert-primary">Boce: </div>
</div>
<div class="btn-group" data-toggle="buttons-boce">
<label class="btn btn-light" style="margin-bottom: 0">
<input type="radio" name="options-boce" id="boce-t" autocomplete="off">True
</label>
<label class="btn btn-light" style="margin-bottom: 0">
<input type="radio" name="options-boce" id="boce-f" autocomplete="off" checked>False
</label>
<label style="margin-left:10px;color: red;height: 30px;line-height: 38px;">是否加到boce、17ce测试</label>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="submit-data">提交</button>
</div>
</div>
</div>
</div>
<script src="../static/js/jquery-3.3.1.min.js"></script>
<script src="../static/js/popper.min.js"></script>
<script src="../static/js/bootstrap.min.js"></script>
<script src="../static/js/template-web.js"></script>
<script src="../static/js/sweetalert.min.js"></script>
<script src="../static/js/index.js"></script>
</body>
</html>
</script>`}}
<script src="/static/js/index.js"></script>
{{end}}

63
view/layout.html Normal file
View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="shortcut icon" href="/static/img/favicon.ico">
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/sweetalert.css" rel="stylesheet">
<script src="/static/js/jquery-3.3.1.min.js"></script>
<script src="/static/js/popper.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<!-- <script src="/static/js/feather-icons.js"></script>-->
<script src="/static/js/sweetalert.min.js"></script>
<script src="/static/js/common.js"></script>
{{template "style" .}}
<title>{{.Title}}</title>
</head>
<body>
<div class="container-fluid bg-dark" style="padding-left: 0; padding-right: 0">
<div class="container" style="padding-left: 0; padding-right: 0">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">后台管理</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse pl-5" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {{if eq .Active "/"}}active{{end}}" href="/">图片上传</a>
</li>
{{ if .IsSuper }}
<li class="nav-item">
<a class="nav-link {{if eq .Active "/user"}}active{{end}}" href="/user">用户管理</a>
</li>
<li class="nav-item">
<a class="nav-link {{if eq .Active "/domain"}}active{{end}}" href="/domain">域名管理</a>
</li>
<li class="nav-item">
<a class="nav-link {{if eq .Active "/picture"}}active{{end}}" href="/picture">图片管理</a>
</li>
{{ end }}
</ul>
</div>
</nav>
</div>
</div>
<div class="container" style="padding: 20px">
{{ .csrfField }}
{{template "content" .}}
</div>
<script>
$(function () {
let pathname = window.location.pathname
let links = $("nav-link");
for (let i = 0; i < links.length; i++) {
}
})
</script>
{{template "script" .}}
</body>
</html>

View File

@ -1,54 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Login Page</title>
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<link rel="shortcut icon" href="/static/img/favicon.ico">
<title>后台登录</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/sweetalert.css" rel="stylesheet">
<script src="/static/js/jquery-3.3.1.min.js"></script>
<script src="/static/js/popper.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script src="/static/js/feather-icons.js"></script>
<script src="/static/js/sweetalert.min.js"></script>
<script src="/static/js/common.js"></script>
<style>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-form {
width: 100%;
max-width: 420px;
padding: 15px;
margin: auto;
}
.login-container {min-height: 100vh;display: flex;align-items: center;justify-content: center;}
.login-form {width: 100%;max-width: 420px;padding: 15px;margin: auto;border-radius: 12px;}
</style>
</head>
<body>
<div class="login-container">
<div class="login-form card shadow-sm">
<div class="card-body">
<h3 class="card-title text-center">Login</h3>
<h3 class="card-title text-center">后台登录</h3>
<form action="/login" method="POST">
<div class="form-group">
<label for="email">Email address</label>
<input type="email" class="form-control" id="email" name="email" placeholder="Enter email" required>
{{ .csrfField }}
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="username-addon">
<span data-feather="user"></span>
</span>
</div>
<input type="text" id="login-username" class="form-control" name="username" placeholder="用户名" aria-label="用户名"
aria-describedby="username-addon" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="password-addon">
<span data-feather="pocket"></span>
</span>
</div>
<input type="password" id="login-password" class="form-control" name="password" placeholder="密码" aria-label="密码"
aria-describedby="password-addon" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Login</button>
<p class="text-center mt-3">
<a href="#">Forgot your password?</a>
</p>
<p class="text-center">
<a href="#">Register</a>
</p>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text"> <span data-feather="image"></span></span>
</div>
<input type="text" class="form-control" id="login-captcha" name="captcha" placeholder="验证码" required>
<div class="input-group-append" style="padding-left: 4px">
<img src="/captcha" style="width: 96px;border: 1px solid #ced4da;border-radius: 4px;" alt="验证码" id="captcha-img">
</div>
</div>
<button type="button" id="login-btn" class="btn btn-primary btn-block">登录</button>
</form>
</div>
</div>
</div>
<!-- Bootstrap JS and dependencies -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.3/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script src="/static/js/login.js"></script>
</body>
</html>

10
view/picture.html Normal file
View File

@ -0,0 +1,10 @@
{{define "style"}}
{{end}}
{{define "content"}}
<h1 class="h2">Picture</h1>
<p>Picture page content goes here.</p>
{{end}}
{{define "script"}}
{{end}}

10
view/user.html Normal file
View File

@ -0,0 +1,10 @@
{{define "style"}}
{{end}}
{{define "content"}}
<h1 class="h2">User</h1>
<p>User page content goes here.</p>
{{end}}
{{define "script"}}
{{end}}