Compare commits
10 Commits
78ebb59142
...
6618c0b6c2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6618c0b6c2 | ||
![]() |
65a1d849b6 | ||
![]() |
534466b40b | ||
![]() |
a4c0ce45e9 | ||
![]() |
64b776254e | ||
![]() |
3dc9fbeaf8 | ||
![]() |
35cb1f9025 | ||
![]() |
90b4e437fe | ||
6e7b9cc89c | |||
d13230a68f |
51
.air.toml
Normal file
51
.air.toml
Normal 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
6
.gitignore
vendored
@ -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
111
cmd/picgo.go
Normal 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
8
configs/captcha.go
Normal 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
40
configs/configs.go
Normal 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
7
configs/mysql.go
Normal 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
9
configs/redis.go
Normal 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
14
configs/server.go
Normal 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"`
|
||||
}
|
68
corelib/captcha/captcha.go
Normal file
68
corelib/captcha/captcha.go
Normal 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
|
||||
}
|
BIN
corelib/captcha/fonts/3Dumb.ttf
Normal file
BIN
corelib/captcha/fonts/3Dumb.ttf
Normal file
Binary file not shown.
BIN
corelib/captcha/fonts/DENNEthree-dee.ttf
Normal file
BIN
corelib/captcha/fonts/DENNEthree-dee.ttf
Normal file
Binary file not shown.
339
corelib/captcha/image.go
Normal file
339
corelib/captcha/image.go
Normal 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:普通字符串,1:10以内简单数学公式
|
||||
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
24
corelib/captcha/random.go
Normal 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
46
corelib/captcha/store.go
Normal 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
34
corelib/encryption.go
Normal 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
13
corelib/json.go
Normal 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
103
corelib/log.go
Normal 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
111
corelib/msyql.go
Normal 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
48
corelib/rand.go
Normal 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
30
corelib/redis.go
Normal 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
38
corelib/regexp.go
Normal 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)
|
||||
}
|
@ -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
18
corelib/session.go
Normal 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
44
corelib/template.go
Normal 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
62
data/user.go
Normal 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
52
deploy/picgo-dev.yml
Normal 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
36
go.mod
@ -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
82
go.sum
@ -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
86
handler/captcha.go
Normal 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
31
handler/domain.go
Normal 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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
32
handler/picture.go
Normal 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")
|
||||
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
32
handler/user.go
Normal 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
15
main.go
@ -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
37
middleware/auth.go
Normal 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
21
middleware/cors.go
Normal 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
26
model/base.go
Normal 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
13
model/domin.go
Normal 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
7
model/login_request.go
Normal 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
13
model/upload.go
Normal 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
16
model/user.go
Normal 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"
|
||||
}
|
@ -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
0
static/css/index.css
Normal file
22
static/img/add.svg
Normal file
22
static/img/add.svg
Normal 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
55
static/js/common.js
Normal 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()
|
||||
|
||||
|
13
static/js/feather-icons.js
Normal file
13
static/js/feather-icons.js
Normal file
File diff suppressed because one or more lines are too long
@ -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
338
static/js/index.js.back
Normal 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
76
static/js/login.js
Normal 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
45
view/domain.html
Normal 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}}
|
265
view/index.html
265
view/index.html
@ -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">×</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
63
view/layout.html
Normal 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>
|
@ -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
10
view/picture.html
Normal 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
10
view/user.html
Normal 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}}
|
Loading…
Reference in New Issue
Block a user