æ·feat: commplete init process

This commit is contained in:
engigu
2023-12-30 17:40:20 +08:00
parent 9307bec4ab
commit 77af985b76
92 changed files with 12054 additions and 2 deletions
+12 -1
View File
@@ -15,7 +15,18 @@
*.out *.out
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ vendor/
# Go workspace file # Go workspace file
go.work go.work
# web sources
/web/node_modules
/web/.vite
/web/.vscode
# Project ignore
/test
/conf
/runtime
+9
View File
@@ -0,0 +1,9 @@
FROM golang:latest
ENV GOPROXY https://goproxy.cn,direct
WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example
RUN go build .
EXPOSE 8000
ENTRYPOINT ["./go-gin-example"]
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) The go-gin-example Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+23
View File
@@ -0,0 +1,23 @@
.PHONY: build clean tool lint help
all: build
build:
@go build -v .
tool:
go vet ./...; true
gofmt -w .
lint:
golint ./...
clean:
rm -rf go-gin-example
go clean -i .
help:
@echo "make: compile packages and dependencies"
@echo "make tool: run specified go tool"
@echo "make lint: golint ./..."
@echo "make clean: remove object files and cached files"
+89 -1
View File
@@ -1 +1,89 @@
# notice-push # Go Gin Example [![rcard](https://goreportcard.com/badge/github.com/EDDYCJY/go-gin-example)](https://goreportcard.com/report/github.com/EDDYCJY/go-gin-example) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://godoc.org/github.com/EDDYCJY/go-gin-example) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/EDDYCJY/go-gin-example/master/LICENSE)
An example of gin contains many useful features
[简体中文](https://message-nest/blob/master/README_ZH.md)
## Installation
```
$ go get github.com/EDDYCJY/go-gin-example
```
## How to run
### Required
- Mysql
- Redis
### Ready
Create a **blog database** and import [SQL](https://message-nest/blob/master/docs/sql/blog.sql)
### Conf
You should modify `conf/app.ini`
```
[database]
Type = mysql
User = root
Password =
Host = 127.0.0.1:3306
Name = blog
TablePrefix = blog_
[redis]
Host = 127.0.0.1:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200
...
```
### Run
```
$ cd $GOPATH/src/go-gin-example
$ go run main.go
```
Project information and existing API
```
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /auth --> message-nest/routers/api.GetAuth (3 handlers)
[GIN-debug] GET /swagger/*any --> message-nest/vendor/github.com/swaggo/gin-swagger.WrapHandler.func1 (3 handlers)
[GIN-debug] GET /api/v1/tags --> message-nest/routers/api/v1.GetTags (4 handlers)
[GIN-debug] POST /api/v1/tags --> message-nest/routers/api/v1.AddTag (4 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> message-nest/routers/api/v1.EditTag (4 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> message-nest/routers/api/v1.DeleteTag (4 handlers)
[GIN-debug] GET /api/v1/articles --> message-nest/routers/api/v1.GetArticles (4 handlers)
[GIN-debug] GET /api/v1/articles/:id --> message-nest/routers/api/v1.GetArticle (4 handlers)
[GIN-debug] POST /api/v1/articles --> message-nest/routers/api/v1.AddArticle (4 handlers)
[GIN-debug] PUT /api/v1/articles/:id --> message-nest/routers/api/v1.EditArticle (4 handlers)
[GIN-debug] DELETE /api/v1/articles/:id --> message-nest/routers/api/v1.DeleteArticle (4 handlers)
Listening port is 8000
Actual pid is 4393
```
Swagger doc
![image](https://i.imgur.com/bVRLTP4.jpg)
## Features
- RESTful API
- Gorm
- Swagger
- logging
- Jwt-go
- Gin
- Graceful restart or stop (fvbock/endless)
- App configurable
- Cron
- Redis
+116
View File
@@ -0,0 +1,116 @@
# Go Gin Example [![rcard](https://goreportcard.com/badge/github.com/EDDYCJY/go-gin-example)](https://goreportcard.com/report/github.com/EDDYCJY/go-gin-example) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://godoc.org/github.com/EDDYCJY/go-gin-example) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/EDDYCJY/go-gin-example/master/LICENSE)
`gin` 的一个例子,包含许多有用特性
## 目录
本项目提供 [Gin实践](https://segmentfault.com/a/1190000013297625) 的连载示例代码
1. [Gin实践 连载一 Golang介绍与环境安装](https://book.eddycjy.com/golang/gin/install.html)
2. [Gin实践 连载二 搭建Blog API's(一)](https://book.eddycjy.com/golang/gin/api-01.html)
3. [Gin实践 连载三 搭建Blog API's(二)](https://book.eddycjy.com/golang/gin/api-02.html)
4. [Gin实践 连载四 搭建Blog API's(三)](https://book.eddycjy.com/golang/gin/api-03.html)
5. [Gin实践 连载五 使用JWT进行身份校验](https://book.eddycjy.com/golang/gin/jwt.html)
6. [Gin实践 连载六 编写一个简单的文件日志](https://book.eddycjy.com/golang/gin/log.html)
7. [Gin实践 连载七 Golang优雅重启HTTP服务](https://book.eddycjy.com/golang/gin/reload-http.html)
8. [Gin实践 连载八 为它加上Swagger](https://book.eddycjy.com/golang/gin/swagger.html)
9. [Gin实践 连载九 将Golang应用部署到Docker](https://book.eddycjy.com/golang/gin/golang-docker.html)
10. [Gin实践 连载十 定制 GORM Callbacks](https://book.eddycjy.com/golang/gin/gorm-callback.html)
11. [Gin实践 连载十一 Cron定时任务](https://book.eddycjy.com/golang/gin/cron.html)
12. [Gin实践 连载十二 优化配置结构及实现图片上传](https://book.eddycjy.com/golang/gin/config-upload.html)
13. [Gin实践 连载十三 优化你的应用结构和实现Redis缓存](https://book.eddycjy.com/golang/gin/application-redis.html)
14. [Gin实践 连载十四 实现导出、导入 Excel](https://book.eddycjy.com/golang/gin/excel.html)
15. [Gin实践 连载十五 生成二维码、合并海报](https://book.eddycjy.com/golang/gin/image.html)
16. [Gin实践 连载十六 在图片上绘制文字](https://book.eddycjy.com/golang/gin/font.html)
17. [Gin实践 连载十七 用 Nginx 部署 Go 应用](https://book.eddycjy.com/golang/gin/nginx.html)
18. [Gin实践 番外 Golang交叉编译](https://book.eddycjy.com/golang/gin/cgo.html)
19. [Gin实践 番外 请入门 Makefile](https://book.eddycjy.com/golang/gin/makefile.html)
## 安装
```
$ go get github.com/EDDYCJY/go-gin-example
```
## 如何运行
### 必须
- Mysql
- Redis
### 准备
创建一个 `blog` 数据库,并且导入建表的 [SQL](https://message-nest/blob/master/docs/sql/blog.sql)
### 配置
你应该修改 `conf/app.ini` 配置文件
```
[database]
Type = mysql
User = root
Password = rootroot
Host = 127.0.0.1:3306
Name = blog
TablePrefix = blog_
[redis]
Host = 127.0.0.1:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200
...
```
### 运行
```
$ cd $GOPATH/src/go-gin-example
$ go run main.go
```
项目的运行信息和已存在的 API's
```
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /auth --> message-nest/routers/api.GetAuth (3 handlers)
[GIN-debug] GET /swagger/*any --> message-nest/vendor/github.com/swaggo/gin-swagger.WrapHandler.func1 (3 handlers)
[GIN-debug] GET /api/v1/tags --> message-nest/routers/api/v1.GetTags (4 handlers)
[GIN-debug] POST /api/v1/tags --> message-nest/routers/api/v1.AddTag (4 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> message-nest/routers/api/v1.EditTag (4 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> message-nest/routers/api/v1.DeleteTag (4 handlers)
[GIN-debug] GET /api/v1/articles --> message-nest/routers/api/v1.GetArticles (4 handlers)
[GIN-debug] GET /api/v1/articles/:id --> message-nest/routers/api/v1.GetArticle (4 handlers)
[GIN-debug] POST /api/v1/articles --> message-nest/routers/api/v1.AddArticle (4 handlers)
[GIN-debug] PUT /api/v1/articles/:id --> message-nest/routers/api/v1.EditArticle (4 handlers)
[GIN-debug] DELETE /api/v1/articles/:id --> message-nest/routers/api/v1.DeleteArticle (4 handlers)
Listening port is 8000
Actual pid is 4393
```
Swagger 文档
![image](https://i.imgur.com/bVRLTP4.jpg)
## 特性
- RESTful API
- Gorm
- Swagger
- logging
- Jwt-go
- Gin
- Graceful restart or stop (fvbock/endless)
- App configurable
- Cron
- Redis
## 联系我
![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)
+56
View File
@@ -0,0 +1,56 @@
module message-nest
go 1.20
require (
github.com/astaxie/beego v1.9.3-0.20171218111859-f16688817aa4
github.com/dgrijalva/jwt-go v3.1.0+incompatible
github.com/gin-gonic/gin v1.9.1
github.com/go-ini/ini v1.32.1-0.20180214101753-32e4be5f41bb
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.16.0
github.com/google/uuid v1.5.0
github.com/jinzhu/gorm v0.0.0-20180213101209-6e1387b44c64
github.com/sirupsen/logrus v1.9.3
github.com/unknwon/com v1.0.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/denisenkom/go-mssqldb v0.0.0-20190920000552-128d9f4ae1cd // indirect
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-sql-driver/mysql v1.4.1-0.20190510102335-877a9775f068 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d // indirect
github.com/jinzhu/now v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lib/pq v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.11.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.3 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/ini.v1 v1.47.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+161
View File
@@ -0,0 +1,161 @@
github.com/astaxie/beego v1.9.3-0.20171218111859-f16688817aa4 h1:dNIynF6ICiq1NghlpIBxljb2JbyC61/JqWB5A9cfUfo=
github.com/astaxie/beego v1.9.3-0.20171218111859-f16688817aa4/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/denisenkom/go-mssqldb v0.0.0-20190920000552-128d9f4ae1cd h1:tXCgEGHPT4XELDS7nB0OBb9968yCOd+MnyNf+6m1u40=
github.com/denisenkom/go-mssqldb v0.0.0-20190920000552-128d9f4ae1cd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.1.0+incompatible h1:FFziAwDQQ2dz1XClWMkwvukur3evtZx7x/wMHKM1i20=
github.com/dgrijalva/jwt-go v3.1.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-ini/ini v1.32.1-0.20180214101753-32e4be5f41bb h1:v+YnQ81wH+hTjaP5nFSpqASFbe9UETYm1vG65qp7Zc0=
github.com/go-ini/ini v1.32.1-0.20180214101753-32e4be5f41bb/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.4.1-0.20190510102335-877a9775f068 h1:q2kwd9Bcgl2QpSi/Wjcx9jzwyICt3EWTP5to43QhwaA=
github.com/go-sql-driver/mysql v1.4.1-0.20190510102335-877a9775f068/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jinzhu/gorm v0.0.0-20180213101209-6e1387b44c64 h1:8I4kQ5M5OjZKNsgRUs20soTdIoo1GbiGApV31kJ9e6Y=
github.com/jinzhu/gorm v0.0.0-20180213101209-6e1387b44c64/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
google.golang.org/appengine v1.6.3 h1:hvZejVcIxAKHR8Pq2gXaDggf6CWT1QEqO+JEBeOKCG8=
google.golang.org/appengine v1.6.3/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.47.0 h1:XAVsOWcIxjm6JVEbCbSZgSBZIF0BrCzXs4orAQr6uc8=
gopkg.in/ini.v1 v1.47.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+55
View File
@@ -0,0 +1,55 @@
package main
import (
"fmt"
"log"
"message-nest/pkg/table"
"net/http"
"github.com/gin-gonic/gin"
"message-nest/models"
"message-nest/pkg/logging"
"message-nest/pkg/setting"
"message-nest/pkg/util"
"message-nest/routers"
)
func init() {
setting.Setup()
models.Setup()
logging.Setup()
util.Setup()
table.Setup()
}
// @title Golang Gin API
// @version 1.0
// @description An example of gin
// @termsOfService https://github.com/EDDYCJY/go-gin-example
// @license.name MIT
// @license.url https://message-nest/blob/master/LICENSE
func main() {
gin.SetMode(setting.ServerSetting.RunMode)
routersInit := routers.InitRouter()
readTimeout := setting.ServerSetting.ReadTimeout
writeTimeout := setting.ServerSetting.WriteTimeout
endPoint := fmt.Sprintf(":%d", setting.ServerSetting.HttpPort)
maxHeaderBytes := 1 << 20
server := &http.Server{
Addr: endPoint,
Handler: routersInit,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
MaxHeaderBytes: maxHeaderBytes,
}
log.Printf("[info] start http server listening http://0.0.0.0%s", endPoint)
err := server.ListenAndServe()
if err != nil {
log.Printf("Server err: %v", err)
}
}
+26
View File
@@ -0,0 +1,26 @@
package middleware
import (
"github.com/gin-gonic/gin"
"net/http"
)
// Cors 直接放行所有跨域请求并放行所有 OPTIONS 方法
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id,M-Token")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At")
c.Header("Access-Control-Allow-Credentials", "true")
// 放行所有OPTIONS方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// 处理请求
c.Next()
}
}
+61
View File
@@ -0,0 +1,61 @@
package middleware
import (
"github.com/dgrijalva/jwt-go"
"net/http"
"github.com/gin-gonic/gin"
"message-nest/pkg/e"
"message-nest/pkg/util"
)
var ExcludedRoutes = []string{
"/api/v1/message/send",
}
// JWT is jwt middleware
func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.FullPath()
for _, route := range ExcludedRoutes {
if path == route {
c.Next()
return
}
}
var code int
var data interface{}
code = e.SUCCESS
token := c.Request.Header.Get("M-Token")
if token == "" {
code = e.ERROR_AUTH_NO_TOKEN
} else {
claims, err := util.ParseToken(token)
if err != nil {
switch err.(*jwt.ValidationError).Errors {
case jwt.ValidationErrorExpired:
code = e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT
default:
code = e.ERROR_AUTH_CHECK_TOKEN_FAIL
}
} else {
c.Set("currentUserName", claims.Username)
}
}
if code != e.SUCCESS {
c.JSON(http.StatusUnauthorized, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": data,
})
c.Abort()
return
}
c.Next()
}
}
+32
View File
@@ -0,0 +1,32 @@
package models
import "github.com/jinzhu/gorm"
type Auth struct {
ID int `gorm:"primary_key" json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
// CheckAuth checks if authentication information exists
func CheckAuth(username, password string) (bool, error) {
var auth Auth
err := db.Select("id").Where(Auth{Username: username, Password: password}).First(&auth).Error
if err != nil && err != gorm.ErrRecordNotFound {
return false, err
}
if auth.ID > 0 {
return true, nil
}
return false, nil
}
// EditUser 编辑用户信息
func EditUser(username string, data interface{}) error {
if err := db.Model(&Auth{}).Where("username = ? ", username).Updates(data).Error; err != nil {
return err
}
return nil
}
+124
View File
@@ -0,0 +1,124 @@
package models
import (
"fmt"
"github.com/google/uuid"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"log"
"message-nest/pkg/setting"
"message-nest/pkg/util"
"time"
)
var db *gorm.DB
//type Model struct {
// ID int `gorm:"primary_key" json:"id"`
// CreatedOn util.Time `json:"created_on"`
// ModifiedOn util.Time `json:"modified_on"`
//}
type UUIDModel struct {
ID uuid.UUID `gorm:"primary_key" json:"id"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
CreatedOn util.Time `json:"created_on"`
ModifiedOn util.Time `json:"modified_on"`
}
// Setup initializes the database instance
func Setup() {
var err error
db, err = gorm.Open(setting.DatabaseSetting.Type, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
setting.DatabaseSetting.User,
setting.DatabaseSetting.Password,
setting.DatabaseSetting.Host,
setting.DatabaseSetting.Name))
if err != nil {
log.Fatalf("models.Setup err: %v", err)
}
gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
return setting.DatabaseSetting.TablePrefix + defaultTableName
}
db.LogMode(true)
db.SingularTable(true)
db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback)
db.Callback().Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback)
db.Callback().Delete().Replace("gorm:delete", deleteCallback)
db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)
}
// CloseDB closes database connection (unnecessary)
func CloseDB() {
defer db.Close()
}
// updateTimeStampForCreateCallback will set `CreatedOn`, `ModifiedOn` when creating
func updateTimeStampForCreateCallback(scope *gorm.Scope) {
if !scope.HasError() {
nowTime := time.Now()
fmt.Printf("fyyyyyynowTime %s", nowTime)
if createTimeField, ok := scope.FieldByName("CreatedOn"); ok {
if createTimeField.IsBlank {
createTimeField.Set(nowTime)
}
}
if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok {
if modifyTimeField.IsBlank {
modifyTimeField.Set(nowTime)
}
}
}
}
// updateTimeStampForUpdateCallback will set `ModifiedOn` when updating
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
if _, ok := scope.Get("gorm:update_column"); !ok {
scope.SetColumn("ModifiedOn", time.Now())
}
}
// deleteCallback will set `DeletedOn` where deleting
func deleteCallback(scope *gorm.Scope) {
if !scope.HasError() {
var extraOption string
if str, ok := scope.Get("gorm:delete_option"); ok {
extraOption = fmt.Sprint(str)
}
deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn")
if !scope.Search.Unscoped && hasDeletedOnField {
scope.Raw(fmt.Sprintf(
"UPDATE %v SET %v=%v%v%v",
scope.QuotedTableName(),
scope.Quote(deletedOnField.DBName),
scope.AddToVars(time.Now().Unix()),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
} else {
scope.Raw(fmt.Sprintf(
"DELETE FROM %v%v%v",
scope.QuotedTableName(),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
}
}
}
// addExtraSpaceIfExist adds a separator
func addExtraSpaceIfExist(str string) string {
if str != "" {
return " " + str
}
return ""
}
+55
View File
@@ -0,0 +1,55 @@
package models
type SendTasksIns struct {
UUIDModel
TaskID string `json:"task_id"`
WayID string `json:"way_id"`
WayType string `json:"way_type"`
ContentType string `json:"content_type"`
Config string `json:"config"`
Extra string `json:"extra"`
}
// InsEmailConfig 实例里面的邮箱config
type InsEmailConfig struct {
ToAccount string `json:"to_account" validate:"required,email" label:"收件邮箱"`
Title string `json:"title" validate:"required,max=150" label:"邮箱标题"`
}
// ManyAddTaskIns 批量添加实例
func ManyAddTaskIns(taskIns []SendTasksIns) error {
tx := db.Begin()
for _, ins := range taskIns {
// 存在就跳过这条ins记录
err := db.Where("id = ?", ins.ID).Find(&SendTasksIns{}).Error
if err == nil {
continue
}
if err := tx.Create(&ins).Error; err != nil {
tx.Rollback()
return err
}
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return err
}
return nil
}
// AddTaskInsOne 添加一条实例
func AddTaskInsOne(ins SendTasksIns) error {
if err := db.Create(&ins).Error; err != nil {
return err
}
return nil
}
// DeleteMsgTaskIns 删除一条实例
func DeleteMsgTaskIns(id string) error {
if err := db.Where("id = ?", id).Delete(&SendTasksIns{}).Error; err != nil {
return err
}
return nil
}
+164
View File
@@ -0,0 +1,164 @@
package models
import (
"errors"
"fmt"
"github.com/google/uuid"
"github.com/jinzhu/gorm"
"message-nest/pkg/table"
)
type SendTasks struct {
UUIDModel
Name string `json:"name"`
}
// AddSendTaskWithID 添加实例的时候添加任务
func AddSendTaskWithID(name string, id string, createdBy string) error {
err := db.Where("id = ?", id).Find(&SendTasks{}).Error
if err == nil {
return nil
}
uuidObj, _ := uuid.Parse(id)
task := SendTasks{
UUIDModel: UUIDModel{
ID: uuidObj,
CreatedBy: createdBy,
ModifiedBy: createdBy,
},
Name: name,
}
if err := db.Create(&task).Error; err != nil {
return err
}
return nil
}
// AddSendTask 添加任务
func AddSendTask(name string, createdBy string) error {
newUUID := uuid.New()
task := SendTasks{
UUIDModel: UUIDModel{
ID: newUUID,
CreatedBy: createdBy,
ModifiedBy: createdBy,
},
Name: name,
}
if err := db.Create(&task).Error; err != nil {
return err
}
return nil
}
// GetSendTasks 获取所有任务
func GetSendTasks(pageNum int, pageSize int, name string, maps interface{}) ([]SendTasks, error) {
var (
tasks []SendTasks
err error
)
query := db.Where(maps)
if name != "" {
query = query.Where("name like ?", fmt.Sprintf("%%%s%%", name))
}
query = query.Order("created_on DESC")
if pageSize > 0 || pageNum > 0 {
query = query.Offset(pageNum).Limit(pageSize)
}
err = query.Find(&tasks).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return tasks, nil
}
// GetSendTasksTotal 获取所有任务总数
func GetSendTasksTotal(name string, maps interface{}) (int, error) {
var (
err error
total int
)
query := db.Model(&SendTasks{}).Where(maps)
if name != "" {
query = query.Where("name like ?", fmt.Sprintf("%%%s%%", name))
}
err = query.Count(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
type SendTasksInsRes struct {
SendTasksIns
WayName string `json:"way_name"`
}
type TaskIns struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
InsData []SendTasksInsRes `json:"ins_data"`
}
// GetSendTasksTotal 获取所有任务下所有的实例
func GetTasksIns(id string) (TaskIns, error) {
insTable := table.InsTableName
waysTable := table.WayTableName
var (
task SendTasks
taskIns []SendTasksInsRes
taskResult TaskIns
)
err := db.Where("id = ?", id).First(&task).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return taskResult, err
}
db.
Table(insTable).
Select(fmt.Sprintf("%s.*, %s.name as way_name", insTable, waysTable)).
Joins(fmt.Sprintf("JOIN %s ON %s.way_id = %s.id", waysTable, insTable, waysTable)).
Where(fmt.Sprintf("%s.task_id = ?", insTable), id).
Scan(&taskIns)
taskResult.ID = task.ID
taskResult.Name = task.Name
taskResult.InsData = taskIns
return taskResult, nil
}
// FindTaskByWayId 通过way_id找到关联的任务
func FindTaskByWayId(wayId string) []SendTasks {
insTable := table.InsTableName
taskTable := table.TasksTableName
var (
tasks []SendTasks
)
db.
Table(taskTable).
Select(fmt.Sprintf("%s.*", taskTable)).
Joins(fmt.Sprintf("JOIN %s ON %s.task_id = %s.id", insTable, insTable, taskTable)).
Where(fmt.Sprintf("%s.way_id = ?", insTable), wayId).
Scan(&tasks)
return tasks
}
// 删除任务并删除所有关联的实例
func DeleteMsgTask(id string) error {
tx := db.Begin()
if err := db.Where("id = ?", id).Delete(&SendTasks{}).Error; err != nil {
tx.Rollback()
return err
}
if err := db.Where("task_id = ?", id).Delete(&SendTasksIns{}).Error; err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}
+80
View File
@@ -0,0 +1,80 @@
package models
import (
"fmt"
"message-nest/pkg/table"
"message-nest/pkg/util"
)
type SendTasksLogs struct {
ID int `gorm:"primary_key" json:"id"`
TaskID string `json:"task_id"`
Log string `json:"log"`
Status int `json:"status"`
CreatedOn util.Time `json:"created_on"`
ModifiedOn util.Time `json:"modified_on"`
}
// Add 添加日志记录
func (log *SendTasksLogs) Add() error {
if err := db.Create(&log).Error; err != nil {
return err
}
return nil
}
// 日志列表的结果
type LogsResult struct {
ID int `json:"id"`
TaskID string `json:"task_id"`
Log string `json:"log"`
CreatedOn util.Time `json:"created_on"`
ModifiedOn util.Time `json:"modified_on"`
TaskName string `json:"task_name"`
Status int `json:"status"`
}
// GetSendLogs 获取所有日志记录
func GetSendLogs(pageNum int, pageSize int, name string, taskId string, maps interface{}) ([]LogsResult, error) {
var logs []LogsResult
logt := table.LogsTableName
taskt := table.TasksTableName
query := db.
Table(logt).
Select(fmt.Sprintf("%s.*, %s.name as task_name", logt, taskt)).
Joins(fmt.Sprintf("JOIN %s ON %s.task_id = %s.id", taskt, logt, taskt))
if name != "" {
query = query.Where(fmt.Sprintf("%s.name like ?", taskt), fmt.Sprintf("%%%s%%", name))
}
if taskId != "" {
query = query.Where(fmt.Sprintf("%s.id = ?", taskt), taskId)
}
query = query.Order("created_on DESC")
if pageSize > 0 || pageNum > 0 {
query = query.Offset(pageNum).Limit(pageSize)
}
query.Scan(&logs)
return logs, nil
}
// GetSendLogsTotal 获取所有日志总数
func GetSendLogsTotal(name string, taskId string, maps interface{}) (int, error) {
var total int
logt := table.LogsTableName
taskt := table.TasksTableName
query := db.
Table(logt).
Joins(fmt.Sprintf("JOIN %s ON %s.task_id = %s.id", taskt, logt, taskt))
if name != "" {
query = query.Where(fmt.Sprintf("%s.name like ?", taskt), fmt.Sprintf("%%%s%%", name))
}
if taskId != "" {
query = query.Where(fmt.Sprintf("%s.id = ?", taskt), taskId)
}
query.Count(&total)
return total, nil
}
+104
View File
@@ -0,0 +1,104 @@
package models
import (
"errors"
"fmt"
"github.com/google/uuid"
"github.com/jinzhu/gorm"
)
type SendWays struct {
UUIDModel
Name string `json:"name"`
Type string `json:"type"`
Auth string `gorm:"not null" json:"auth"`
}
func AddSendWay(name string, auth string, wayType string, createdBy string, modifiedBy string) error {
newUUID := uuid.New()
way := SendWays{
UUIDModel: UUIDModel{
ID: newUUID,
CreatedBy: createdBy,
ModifiedBy: modifiedBy,
},
Name: name,
Type: wayType,
Auth: auth,
}
if err := db.Create(&way).Error; err != nil {
return err
}
return nil
}
func GetSendWays(pageNum int, pageSize int, name string, type_ string, maps interface{}) ([]SendWays, error) {
var (
ways []SendWays
err error
)
query := db.Where(maps)
if name != "" {
query = query.Where("name like ?", fmt.Sprintf("%%%s%%", name))
}
if type_ != "" {
query = query.Where("type = ?", type_)
}
query = query.Order("created_on DESC")
if pageSize > 0 || pageNum > 0 {
query = query.Offset(pageNum).Limit(pageSize)
}
err = query.Find(&ways).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return ways, nil
}
func GetSendWaysTotal(name string, type_ string, maps interface{}) (int, error) {
var (
err error
total int
)
query := db.Model(&SendWays{}).Where(maps)
if name != "" {
query = query.Where("name like ?", fmt.Sprintf("%%%s%%", name))
}
if type_ != "" {
query = query.Where("type = ?", type_)
}
err = query.Count(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func GetWayByID(id string) (SendWays, error) {
var way SendWays
err := db.Where("id = ? ", id).Find(&way).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return way, err
}
return way, nil
}
func DeleteMsgWay(id string) error {
if err := db.Where("id = ?", id).Delete(&SendWays{}).Error; err != nil {
return err
}
return nil
}
func EditSendWay(id string, data interface{}) error {
if err := db.Model(&SendWays{}).Where("id = ? ", id).Updates(data).Error; err != nil {
return err
}
return nil
}
+62
View File
@@ -0,0 +1,62 @@
package app
import (
"fmt"
"github.com/astaxie/beego/validation"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"message-nest/pkg/e"
"message-nest/pkg/util"
"net/http"
"strings"
)
// BindAndValid binds and validates data
func BindAndValid(c *gin.Context, form interface{}) (int, int) {
err := c.Bind(form)
if err != nil {
return http.StatusBadRequest, e.INVALID_PARAMS
}
valid := validation.Validation{}
check, err := valid.Valid(form)
if err != nil {
return http.StatusInternalServerError, e.ERROR
}
if !check {
MarkErrors(valid.Errors)
return http.StatusBadRequest, e.INVALID_PARAMS
}
return http.StatusOK, e.SUCCESS
}
func CommonPlaygroundValid(obj interface{}) (int, string) {
if err := util.CustomerValidate.Struct(obj); err != nil {
errs := err.(validator.ValidationErrors)
errMsg := BuildValidationErrors(errs)
return http.StatusBadRequest, errMsg
}
return http.StatusOK, ""
}
func BindJsonAndPlayValid(c *gin.Context, req interface{}) (int, string) {
err := c.ShouldBindJSON(req)
if err != nil {
return http.StatusBadRequest, err.Error()
} else {
return CommonPlaygroundValid(req)
}
}
func BuildValidationErrors(errors []validator.FieldError) string {
var errorMsgBuilder strings.Builder
for i, err := range errors {
if i > 0 {
errorMsgBuilder.WriteString("; ")
}
message := err.Translate(util.Trans)
errorMsgBuilder.WriteString(fmt.Sprintf("%s", message))
}
return errorMsgBuilder.String()
}
+27
View File
@@ -0,0 +1,27 @@
package app
import (
"fmt"
"github.com/astaxie/beego/validation"
"github.com/gin-gonic/gin"
"message-nest/pkg/logging"
)
// MarkErrors logs error logs
func MarkErrors(errors []*validation.Error) {
for _, err := range errors {
logging.Logger.Error(err.Key, err.Message)
}
return
}
// 从请求中获取当前用户
func GetCurrentUserName(c *gin.Context) string {
userName, ok := c.Get("currentUserName")
if !ok {
return ""
} else {
return fmt.Sprintf("%s", userName)
}
}
+37
View File
@@ -0,0 +1,37 @@
package app
import (
"github.com/gin-gonic/gin"
"net/http"
"message-nest/pkg/e"
)
type Gin struct {
C *gin.Context
}
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// Response setting gin.JSON
func (g *Gin) Response(httpCode, errCode int, data interface{}) {
g.C.JSON(httpCode, Response{
Code: errCode,
Msg: e.GetMsg(errCode),
Data: data,
})
return
}
func (g *Gin) CResponse(errCode int, Msg string, data interface{}) {
g.C.JSON(http.StatusOK, Response{
Code: errCode,
Msg: Msg,
Data: data,
})
return
}
+38
View File
@@ -0,0 +1,38 @@
package e
const (
SUCCESS = 200
ERROR = 500
INVALID_PARAMS = 400
ERROR_EXIST_TAG = 10001
ERROR_EXIST_TAG_FAIL = 10002
ERROR_NOT_EXIST_TAG = 10003
ERROR_GET_TAGS_FAIL = 10004
ERROR_COUNT_TAG_FAIL = 10005
ERROR_ADD_TAG_FAIL = 10006
ERROR_EDIT_TAG_FAIL = 10007
ERROR_DELETE_TAG_FAIL = 10008
ERROR_EXPORT_TAG_FAIL = 10009
ERROR_IMPORT_TAG_FAIL = 10010
ERROR_NOT_EXIST_ARTICLE = 10011
ERROR_CHECK_EXIST_ARTICLE_FAIL = 10012
ERROR_ADD_ARTICLE_FAIL = 10013
ERROR_DELETE_ARTICLE_FAIL = 10014
ERROR_EDIT_ARTICLE_FAIL = 10015
ERROR_COUNT_ARTICLE_FAIL = 10016
ERROR_GET_ARTICLES_FAIL = 10017
ERROR_GET_ARTICLE_FAIL = 10018
ERROR_GEN_ARTICLE_POSTER_FAIL = 10019
ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
ERROR_AUTH_TOKEN = 20003
ERROR_AUTH = 20004
ERROR_AUTH_NO_TOKEN = 20005
ERROR_UPLOAD_SAVE_IMAGE_FAIL = 30001
ERROR_UPLOAD_CHECK_IMAGE_FAIL = 30002
ERROR_UPLOAD_CHECK_IMAGE_FORMAT = 30003
)
+44
View File
@@ -0,0 +1,44 @@
package e
var MsgFlags = map[int]string{
SUCCESS: "ok",
ERROR: "fail",
INVALID_PARAMS: "请求参数错误",
ERROR_EXIST_TAG: "已存在该标签名称",
ERROR_EXIST_TAG_FAIL: "获取已存在标签失败",
ERROR_NOT_EXIST_TAG: "该标签不存在",
ERROR_GET_TAGS_FAIL: "获取所有标签失败",
ERROR_COUNT_TAG_FAIL: "统计标签失败",
ERROR_ADD_TAG_FAIL: "新增标签失败",
ERROR_EDIT_TAG_FAIL: "修改标签失败",
ERROR_DELETE_TAG_FAIL: "删除标签失败",
ERROR_EXPORT_TAG_FAIL: "导出标签失败",
ERROR_IMPORT_TAG_FAIL: "导入标签失败",
ERROR_NOT_EXIST_ARTICLE: "该文章不存在",
ERROR_ADD_ARTICLE_FAIL: "新增文章失败",
ERROR_DELETE_ARTICLE_FAIL: "删除文章失败",
ERROR_CHECK_EXIST_ARTICLE_FAIL: "检查文章是否存在失败",
ERROR_EDIT_ARTICLE_FAIL: "修改文章失败",
ERROR_COUNT_ARTICLE_FAIL: "统计文章失败",
ERROR_GET_ARTICLES_FAIL: "获取多个文章失败",
ERROR_GET_ARTICLE_FAIL: "获取单个文章失败",
ERROR_GEN_ARTICLE_POSTER_FAIL: "生成文章海报失败",
ERROR_AUTH_CHECK_TOKEN_FAIL: "Token鉴权失败",
ERROR_AUTH_CHECK_TOKEN_TIMEOUT: "Token已超时",
ERROR_AUTH_TOKEN: "Token生成失败",
ERROR_AUTH_NO_TOKEN: "Token缺失",
ERROR_AUTH: "Token错误",
ERROR_UPLOAD_SAVE_IMAGE_FAIL: "保存图片失败",
ERROR_UPLOAD_CHECK_IMAGE_FAIL: "检查图片失败",
ERROR_UPLOAD_CHECK_IMAGE_FORMAT: "校验图片错误,图片格式或大小有问题",
}
// GetMsg get error information based on Code
func GetMsg(code int) string {
msg, ok := MsgFlags[code]
if ok {
return msg
}
return MsgFlags[ERROR]
}
+15
View File
@@ -0,0 +1,15 @@
package logging
import (
"github.com/sirupsen/logrus"
"os"
)
var Logger = logrus.New()
func Setup() {
Logger := logrus.New()
Logger.SetFormatter(&logrus.JSONFormatter{})
Logger.SetOutput(os.Stdout)
Logger.SetLevel(logrus.InfoLevel)
}
+9
View File
@@ -0,0 +1,9 @@
package message
type DTalkMessage struct {
WebhookUrl string
}
func (d *DTalkMessage) send(content string) {
}
+39
View File
@@ -0,0 +1,39 @@
package message
import (
"fmt"
"gopkg.in/gomail.v2"
)
type EmailMessage struct {
Server string
Port int
Account string
Passwd string
GM *gomail.Dialer
}
func (e *EmailMessage) Init(host string, port int, account string, passwd string) {
e.Server = host
e.Port = port
e.Account = account
e.Passwd = passwd
e.GM = gomail.NewDialer(host, port, account, passwd)
}
func (e *EmailMessage) SendTextMessage(toEmail string, title string, content string) string {
m := gomail.NewMessage()
m.SetHeader("From", e.Account)
m.SetHeader("To", toEmail)
m.SetHeader("Subject", title)
m.SetBody("text/html", content)
if err := e.GM.DialAndSend(m); err != nil {
return fmt.Sprintf("邮件发送失败: %s", err)
}
return ""
}
func (e *EmailMessage) SendHtmlMessage(toEmail string, title string, content string) string {
return e.SendTextMessage(toEmail, title, content)
}
+97
View File
@@ -0,0 +1,97 @@
package setting
import (
"log"
"time"
"github.com/go-ini/ini"
)
type App struct {
JwtSecret string
PageSize int
PrefixUrl string
RuntimeRootPath string
ImageSavePath string
ImageMaxSize int
ImageAllowExts []string
ExportSavePath string
QrCodeSavePath string
FontSavePath string
LogSavePath string
LogSaveName string
LogFileExt string
TimeFormat string
LogKeepNum int
CleanTaskLogID string
}
var AppSetting = &App{}
type Server struct {
RunMode string
HttpPort int
ReadTimeout time.Duration
WriteTimeout time.Duration
}
var ServerSetting = &Server{}
type Database struct {
Type string
User string
Password string
Host string
Name string
TablePrefix string
}
var DatabaseSetting = &Database{}
type Redis struct {
Host string
Password string
MaxIdle int
MaxActive int
IdleTimeout time.Duration
}
var RedisSetting = &Redis{}
var cfg *ini.File
// Setup initialize the configuration instance
func Setup() {
var err error
cfg, err = ini.Load("conf/app.ini")
if err != nil {
log.Fatalf("setting.Setup, fail to parse 'conf/app.ini': %v", err)
}
mapTo("app", AppSetting)
mapTo("server", ServerSetting)
mapTo("database", DatabaseSetting)
mapTo("redis", RedisSetting)
AppSetting.ImageMaxSize = AppSetting.ImageMaxSize * 1024 * 1024
ServerSetting.ReadTimeout = ServerSetting.ReadTimeout * time.Second
ServerSetting.WriteTimeout = ServerSetting.WriteTimeout * time.Second
RedisSetting.IdleTimeout = RedisSetting.IdleTimeout * time.Second
// 默认值
AppSetting.LogKeepNum = 1000
AppSetting.CleanTaskLogID = "00000000-0000-0000-0000-000000000001"
}
// mapTo map section
func mapTo(section string, v interface{}) {
err := cfg.Section(section).MapTo(v)
if err != nil {
log.Fatalf("Cfg.MapTo %s err: %v", section, err)
}
}
+18
View File
@@ -0,0 +1,18 @@
package table
import (
"fmt"
"message-nest/pkg/setting"
)
var InsTableName string
var WayTableName string
var LogsTableName string
var TasksTableName string
func Setup() {
InsTableName = fmt.Sprintf("%ssend_tasks_ins", setting.DatabaseSetting.TablePrefix)
WayTableName = fmt.Sprintf("%ssend_ways", setting.DatabaseSetting.TablePrefix)
LogsTableName = fmt.Sprintf("%ssend_tasks_logs", setting.DatabaseSetting.TablePrefix)
TasksTableName = fmt.Sprintf("%ssend_tasks", setting.DatabaseSetting.TablePrefix)
}
+19
View File
@@ -0,0 +1,19 @@
package util
import (
"time"
)
func TimeStampToYmdMHD(stamp int) string {
timestamp := int64(stamp)
timeObj := time.Unix(timestamp, 0)
formattedTime := timeObj.Format("2006-01-02 15:04:05")
return formattedTime
}
func GetNowTimeStampToYmdMHD() string {
now := time.Now().Unix()
timeObj := time.Unix(now, 0)
formattedTime := timeObj.Format("2006-01-02 15:04:05")
return formattedTime
}
+50
View File
@@ -0,0 +1,50 @@
package util
import (
"time"
"github.com/dgrijalva/jwt-go"
)
var jwtSecret []byte
type Claims struct {
Username string `json:"username"`
Password string `json:"password"`
jwt.StandardClaims
}
// GenerateToken generate tokens used for auth
func GenerateToken(username, password string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(7 * 24 * time.Hour)
claims := Claims{
username,
EncodeMD5(password),
jwt.StandardClaims{
ExpiresAt: expireTime.Unix(),
Issuer: "message-nest",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(jwtSecret)
return token, err
}
// ParseToken parsing token
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
+14
View File
@@ -0,0 +1,14 @@
package util
import (
"crypto/md5"
"encoding/hex"
)
// EncodeMD5 md5 encryption
func EncodeMD5(value string) string {
m := md5.New()
m.Write([]byte(value))
return hex.EncodeToString(m.Sum(nil))
}
+29
View File
@@ -0,0 +1,29 @@
package util
import (
"github.com/gin-gonic/gin"
"github.com/unknwon/com"
"message-nest/pkg/setting"
)
// GetPage get page parameters
func GetPage(c *gin.Context) int {
result := 0
page := com.StrTo(c.Query("page")).MustInt()
if page > 0 {
result = (page - 1) * setting.AppSetting.PageSize
}
return result
}
func GetPageSize(c *gin.Context) (int, int) {
result := 0
page := com.StrTo(c.Query("page")).MustInt()
size := com.StrTo(c.Query("size")).MustInt()
if page > 0 {
result = (page - 1) * size
}
return result, size
}
+55
View File
@@ -0,0 +1,55 @@
package util
import (
"database/sql/driver"
"fmt"
"time"
)
const timeFormat = "2006-01-02 15:04:05"
const timezone = "Asia/Shanghai"
// 全局定义
type Time time.Time
func (t Time) MarshalJSON() ([]byte, error) {
b := make([]byte, 0, len(timeFormat)+2)
b = append(b, '"')
b = time.Time(t).AppendFormat(b, timeFormat)
b = append(b, '"')
return b, nil
}
func (t *Time) UnmarshalJSON(data []byte) (err error) {
now, err := time.ParseInLocation(`"`+timeFormat+`"`, string(data), time.Local)
*t = Time(now)
return
}
func (t Time) String() string {
return time.Time(t).Format(timeFormat)
}
func (t Time) local() time.Time {
loc, _ := time.LoadLocation(timezone)
return time.Time(t).In(loc)
}
func (t Time) Value() (driver.Value, error) {
var zeroTime time.Time
var ti = time.Time(t)
if ti.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return ti, nil
}
func (t *Time) Scan(v interface{}) error {
value, ok := v.(time.Time)
if ok {
*t = Time(value)
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}
+49
View File
@@ -0,0 +1,49 @@
package util
import (
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
"github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_trans "github.com/go-playground/validator/v10/translations/zh"
"reflect"
"strings"
)
//var trans ut.Translator
func TransInit() (trans ut.Translator, validate *validator.Validate) {
// 创建翻译器
zhTrans := zh.New() // 中文转换器
enTrans := en.New() // 因为转换器
uni := ut.New(zhTrans, zhTrans, enTrans) // 创建一个通用转换器
curLocales := "zh" // 设置当前语言类型
trans, _ = uni.GetTranslator(curLocales) // 获取对应语言的转换器
validate = validator.New() // 创建验证器
_ = zh_trans.RegisterDefaultTranslations(validate, trans)
//switch curLocales {
//case "zh":
// // 内置tag注册 中文翻译器
// _ = zh_trans.RegisterDefaultTranslations(validate, trans)
//case "en":
// // 内置tag注册 英文翻译器
// _ = en_trans.RegisterDefaultTranslations(validate, trans)
//}
// 注册 RegisterTagNameFunc
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
name := strings.SplitN(field.Tag.Get("label"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
return trans, validate
}
var Trans, CustomerValidate = TransInit()
+10
View File
@@ -0,0 +1,10 @@
package util
import (
"message-nest/pkg/setting"
)
// Setup Initialize the util
func Setup() {
jwtSecret = []byte(setting.AppSetting.JwtSecret)
}
+69
View File
@@ -0,0 +1,69 @@
package api
import (
"net/http"
"github.com/astaxie/beego/validation"
"github.com/gin-gonic/gin"
"message-nest/pkg/app"
"message-nest/pkg/e"
"message-nest/pkg/util"
"message-nest/service/auth_service"
)
type auth struct {
Username string `valid:"Required; MaxSize(50)"`
Password string `valid:"Required; MaxSize(50)"`
}
type ReqAuth struct {
Username string `json:"username"`
Password string `json:"passwd"`
}
func GetAuth(c *gin.Context) {
appG := app.Gin{C: c}
valid := validation.Validation{}
var user ReqAuth
err := c.ShouldBindJSON(&user)
if err != nil {
app.MarkErrors(valid.Errors)
appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
return
}
username := user.Username
password := user.Password
a := auth{Username: username, Password: password}
ok, _ := valid.Valid(&a)
if !ok {
app.MarkErrors(valid.Errors)
appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
return
}
authService := auth_service.Auth{Username: username, Password: password}
isExist, err := authService.Check()
if err != nil {
appG.Response(http.StatusInternalServerError, e.ERROR_AUTH_CHECK_TOKEN_FAIL, nil)
return
}
if !isExist {
appG.Response(http.StatusUnauthorized, e.ERROR_AUTH, nil)
return
}
token, err := util.GenerateToken(username, password)
if err != nil {
appG.Response(http.StatusInternalServerError, e.ERROR_AUTH_TOKEN, nil)
return
}
appG.Response(http.StatusOK, e.SUCCESS, map[string]string{
"token": token,
})
}
+179
View File
@@ -0,0 +1,179 @@
package v1
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"message-nest/models"
"message-nest/pkg/app"
"message-nest/pkg/e"
"message-nest/service/send_ins_service"
"message-nest/service/send_task_service"
"net/http"
)
type DeleteMsgTaskInsReq struct {
ID string `json:"id" validate:"required,len=36" label:"实例id"`
}
// DeleteMsgSendWay 删除消息渠道
func DeleteMsgTaskIns(c *gin.Context) {
var (
appG = app.Gin{C: c}
req DeleteMsgTaskInsReq
)
errCode, errMsg := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errMsg, nil)
return
}
InsService := send_ins_service.SendTaskInsService{
ID: req.ID,
}
err := InsService.Delete()
if err != nil {
appG.CResponse(http.StatusBadRequest, "删除实例失败!", nil)
return
}
appG.CResponse(http.StatusOK, "删除实例成功!", nil)
}
// GetMsgSendWayIns 获取消息任务实例
func GetMsgSendWayIns(c *gin.Context) {
appG := app.Gin{C: c}
id := c.Query("id")
if id == "" {
appG.CResponse(http.StatusBadRequest, "任务id为空!", nil)
return
}
sendTaskService := send_task_service.SendTaskService{
ID: id,
}
task, err := sendTaskService.GetTaskWithIns()
if err != nil {
appG.CResponse(http.StatusBadRequest, "获取到的任务信息为空!", nil)
return
}
appG.CResponse(http.StatusOK, "获取任务信息成功", task)
}
type SendTasksInsReq struct {
ID string `json:"id" validate:"required,len=36" label:"实例id"`
TaskId string `json:"task_id" validate:"required,len=36" label:"ins任务id"`
WayID string `json:"way_id" validate:"required,len=36" label:"渠道id"`
ContentType string `json:"content_type" validate:"required,max=100" label:"实例内容类型"`
Config string `json:"config" validate:"" label:"任务配置"`
Extra string `json:"extra" validate:"" label:"任务额外信息"`
//WayName string `json:"way_name" validate:"required,max=100" label:"渠道名"`
WayType string `json:"way_type" validate:"required,max=100" label:"渠道类型"`
}
type AddManyTasksInsReq struct {
TaskId string `json:"id" validate:"required,len=36" label:"任务id"`
TaskName string `json:"name" validate:"required,max=100" label:"任务名"`
InsData []SendTasksInsReq `json:"ins_data"`
}
// AddManyTasksIns 添加发送任务关联的实例id
func AddManyTasksIns(c *gin.Context) {
var (
appG = app.Gin{C: c}
req AddManyTasksInsReq
)
currentUser := app.GetCurrentUserName(c)
errCode, errStr := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errStr, nil)
return
}
// 校验提交的各个实例信息
var taskIns []models.SendTasksIns
if len(req.InsData) > 0 {
for _, data := range req.InsData {
code, dataErrStr := app.CommonPlaygroundValid(data)
if code != http.StatusOK {
appG.CResponse(code, dataErrStr, nil)
return
}
uuidObj, _ := uuid.Parse(data.ID)
taskIns = append(taskIns, models.SendTasksIns{
UUIDModel: models.UUIDModel{
ID: uuidObj,
CreatedBy: currentUser,
ModifiedBy: currentUser,
},
TaskID: data.TaskId,
WayID: data.WayID,
WayType: data.WayType,
ContentType: data.ContentType,
Config: data.Config,
Extra: data.Extra,
})
}
}
sendTaskInsService := send_ins_service.SendTaskInsService{}
err := sendTaskInsService.ManyAdd(taskIns)
if err != "" {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("添加实例失败!错误原因:%s", err), nil)
return
}
sendTaskService := send_task_service.SendTaskService{
ID: req.TaskId,
Name: req.TaskName,
CreatedBy: currentUser,
}
errAdd := sendTaskService.AddWithID()
if errAdd != nil {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("添加任务失败!错误原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "添加实例成功!", nil)
}
// AddTasksIns 添加发送任务关联的实例id
func AddTasksIns(c *gin.Context) {
var (
appG = app.Gin{C: c}
req SendTasksInsReq
)
//currentUser := app.GetCurrentUserName(c)
errCode, errStr := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errStr, nil)
return
}
sendTaskInsService := send_ins_service.SendTaskInsService{}
uuidObj, _ := uuid.Parse(req.ID)
err := sendTaskInsService.AddOne(models.SendTasksIns{
UUIDModel: models.UUIDModel{ID: uuidObj},
TaskID: req.TaskId,
WayID: req.WayID,
WayType: req.WayType,
ContentType: req.ContentType,
Config: req.Config,
Extra: req.Extra,
//Cre: req.Extra,
})
if err != "" {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("添加实例失败!错误原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "添加实例成功!", nil)
}
+44
View File
@@ -0,0 +1,44 @@
package v1
import (
"github.com/gin-gonic/gin"
"message-nest/pkg/app"
"message-nest/pkg/e"
"message-nest/service/send_message_service"
"net/http"
)
type SendMessageReq struct {
TaskID string `json:"task_id" validate:"required,len=36" label:"任务id"`
Text string `json:"text" validate:"required" label:"文本内容"`
HTML string `json:"html" label:"html内容"`
MarkDown string `json:"markdown" label:"markdown内容"`
}
// DoSendMassage 外部调用发信接口
func DoSendMassage(c *gin.Context) {
var (
appG = app.Gin{C: c}
req SendMessageReq
)
errCode, errMsg := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errMsg, nil)
return
}
msgService := send_message_service.SendMessageService{
TaskID: req.TaskID,
Text: req.Text,
HTML: req.HTML,
MarkDown: req.MarkDown,
}
err := msgService.Send()
if err != "" {
appG.CResponse(http.StatusBadRequest, "发送失败!", nil)
return
}
appG.CResponse(http.StatusOK, "发送成功!", nil)
}
+103
View File
@@ -0,0 +1,103 @@
package v1
import (
"message-nest/pkg/e"
"net/http"
"github.com/gin-gonic/gin"
"message-nest/pkg/app"
"message-nest/pkg/util"
"message-nest/service/send_task_service"
)
type DeleteMsgSendTaskReq struct {
ID string `json:"id" validate:"required,len=36" label:"任务id"`
}
// DeleteMsgSendTask 删除消息任务
func DeleteMsgSendTask(c *gin.Context) {
var (
appG = app.Gin{C: c}
req DeleteMsgSendTaskReq
)
errCode, errMsg := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errMsg, nil)
return
}
MsgSendTaskService := send_task_service.SendTaskService{
ID: req.ID,
}
err := MsgSendTaskService.Delete()
if err != nil {
appG.CResponse(http.StatusBadRequest, "删除发信任务失败!", nil)
return
}
appG.CResponse(http.StatusOK, "删除发信任务成功!", nil)
}
// GetMsgSendTaskList 获取消息任务列表
func GetMsgSendTaskList(c *gin.Context) {
appG := app.Gin{C: c}
name := c.Query("name")
offset, limit := util.GetPageSize(c)
MsgSendTaskService := send_task_service.SendTaskService{
Name: name,
PageNum: offset,
PageSize: limit,
}
tasks, err := MsgSendTaskService.GetAll()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取任务任务失败!", nil)
return
}
count, err := MsgSendTaskService.Count()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取任务任务总数失败!", nil)
return
}
appG.CResponse(http.StatusOK, "获取任务任务成功", map[string]interface{}{
"lists": tasks,
"total": count,
})
}
type AddMsgSendTaskReq struct {
Name string `json:"name" validate:"required,max=100,min=1" label:"任务任务名"`
}
// AddMsgSendTask 添加发送任务
func AddMsgSendTask(c *gin.Context) {
var (
appG = app.Gin{C: c}
req AddMsgSendTaskReq
)
currentUser := app.GetCurrentUserName(c)
errCode, errStr := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errStr, nil)
return
}
MsgSendTaskService := send_task_service.SendTaskService{
Name: req.Name,
CreatedBy: currentUser,
ModifiedBy: currentUser,
}
err := MsgSendTaskService.Add()
if err != nil {
appG.CResponse(http.StatusBadRequest, "添加任务任务失败!", nil)
return
}
appG.CResponse(http.StatusOK, "添加任务任务成功!", nil)
}
+40
View File
@@ -0,0 +1,40 @@
package v1
import (
"github.com/gin-gonic/gin"
"message-nest/pkg/app"
"message-nest/pkg/util"
"message-nest/service/send_logs_service"
"net/http"
)
// GetMsgSendWayList 获取消息渠道列表
func GetTaskSendLogsList(c *gin.Context) {
appG := app.Gin{C: c}
name := c.Query("name")
taskId := c.Query("taskid")
offset, limit := util.GetPageSize(c)
logsService := send_logs_service.SendTaskLogsService{
TaskId: taskId,
Name: name,
PageNum: offset,
PageSize: limit,
}
ways, err := logsService.GetAll()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取日志失败!", nil)
return
}
count, err := logsService.Count()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取日志总数失败!", nil)
return
}
appG.CResponse(http.StatusOK, "获取日志成功", map[string]interface{}{
"lists": ways,
"total": count,
})
}
+222
View File
@@ -0,0 +1,222 @@
package v1
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"message-nest/pkg/app"
"message-nest/pkg/e"
"message-nest/pkg/util"
"message-nest/service/send_way_service"
)
type DeleteMsgSendWayReq struct {
ID string `json:"id" validate:"required,len=36" label:"渠道id"`
}
// DeleteMsgSendWay 删除消息渠道
func DeleteMsgSendWay(c *gin.Context) {
var (
appG = app.Gin{C: c}
req DeleteMsgSendWayReq
)
errCode, errMsg := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errMsg, nil)
return
}
sendWayService := send_way_service.SendWay{
ID: req.ID,
}
err := sendWayService.Delete()
if err != nil {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("删除发信渠道失败!%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "删除发信渠道成功!", nil)
}
// GetMsgSendWay 获取消息渠道
func GetMsgSendWay(c *gin.Context) {
appG := app.Gin{C: c}
id := c.Query("id")
if id == "" {
appG.CResponse(http.StatusBadRequest, "渠道id为空!", nil)
return
}
sendWayService := send_way_service.SendWay{
ID: id,
}
way, err := sendWayService.GetByID()
if err != nil {
appG.CResponse(http.StatusBadRequest, "获取到的渠道信息为空!", nil)
return
}
appG.CResponse(http.StatusOK, "获取渠道信息成功", way)
}
// GetMsgSendWayList 获取消息渠道列表
func GetMsgSendWayList(c *gin.Context) {
appG := app.Gin{C: c}
name := c.Query("name")
type_ := c.Query("type")
offset, limit := util.GetPageSize(c)
sendWayService := send_way_service.SendWay{
Name: name,
Type: type_,
PageNum: offset,
PageSize: limit,
}
ways, err := sendWayService.GetAll()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取渠道信息失败!", nil)
return
}
count, err := sendWayService.Count()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "获取渠道信息总数失败!", nil)
return
}
appG.CResponse(http.StatusOK, "获取渠道信息成功", map[string]interface{}{
"lists": ways,
"total": count,
})
}
type AddMsgSendWayReq struct {
Name string `json:"name" validate:"required,max=100,min=1" label:"渠道名"`
Type string `json:"type" validate:"required,max=100,min=1" label:"渠道类型"`
Auth string `json:"auth" validate:"required,max=2048,min=6" label:"渠道认证方式"`
}
// AddMsgSendWay 添加发送渠道
func AddMsgSendWay(c *gin.Context) {
var (
appG = app.Gin{C: c}
req AddMsgSendWayReq
)
currentUser := app.GetCurrentUserName(c)
errCode, errStr := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errStr, nil)
return
}
sendWayService := send_way_service.SendWay{
Name: req.Name,
Type: req.Type,
Auth: req.Auth,
CreatedBy: currentUser,
ModifiedBy: currentUser,
}
diffMsg, _ := sendWayService.ValidateDiffWay()
if diffMsg != "" {
appG.CResponse(http.StatusBadRequest, diffMsg, nil)
return
}
err := sendWayService.Add()
if err != nil {
appG.CResponse(http.StatusBadRequest, "添加渠道失败!", nil)
return
}
appG.CResponse(http.StatusOK, "添加渠道成功!", nil)
}
type EditSendWayReq struct {
ID string `json:"id" validate:"required,len=36" label:"渠道id"`
Name string `json:"name" validate:"required,max=100,min=1" label:"渠道名"`
Type string `json:"type" validate:"required,max=100,min=1" label:"渠道类型"`
Auth string `json:"auth" validate:"required" label:"渠道认证信息"`
}
// EditSendWay 编辑发送渠道
func EditSendWay(c *gin.Context) {
var (
appG = app.Gin{C: c}
req = EditSendWayReq{}
)
currentUser := app.GetCurrentUserName(c)
errCode, errMsg := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errMsg, nil)
return
}
sendWayService := send_way_service.SendWay{
ID: req.ID,
Name: req.Name,
Type: req.Type,
Auth: req.Auth,
ModifiedBy: currentUser,
}
diffMsg, _ := sendWayService.ValidateDiffWay()
if diffMsg != "" {
appG.CResponse(http.StatusBadRequest, diffMsg, nil)
return
}
err := sendWayService.Edit()
if err != nil {
appG.CResponse(http.StatusInternalServerError, "编辑渠道信息失败", nil)
return
}
appG.CResponse(http.StatusOK, "编辑渠道信息成功", nil)
}
type TestSendWayReq struct {
Name string `json:"name" validate:"required,max=100,min=1" label:"渠道名"`
Type string `json:"type" validate:"required,max=100,min=1" label:"渠道类型"`
Auth string `json:"auth" validate:"required" label:"渠道认证信息"`
}
// TestSendWay 测试发送渠道
func TestSendWay(c *gin.Context) {
var (
appG = app.Gin{C: c}
req = TestSendWayReq{}
)
errCode, errMsg := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errMsg, nil)
return
}
sendWayService := send_way_service.SendWay{
Name: req.Name,
Type: req.Type,
Auth: req.Auth,
}
diffMsg, msgObj := sendWayService.ValidateDiffWay()
if diffMsg != "" {
appG.CResponse(http.StatusBadRequest, diffMsg, nil)
return
}
errTestMsg := sendWayService.TestSendWay(msgObj)
if errTestMsg != "" {
appG.CResponse(http.StatusInternalServerError, errTestMsg, nil)
return
}
appG.CResponse(http.StatusOK, "测试渠道信息成功", nil)
}
+44
View File
@@ -0,0 +1,44 @@
package v1
import (
"fmt"
"github.com/gin-gonic/gin"
"message-nest/pkg/app"
"message-nest/pkg/e"
"message-nest/service/settings_service"
"net/http"
)
type EditUserPasswd struct {
OldPassword string `json:"old_passwd" validate:"required,max=50" label:"旧密码"`
NewPassword string `json:"new_passwd" validate:"required,max=50" label:"新密码"`
}
// EditPasswd 用户重设密码
func EditPasswd(c *gin.Context) {
var (
appG = app.Gin{C: c}
req EditUserPasswd
)
currentUser := app.GetCurrentUserName(c)
errCode, errStr := app.BindJsonAndPlayValid(c, &req)
if errCode != e.SUCCESS {
appG.CResponse(errCode, errStr, nil)
return
}
sendTaskService := settings_service.UserSettings{
UserName: currentUser,
OldPassword: req.OldPassword,
NewPassword: req.NewPassword,
}
err := sendTaskService.EditUserPasswd()
if err != nil {
appG.CResponse(http.StatusBadRequest, fmt.Sprintf("修改密码失败!错误原因:%s", err), nil)
return
}
appG.CResponse(http.StatusOK, "修改成功!", nil)
}
+53
View File
@@ -0,0 +1,53 @@
package routers
import (
"github.com/gin-gonic/gin"
"message-nest/middleware"
"message-nest/routers/api"
"message-nest/routers/api/v1"
)
// InitRouter initialize routing information
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.Use(middleware.Cors())
r.POST("/auth", api.GetAuth)
apiv1 := r.Group("/api/v1")
apiv1.Use(middleware.JWT())
{
// sendways
apiv1.POST("/sendways/add", v1.AddMsgSendWay)
apiv1.POST("/sendways/delete", v1.DeleteMsgSendWay)
apiv1.POST("/sendways/edit", v1.EditSendWay)
apiv1.POST("/sendways/test", v1.TestSendWay)
apiv1.GET("/sendways/list", v1.GetMsgSendWayList)
apiv1.GET("/sendways/get", v1.GetMsgSendWay)
// sendtasks
apiv1.GET("/sendtasks/list", v1.GetMsgSendTaskList)
apiv1.POST("/sendtasks/add", v1.AddMsgSendTask)
apiv1.POST("/sendtasks/delete", v1.DeleteMsgSendTask)
// sendtasks/ins
apiv1.POST("/sendtasks/ins/addmany", v1.AddManyTasksIns)
apiv1.POST("/sendtasks/ins/addone", v1.AddTasksIns)
apiv1.GET("/sendtasks/ins/gettask", v1.GetMsgSendWayIns)
apiv1.POST("/sendtasks/ins/delete", v1.DeleteMsgTaskIns)
// message/send
apiv1.POST("/message/send", v1.DoSendMassage)
apiv1.GET("/sendlogs/list", v1.GetTaskSendLogsList)
// settings
apiv1.POST("/settings/setpasswd", v1.EditPasswd)
}
return r
}
+12
View File
@@ -0,0 +1,12 @@
package auth_service
import "message-nest/models"
type Auth struct {
Username string
Password string
}
func (a *Auth) Check() (bool, error) {
return models.CheckAuth(a.Username, a.Password)
}
+82
View File
@@ -0,0 +1,82 @@
package send_ins_service
import (
"encoding/json"
"fmt"
"message-nest/models"
"message-nest/pkg/app"
)
type SendTaskInsService struct {
ID string
Name string
CreatedBy string
ModifiedBy string
CreatedOn string
PageNum int
PageSize int
}
// ValidateDiffWay 各种发信渠道具体字段校验
func (sw *SendTaskInsService) ValidateDiffIns(ins models.SendTasksIns) (string, interface{}) {
var empty interface{}
if ins.WayType == "Email" {
var emailConfig models.InsEmailConfig
err := json.Unmarshal([]byte(ins.Config), &emailConfig)
if err != nil {
return "邮箱auth反序列化失败!", empty
}
_, Msg := app.CommonPlaygroundValid(emailConfig)
return Msg, emailConfig
}
return "", empty
}
func (st *SendTaskInsService) ManyAdd(taskIns []models.SendTasksIns) string {
for _, ins := range taskIns {
errStr, _ := st.ValidateDiffIns(ins)
if errStr != "" {
return errStr
}
}
err := models.ManyAddTaskIns(taskIns)
if err != nil {
return fmt.Sprintf("%s", err)
}
return ""
}
func (st *SendTaskInsService) AddOne(ins models.SendTasksIns) string {
errStr, _ := st.ValidateDiffIns(ins)
if errStr != "" {
return errStr
}
err := models.AddTaskInsOne(ins)
if err != nil {
return fmt.Sprintf("%s", err)
}
return ""
}
func (st *SendTaskInsService) Delete() error {
return models.DeleteMsgTaskIns(st.ID)
}
func (st *SendTaskInsService) Count() (int, error) {
return models.GetSendTasksTotal(st.Name, st.getMaps())
}
func (st *SendTaskInsService) GetAll() ([]models.SendTasks, error) {
tasks, err := models.GetSendTasks(st.PageNum, st.PageSize, st.Name, st.getMaps())
if err != nil {
return nil, err
}
return tasks, nil
}
func (st *SendTaskInsService) getMaps() map[string]interface{} {
maps := make(map[string]interface{})
return maps
}
+31
View File
@@ -0,0 +1,31 @@
package send_logs_service
import (
"message-nest/models"
)
type SendTaskLogsService struct {
ID int
TaskId string
Name string
PageNum int
PageSize int
}
func (st *SendTaskLogsService) Count() (int, error) {
return models.GetSendLogsTotal(st.Name, st.TaskId, st.getMaps())
}
func (st *SendTaskLogsService) GetAll() ([]models.LogsResult, error) {
tasks, err := models.GetSendLogs(st.PageNum, st.PageSize, st.Name, st.TaskId, st.getMaps())
if err != nil {
return nil, err
}
return tasks, nil
}
func (st *SendTaskLogsService) getMaps() map[string]interface{} {
maps := make(map[string]interface{})
return maps
}
@@ -0,0 +1,129 @@
package send_message_service
import (
"fmt"
"message-nest/models"
"message-nest/pkg/logging"
"message-nest/pkg/message"
"message-nest/service/send_task_service"
"message-nest/service/send_way_service"
"strings"
)
type SendMessageService struct {
TaskID string
Text string
HTML string
MarkDown string
}
func (sm *SendMessageService) Send() string {
var logOutput []string
status := 1
sendTaskService := send_task_service.SendTaskService{
ID: sm.TaskID,
}
task, err := sendTaskService.GetTaskWithIns()
if err != nil {
return fmt.Sprintf("任务不存在!任务id: %s", sm.TaskID)
}
for idx, ins := range task.InsData {
way, err := models.GetWayByID(ins.WayID)
if err != nil {
logOutput = append(logOutput, fmt.Sprintf("渠道信息不存在!渠道id%s", ins.WayID))
}
wayService := send_way_service.SendWay{
ID: fmt.Sprintf("%s", way.ID),
Name: way.Name,
Auth: way.Auth,
Type: way.Type,
}
logOutput = append(logOutput, fmt.Sprintf(">> 实例 %d", idx+1))
logOutput = append(logOutput, fmt.Sprintf("开始发送,实例: %s", ins.WayID))
logOutput = append(logOutput, fmt.Sprintf("实例类型: %s + %s", ins.WayType, ins.ContentType))
logOutput = append(logOutput, fmt.Sprintf("实例配置: %s", ins.Config))
errStr, msgObj := wayService.ValidateDiffWay()
if errStr != "" {
sm.MarkStatus(errStr, &status)
logOutput = append(logOutput, fmt.Sprintf("实例渠道认证校验失败: %s", errStr))
continue
}
// 邮箱类型的实例
emailAuth, ok := msgObj.(send_way_service.WayDetailEmail)
if ok {
errMsg := sm.SendTaskEmail(emailAuth)
sm.MarkStatus(errMsg, &status)
logOutput = append(logOutput, sm.TransError(errMsg))
continue
}
logOutput = append(logOutput, fmt.Sprintf("未知渠道的发信实例: %s", ins.ID))
}
logOutput = sm.FormatSendContent(logOutput)
sm.RecordSendLog(logOutput, status)
return ""
}
// FormatSendContent 格式化输出的发送内容
func (sm *SendMessageService) FormatSendContent(logOutput []string) []string {
logOutput = append(logOutput, fmt.Sprintf(">> 发送的内容:"))
if sm.Text != "" {
logOutput = append(logOutput, fmt.Sprintf("Text: %s", sm.Text))
}
if sm.HTML != "" {
logOutput = append(logOutput, fmt.Sprintf("HTML: %s", sm.HTML))
}
if sm.MarkDown != "" {
logOutput = append(logOutput, fmt.Sprintf("MarkDown: %s", sm.MarkDown))
}
return logOutput
}
// MarkStatus 标记任务状态
func (sm *SendMessageService) MarkStatus(errStr string, status *int) {
if errStr != "" {
*status = 0
}
}
// RecordSendLog 记录发送日志
func (sm *SendMessageService) RecordSendLog(logOutput []string, status int) {
if len(logOutput) <= 0 {
return
}
log := models.SendTasksLogs{
Log: strings.Join(logOutput, "\n"),
TaskID: sm.TaskID,
Status: status,
}
err := log.Add()
if err != nil {
logging.Logger.Error(fmt.Sprintf("添加日志失败!原因是:%s", err))
}
}
// TransError 转化错误
func (sm *SendMessageService) TransError(err string) string {
if err == "" {
return "发送成功!\n"
} else {
return fmt.Sprintf("发送失败:%s", err)
}
}
// SendTaskEmail 执行发送邮件
func (sm *SendMessageService) SendTaskEmail(auth send_way_service.WayDetailEmail) string {
var emailer message.EmailMessage
emailer.Init(auth.Server, auth.Port, auth.Account, auth.Passwd)
//errMsg := emailer.SendTextMessage("sayheya@qq.com", "test", "This is a test email from message-nest.")
//return errMsg
return ""
}
+49
View File
@@ -0,0 +1,49 @@
package send_task_service
import (
"message-nest/models"
)
type SendTaskService struct {
ID string
Name string
CreatedBy string
ModifiedBy string
CreatedOn string
PageNum int
PageSize int
}
func (st *SendTaskService) Add() error {
return models.AddSendTask(st.Name, st.CreatedBy)
}
func (st *SendTaskService) AddWithID() error {
return models.AddSendTaskWithID(st.Name, st.ID, st.CreatedBy)
}
func (st *SendTaskService) Delete() error {
return models.DeleteMsgTask(st.ID)
}
func (st *SendTaskService) GetTaskWithIns() (models.TaskIns, error) {
return models.GetTasksIns(st.ID)
}
func (st *SendTaskService) Count() (int, error) {
return models.GetSendTasksTotal(st.Name, st.getMaps())
}
func (st *SendTaskService) GetAll() ([]models.SendTasks, error) {
tasks, err := models.GetSendTasks(st.PageNum, st.PageSize, st.Name, st.getMaps())
if err != nil {
return nil, err
}
return tasks, nil
}
func (st *SendTaskService) getMaps() map[string]interface{} {
maps := make(map[string]interface{})
return maps
}
+117
View File
@@ -0,0 +1,117 @@
package send_way_service
import (
"encoding/json"
"errors"
"fmt"
"message-nest/models"
"message-nest/pkg/app"
"message-nest/pkg/message"
"strings"
)
type SendWay struct {
ID string
Name string
Type string
CreatedBy string
ModifiedBy string
Auth string
CreatedOn string
PageNum int
PageSize int
}
// WayDetailEmail 邮箱渠道明细字段
type WayDetailEmail struct {
Server string `validate:"required,max=50" label:"SMTP服务地址"`
Port int `validate:"required,max=65535" label:"SMTP服务端口"`
Account string `validate:"required,email" label:"邮箱账号"`
Passwd string `validate:"required,max=50" label:"邮箱密码"`
}
// WayDetailDTalk 钉钉渠道明细字段
type WayDetailDTalk struct {
WebhookUrl string `validate:"required,url" label:"钉钉webhookUrl地址"`
}
func (sw *SendWay) GetByID() (interface{}, error) {
return models.GetWayByID(sw.ID)
}
func (sw *SendWay) Add() error {
return models.AddSendWay(sw.Name, sw.Auth, sw.Type, sw.CreatedBy, sw.ModifiedBy)
}
func (sw *SendWay) Edit() error {
data := make(map[string]interface{})
data["modified_by"] = sw.ModifiedBy
data["name"] = sw.Name
data["auth"] = sw.Auth
return models.EditSendWay(sw.ID, data)
}
func (sw *SendWay) Delete() error {
tasks := models.FindTaskByWayId(sw.ID)
if len(tasks) > 0 {
var names []string
for _, task := range tasks {
names = append(names, task.Name)
}
return errors.New(fmt.Sprintf("已经存在使用的任务,删除失败!任务名:%s", strings.Join(names, ", ")))
}
return models.DeleteMsgWay(sw.ID)
}
func (sw *SendWay) Count() (int, error) {
return models.GetSendWaysTotal(sw.Name, sw.Type, sw.getMaps())
}
func (sw *SendWay) GetAll() ([]models.SendWays, error) {
tags, err := models.GetSendWays(sw.PageNum, sw.PageSize, sw.Name, sw.Type, sw.getMaps())
if err != nil {
return nil, err
}
return tags, nil
}
func (sw *SendWay) getMaps() map[string]interface{} {
maps := make(map[string]interface{})
return maps
}
// ValidateDiffWay 各种发信渠道具体字段校验
func (sw *SendWay) ValidateDiffWay() (string, interface{}) {
var empty interface{}
if sw.Type == "Email" {
var email WayDetailEmail
err := json.Unmarshal([]byte(sw.Auth), &email)
if err != nil {
return "邮箱auth反序列化失败!", empty
}
_, Msg := app.CommonPlaygroundValid(email)
return Msg, email
} else if sw.Type == "Dtalk" {
var dtalk WayDetailDTalk
err := json.Unmarshal([]byte(sw.Auth), &dtalk)
if err != nil {
return "钉钉auth反序列化失败!", empty
}
_, Msg := app.CommonPlaygroundValid(dtalk)
return Msg, dtalk
}
return fmt.Sprintf("未知的发信渠道校验: %s", sw.Type), empty
}
// TestSendWay 尝试带发信测试连通性
func (sw *SendWay) TestSendWay(msgObj interface{}) string {
emailAuth, ok := msgObj.(WayDetailEmail)
if ok {
var emailer message.EmailMessage
emailer.Init(emailAuth.Server, 465, emailAuth.Account, emailAuth.Passwd)
errMsg := emailer.SendTextMessage(emailAuth.Account, "test", "This is a test email from message-nest.")
return errMsg
}
return fmt.Sprintf("未知的发信渠道校验: %s", sw.Type)
}
+23
View File
@@ -0,0 +1,23 @@
package settings_service
import (
"errors"
"message-nest/models"
)
type UserSettings struct {
UserName string
OldPassword string
NewPassword string
}
// EditUserPasswd 用户设置密码
func (us *UserSettings) EditUserPasswd() error {
ok, _ := models.CheckAuth(us.UserName, us.OldPassword)
if !ok {
return errors.New("旧密码校验失败!")
}
var user = make(map[string]string)
user["password"] = us.NewPassword
return models.EditUser(us.UserName, user)
}
+14
View File
@@ -0,0 +1,14 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
+30
View File
@@ -0,0 +1,30 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: build-api-front-output
on:
workflow_dispatch:
push:
branches: [ "api-front" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- run: npm install
- run: npm run build
- name: Upload files to server
uses: appleboy/scp-action@master
with:
host: 'nd2.ex.engigu.cn'
username: 'root'
password: ${{ secrets.ND2PWD }}
source: './dist'
target: '/root/workspace/ngproxy/data/pages/api-front'
+28
View File
@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
+31
View File
@@ -0,0 +1,31 @@
# message-nest
This template should help get you started developing with Vue 3 in Vite.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Message Nest</title>
</head>
<style>
body {
margin: 0;
padding: 0;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
</style>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+6266
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "message-nest",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.6.2",
"element-plus": "^2.4.2",
"fingerprintjs2": "^2.1.4",
"lodash": "^4.17.21",
"pinia": "^2.1.7",
"prismjs": "^1.29.0",
"uuid": "^9.0.1",
"vue": "^3.3.4",
"vue-clipboard3": "^2.0.0",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/eslint-config-prettier": "^8.0.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.3",
"vite": "^4.4.11",
"vite-plugin-prismjs": "^0.0.8"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg t="1702547210136" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1861" width="200" height="200"><path d="M970.091313 224.033616a14.201535 14.201535 0 0 0-14.100687-3.631838c-88.419556-13.429657-142.919111-12.953859-248.242424 23.762747-69.454869 24.217859-109.056 79.873293-135.267556 116.712728-6.692202 9.403475-18.949172 26.625293-23.77309 29.509818-5.70699-1.303273-21.929374-16.036202-31.762101-24.965172-28.626747-25.994343-64.252121-58.353778-106.767516-58.757172-0.312889-0.005172-0.627071-0.005172-0.939959-0.005171-29.62101 0-54.798222 13.434828-72.857859 38.89907-13.222788 18.651798-17.914828 37.388929-18.106182 38.176323a14.31402 14.31402 0 0 0 2.893576 12.515556 14.336 14.336 0 0 0 11.782465 5.095434l0.409858-0.024565c19.192242-1.152 59.141172-3.545212 65.807516 81.857939 4.288646 54.899071 29.742545 108.631919 69.833697 147.419798 45.499475 44.025535 106.237414 67.298263 175.650909 67.298263 98.97503 0 166.222869-24.480323 205.207272-45.015919 43.172202-22.742626 62.621737-45.918384 63.429819-46.898425a14.170505 14.170505 0 0 0 1.529535-15.874586 14.191192 14.191192 0 0 0-14.166626-7.337373c-37.924202 4.443798-92.767677 6.09099-138.666667-11.298909-7.580444-2.872889-13.878303-5.893172-18.959515-8.691071 20.273131-9.964606 51.000889-28.383677 83.621495-59.93503 34.186343-33.065374 37.722505-69.908687 40.838464-102.423273 2.297535-23.919192 4.666182-48.651636 18.500526-73.091879 28.807758-50.883232 66.645333-74.489535 74.560646-79.035475a14.201535 14.201535 0 0 0 12.135434-7.776969 14.193778 14.193778 0 0 0-2.59103-16.484849z" fill="" p-id="1862"></path><path d="M674.834101 716.481939c-33.422222 4.90796-74.989899 16.884364-117.22602-8.102787-34.843152-20.613172-53.056646-20.993293-77.783919-18.49794-0.156444 0.015515-0.312889 0.015515-0.469334 0.034909-0.274101 0.033616-0.548202 0.060768-0.822303 0.094384-4.697212 0.487434-9.65301 1.065374-14.995394 1.62004-6.702545 0.649051-13.414141 1.19596-20.133495 1.634263-75.333818 4.102465-136.860444-5.381172-163.012525-10.404202-49.488162-9.872808-86.57196-23.921778-108.946101-33.970424-59.832889-26.868364-87.080081-56.671677-87.080081-72.989738 0-1.873455 0.384-2.222545 0.787394-2.585858 2.196687-1.974303 10.616242-6.520242 41.302626-6.004364 24.793212 0.418909 56.970343 3.858101 94.308849 7.853253 40.722101 4.348121 86.873212 9.283232 136.348444 11.686788 6.913293 0.333576 13.959758 0.625778 20.93899 0.858505 9.510788 0.307717 17.545051-7.182222 17.868283-16.707233 0.319354-9.535354-7.175758-17.550222-16.705939-17.868282a1386.24 1386.24 0 0 1-20.419233-0.839112c-48.521051-2.358303-94.161455-7.236525-134.434909-11.544565-90.868364-9.712485-139.353212-13.818828-162.333737 6.833131-8.02004 7.21196-12.262141 17.004606-12.262141 28.317737 0 0.672323 0.029737 1.348525 0.065939 2.026021l0.005172 0.100848c-0.045253 0.524929-0.071111 1.039515-0.071111 1.539879 0 41.464242 9.561212 81.661414 28.414707 119.484768 17.493333 35.088808 42.251636 66.663434 73.583192 93.927434 9.169455-6.551273 20.155475-13.604202 32.524929-20.050748 49.737697-25.921939 97.838545-29.339152 139.099798-9.884444 101.590626 47.906909 142.060606 47.837091 206.198949-0.364606 5.70699-4.287354 13.814949-3.140525 18.103596 2.567758 4.289939 5.708283 3.140525 13.813657-2.567757 18.103595-38.425859 28.877576-69.968162 41.279354-105.143596 41.280647-0.484848 0-0.969697-0.002586-1.457132-0.007758-32.760242-0.316768-69.311354-11.381657-126.16404-38.190545-51.905939-24.47903-106.215434 1.008485-139.548444 23.465374 64.611556 47.922424 146.341495 74.101657 232.704 74.101656 117.55701 0 226.204444-48.934788 292.761858-131.362909l0.002586-0.002586c18.79402-14.959192 12.028121-41.362101-23.442101-36.152889z" fill="" p-id="1863"></path></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg t="1702547210136" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1861" width="200" height="200"><path d="M970.091313 224.033616a14.201535 14.201535 0 0 0-14.100687-3.631838c-88.419556-13.429657-142.919111-12.953859-248.242424 23.762747-69.454869 24.217859-109.056 79.873293-135.267556 116.712728-6.692202 9.403475-18.949172 26.625293-23.77309 29.509818-5.70699-1.303273-21.929374-16.036202-31.762101-24.965172-28.626747-25.994343-64.252121-58.353778-106.767516-58.757172-0.312889-0.005172-0.627071-0.005172-0.939959-0.005171-29.62101 0-54.798222 13.434828-72.857859 38.89907-13.222788 18.651798-17.914828 37.388929-18.106182 38.176323a14.31402 14.31402 0 0 0 2.893576 12.515556 14.336 14.336 0 0 0 11.782465 5.095434l0.409858-0.024565c19.192242-1.152 59.141172-3.545212 65.807516 81.857939 4.288646 54.899071 29.742545 108.631919 69.833697 147.419798 45.499475 44.025535 106.237414 67.298263 175.650909 67.298263 98.97503 0 166.222869-24.480323 205.207272-45.015919 43.172202-22.742626 62.621737-45.918384 63.429819-46.898425a14.170505 14.170505 0 0 0 1.529535-15.874586 14.191192 14.191192 0 0 0-14.166626-7.337373c-37.924202 4.443798-92.767677 6.09099-138.666667-11.298909-7.580444-2.872889-13.878303-5.893172-18.959515-8.691071 20.273131-9.964606 51.000889-28.383677 83.621495-59.93503 34.186343-33.065374 37.722505-69.908687 40.838464-102.423273 2.297535-23.919192 4.666182-48.651636 18.500526-73.091879 28.807758-50.883232 66.645333-74.489535 74.560646-79.035475a14.201535 14.201535 0 0 0 12.135434-7.776969 14.193778 14.193778 0 0 0-2.59103-16.484849z" fill="" p-id="1862"></path><path d="M674.834101 716.481939c-33.422222 4.90796-74.989899 16.884364-117.22602-8.102787-34.843152-20.613172-53.056646-20.993293-77.783919-18.49794-0.156444 0.015515-0.312889 0.015515-0.469334 0.034909-0.274101 0.033616-0.548202 0.060768-0.822303 0.094384-4.697212 0.487434-9.65301 1.065374-14.995394 1.62004-6.702545 0.649051-13.414141 1.19596-20.133495 1.634263-75.333818 4.102465-136.860444-5.381172-163.012525-10.404202-49.488162-9.872808-86.57196-23.921778-108.946101-33.970424-59.832889-26.868364-87.080081-56.671677-87.080081-72.989738 0-1.873455 0.384-2.222545 0.787394-2.585858 2.196687-1.974303 10.616242-6.520242 41.302626-6.004364 24.793212 0.418909 56.970343 3.858101 94.308849 7.853253 40.722101 4.348121 86.873212 9.283232 136.348444 11.686788 6.913293 0.333576 13.959758 0.625778 20.93899 0.858505 9.510788 0.307717 17.545051-7.182222 17.868283-16.707233 0.319354-9.535354-7.175758-17.550222-16.705939-17.868282a1386.24 1386.24 0 0 1-20.419233-0.839112c-48.521051-2.358303-94.161455-7.236525-134.434909-11.544565-90.868364-9.712485-139.353212-13.818828-162.333737 6.833131-8.02004 7.21196-12.262141 17.004606-12.262141 28.317737 0 0.672323 0.029737 1.348525 0.065939 2.026021l0.005172 0.100848c-0.045253 0.524929-0.071111 1.039515-0.071111 1.539879 0 41.464242 9.561212 81.661414 28.414707 119.484768 17.493333 35.088808 42.251636 66.663434 73.583192 93.927434 9.169455-6.551273 20.155475-13.604202 32.524929-20.050748 49.737697-25.921939 97.838545-29.339152 139.099798-9.884444 101.590626 47.906909 142.060606 47.837091 206.198949-0.364606 5.70699-4.287354 13.814949-3.140525 18.103596 2.567758 4.289939 5.708283 3.140525 13.813657-2.567757 18.103595-38.425859 28.877576-69.968162 41.279354-105.143596 41.280647-0.484848 0-0.969697-0.002586-1.457132-0.007758-32.760242-0.316768-69.311354-11.381657-126.16404-38.190545-51.905939-24.47903-106.215434 1.008485-139.548444 23.465374 64.611556 47.922424 146.341495 74.101657 232.704 74.101656 117.55701 0 226.204444-48.934788 292.761858-131.362909l0.002586-0.002586c18.79402-14.959192 12.028121-41.362101-23.442101-36.152889z" fill="" p-id="1863"></path></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+9
View File
@@ -0,0 +1,9 @@
<template>
<Index></Index>
</template>
<script setup>
import Index from "./views/home/index.vue"
</script>
<style></style>
+89
View File
@@ -0,0 +1,89 @@
// request.js
import axios from 'axios';
import { ElMessage } from 'element-plus'
import { usePageState } from '../store/page_sate';
import { CONSTANT } from '../constant';
const ERR_NETWORK = "ERR_NETWORK";
const request = axios.create({
baseURL: 'http://127.0.0.1:8000',
timeout: 50000,
withCredentials: true, // 允许发送跨域凭证(例如,用于保持登录状态)
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
const pageState = usePageState();
// console.log('config', config);
if (!CONSTANT.NO_AUTH_URL.includes(config.url)) {
config.url = '/api/v1' + config.url;
}
if (pageState.Token && !CONSTANT.NO_AUTH_URL.includes(config.url)) {
// 添加 token 参数到 URL 中
config.headers = {
...config.headers,
'm-token': pageState.Token,
};
}
return config;
},
(error) => {
// 处理请求错误
handleException(error);
// return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
// console.log('request.interceptors.response', response);
if (response && response.data.code != 200) {
ElMessage({ message: response.data.msg, type: 'error' })
// Promise.reject();
}
return response;
},
(error) => {
if (error.response && error.response.status === 401) {
// 未授权,可能是登录状态过期,执行登出操作
logout();
}
handleException(error);
// return Promise.reject(error);
}
);
// 异常处理
const handleException = (error) => {
console.log('handleException', error);
if (error.code == ERR_NETWORK) {
ElMessage({ message: `网络错误!`, type: 'error' })
} else if (error.response && error.response.data.code != 200) {
ElMessage({ message: error.response.data.msg, type: 'error' })
};
// if (error.response) {
// // 服务器返回错误状态码
// console.error('Server Error:', error.response.status, error.response.data);
// } else if (error.request) {
// // 请求发送成功,但没有收到响应
// console.error('No response received:', error.request);
// } else {
// // 其他错误
// console.error('Error:', error.message);
// }
};
// 登出系统
const logout = () => {
// 执行登出逻辑,清除本地存储的用户信息等
console.log('User logged out');
};
export { request, handleException, logout };
+33
View File
@@ -0,0 +1,33 @@
a {
text-decoration: none;
}
/* #main-container {
display: block;
margin-left: 0px;
padding: 0;
} */
.main-center-body {
font-family: Arial, sans-serif;
margin: 0 0 0 0;
padding: 0;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
/* flex: 1; */
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
/* margin-top: -10vh; */
/* flex: 1; */
}
+26
View File
@@ -0,0 +1,26 @@
textarea {
width: 99%;
padding: 2px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
}
button {
width: 46%;
padding: 10px;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 3%;
margin-left: 1%;
margin-right: 1%;
}
@media (max-width: 768px) {
.container {
max-width: 90%;
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

+1
View File
@@ -0,0 +1 @@
@import './base.css';
+1
View File
@@ -0,0 +1 @@
<svg t="1700568914730" class="titleicon" viewBox="0 0 1280 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4276" width="200" height="200"><path d="M1046.485822 53.112897c4.54104 44.898732-52.062062 38.886651-76.749969 36.200402-24.687907-2.622291-56.794977 18.995617-73.29622 42.532274-9.977496 14.390619-18.164159 37.351651-24.048323 57.498518 104.891624-10.233329 257.368228 37.351651 342.176943 157.657228 15.030202 23.536657 27.246239 48.160605 36.903944 73.616011 38.183109 101.054125 34.089778 207.03304 4.732914 308.790707a544.732902 544.732902 0 0 1-21.234158 61.016225 406.90275 406.90275 0 0 1-65.813098 105.914957 403.12921 403.12921 0 0 1-165.204307 118.514743l-18.547909 6.587706-12.08812 1.918749H215.923245l-7.54708-0.7675-12.791662-3.38979c-104.891624-33.322278-189.956172-133.225154-195.32867-325.29195L0 674.403893l0.575625-13.23937c1.854791-27.629989 8.186663-62.743099 21.553949-101.885583a410.996082 410.996082 0 0 1 127.596823-186.054716 383.557968 383.557968 0 0 1 106.426623-62.551224c86.023923-33.514153 176.397011-35.816652 260.118436-19.827075l4.541039 0.831458 4.349165 0.895416 15.222077 3.453749 11.832287 3.197915 59.033518 18.867701-36.072486 44.770815c-15.733744 19.507284-43.875399 47.201231-83.401632 74.703302l-17.39666 11.512496-17.908326 10.744995-8.31458 4.477082 26.222906 116.404119-36.328318 9.785621-14.070828 3.453749c-25.583323 5.500414-50.974771 7.035414-74.063719-0.447709-18.803742-6.012081-42.660191-24.304157-63.830391-46.369772l-8.890205-9.657705-3.645623-4.349165-1.726874 3.709582a136.231194 136.231194 0 0 0-9.08208 30.380196c-5.756248 33.450195 8.058747 73.424137 24.815823 92.547671l4.604998 4.732914 18.036243 15.541869 6.971455 5.244581c39.334359 27.693947 79.116426 30.380196 130.986613-8.570413 20.146867-15.158119 38.694776-43.939357 54.556436-83.017883 7.16333-17.588534 13.431245-36.456235 19.315409-56.794976l3.261874-11.896246 3.261874-12.407911 9.593746-38.502901 5.756247-20.274783 5.820206-17.588535c11.256662-31.787279 26.222906-62.231433 45.858106-91.076629 51.998104-75.854552 129.707447-128.939947 236.837612-149.790356-23.152907-23.024991-58.074143-49.311855-97.728293-51.614354-90.820796-6.971455-147.935565 45.666231-155.418687 50.143313-7.611039 4.477082-32.810612 18.547909-54.684353 0-21.809783-18.547909-10.872912-83.145799 45.410399-113.270162 56.28331-30.124363 151.261396-36.328318 213.428871 14.070828 35.304986 28.71728 56.027477 68.435389 67.284139 97.47246l6.39583-0.959375 1.535-0.255833c-1.023333-23.728532 1.023333-54.492478 13.942911-96.257252C897.7188 12.819164 943.385031 4.184792 959.886274 0.795002c16.565202-3.325832 81.546842 1.918749 86.599548 52.253937z m-170.960555 215.091787l-6.779581 0.831458-17.652492 2.878124c-88.77413 16.309368-150.110147 57.882268-191.235339 118.003077-15.605827 22.769157-27.502072 47.137272-36.64811 72.848512l-4.796873 14.454577-4.15729 14.582494-13.623119 53.852895-3.645624 13.047494a632.227866 632.227866 0 0 1-22.065616 64.853724c-20.722492 51.102687-46.433731 90.884755-80.139759 116.276202-81.099134 60.888308-157.337436 55.835602-222.319075 9.913538l-10.169371-7.483122-9.785621-8.058747-7.674997-6.77958-11.192704-10.553121c-34.665402-36.136444-58.266018-102.653083-47.776855-163.2216a223.214492 223.214492 0 0 1 28.909155-76.494135c9.785621-16.501243 19.763117-29.228946 27.94978-37.991235l4.668956-4.796873 43.491649-42.212483 20.274783 57.370602c7.227289 20.466658 48.73623 64.59789 62.423308 68.947055l1.854791 0.511667 2.110624 0.319791 3.006041 0.127917-22.385408-99.647043 29.420822-13.111453a354.968605 354.968605 0 0 0 45.474356-24.304157l14.326661-9.593746 14.326661-10.425204-13.814995-1.279166a390.337549 390.337549 0 0 0-127.916614 11.384579l-17.972285 5.308539-17.780409 6.395831a305.784667 305.784667 0 0 0-84.936632 49.88748A332.839031 332.839031 0 0 0 95.937461 584.99018c-10.872912 31.787279-15.989577 60.05685-17.460618 80.779341l-0.38375 8.442497 0.255833 19.827075c2.814166 86.983298 25.32749 150.046189 61.527892 193.921588 22.065616 26.734572 46.689564 43.491649 68.819138 52.445812l9.337913 3.38979 2.942082 0.831458h745.561987l11.000829-3.837499a320.622994 320.622994 0 0 0 108.601206-70.098304l12.791661-13.111453 10.872912-12.279995c22.513324-26.734572 39.014567-53.852895 48.992063-75.598719l3.837499-8.954163 8.31458-21.234158c3.581665-9.785621 6.651664-19.25145 10.361246-32.107071 25.007698-86.791423 28.397488-176.141178-2.814166-258.711352a322.349868 322.349868 0 0 0-28.525405-57.626435c-38.758734-54.876228-94.978086-90.373088-161.942434-109.112872-42.532274-11.960203-87.750797-15.86166-112.56662-13.751036z m104.699749 166.931182a60.696434 60.696434 0 0 1 35.432902 55.963519c0 33.386236-25.775198 60.4406-57.43456 60.504558a57.562476 57.562476 0 0 1-53.149353-37.41561 62.934974 62.934974 0 0 1 12.47187-66.004973 55.387894 55.387894 0 0 1 62.679141-13.111452z" p-id="4277"></path></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

+34
View File
@@ -0,0 +1,34 @@
// 定义一些常量名
const CONSTANT = {
PAGE: 1,
PAGE_SIZE: 6,
TOTAL: 0,
LOG_TASK_ID: "00000000-0000-0000-0000-000000000001",
STORE_TOKEN_NAME: '__message_nest_token__',
NO_AUTH_URL: [
'/auth',
],
WAYS_DATA: [
{
type: 'Email',
label: '邮箱',
inputs: [
{ subLabel: 'smtp服务地址', value: '', col: 'server' },
{ subLabel: 'smtp服务端口', value: '', col: 'port' },
{ subLabel: '邮箱账号', value: '', col: 'account' },
{ subLabel: '邮箱密码', value: '', col: 'passwd' },
{ subLabel: '渠道名', value: '', col: 'name' },
]
},
{
type: 'Dtalk',
label: '钉钉',
inputs: [
{ subLabel: 'webhook地址', value: '', col: 'webhook_url' },
{ subLabel: '渠道名', value: '', col: 'name' },
]
},
]
}
export { CONSTANT }
+20
View File
@@ -0,0 +1,20 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// import './assets/styles/global.css'
import pinia from './store';
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.use(pinia);
app.mount('#app')
+47
View File
@@ -0,0 +1,47 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import LoginInex from '../views/home/login.vue'
import { CONSTANT } from '../constant'
const router = createRouter({
// history: createWebHistory(import.meta.env.BASE_URL),
history: createWebHashHistory(),
routes: [
{
path: '/login',
name: 'login',
component: LoginInex
},
{
path: '/sendways',
name: 'sendWays',
component: () => import('../views/tabsTools/sendWays/sendWays.vue')
},
{
path: '/sendtasks',
name: 'sendtasks',
component: () => import('../views/tabsTools/sendTasks/sendTasks.vue')
},
{
path: '/sendlogs',
name: 'sendlogs',
component: () => import('../views/tabsTools/sendLogs/sendLogs.vue')
},
{
path: '/settings',
name: 'settings',
component: () => import('../views/tabsTools/settings/settings.vue')
},
]
})
// 登录失效重定向到登录页面
router.beforeEach((to, from, next) => {
const isAuthenticated = Boolean(localStorage.getItem(CONSTANT.STORE_TOKEN_NAME));
if (!isAuthenticated && to.path !== '/login') {
next('/login');
} else {
next();
}
});
export default router
+6
View File
@@ -0,0 +1,6 @@
// store/index.js
import { createPinia } from 'pinia';
const pinia = createPinia();
export default pinia;
+23
View File
@@ -0,0 +1,23 @@
import { defineStore } from 'pinia';
export const usePageState = defineStore({
id: 'pageState',
state: () => ({
isLogin: false, // 全局的登录状态
Token: '', // 全局的登录状态
isShowAddWayDialog: false,
ShowDialogData: {}
}),
actions: {
setIsLogin(state) {
this.isLogin = state;
},
setToken(token) {
this.Token = token;
},
setShowAddWayDialog(status) {
this.isShowAddWayDialog = status;
},
},
});
+27
View File
@@ -0,0 +1,27 @@
import useClipboard from "vue-clipboard3";
import { ElMessage } from 'element-plus'
function copyToClipboard(obj) {
const { toClipboard } = useClipboard();
try {
if (!obj) {
ElMessage({
message: `复制内容为空`,
type: 'error',
})
} else {
toClipboard(obj);
ElMessage({
message: '复制成功',
type: 'success',
})
}
} catch (e) {
ElMessage({
message: `复制失败,${e}`,
type: 'error',
})
}
};
export { copyToClipboard };
+28
View File
@@ -0,0 +1,28 @@
const gethttpOrigin = () => {
return window.location.origin
}
class ApiStrGenerate {
static getCurlString(task_id, options) {
let data = { task_id: task_id };
data.text = 'Hello World!';
if (options.html) {
data.html = '<h1> Hello World! </h1>';
}
if (options.markdown) {
data.html = '** Hello World! **';
}
let dataStr = JSON.stringify(data, null, 4)
let example = `curl -X POST --location '${gethttpOrigin()}/api/v1/message/send' \\
--header 'Accept: application/json' \\
--data '${dataStr}'`;
return example;
}
}
export { ApiStrGenerate };
@@ -0,0 +1,23 @@
<template>
<el-popconfirm width="70px" confirm-button-text="确定" cancel-button-text="算了" icon-color="#626AEF" title="确定删除吗?"
@confirm="handleDelete()">
<template #reference>
<el-button link size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
handleDelete() {
// 触发自定义事件
this.$emit('customHandleDelete');
}
}
});
</script>
+121
View File
@@ -0,0 +1,121 @@
<template>
<div class="inex-title-bar" v-if="pageState.isLogin">
<el-menu :collapse="isCollapse" breakpoint="768px" mode="horizontal" @select="handleSelect()"
:default-active="currActivate()" :ellipsis="false" :menu-width="'auto'">
<el-menu-item index="0" :disabled="false">
<img style="width: 60px" src="../../../public/titlelogo.svg" alt="Element logo" />
</el-menu-item>
<div class="flex-grow" style="flex-grow: 1" />
<div v-for="(item, index) in menuData" :key="index" class="banner-title">
<router-link :to="{ path: item.path }">
<el-menu-item :index="item.id">{{ item.title }}</el-menu-item>
</router-link>
</div>
<el-button plain class="logout-btn" @click="clickLogout()">登出</el-button>
</el-menu>
</div>
<router-view></router-view>
</template>
<script>
import { ref, onMounted, reactive } from 'vue';
import { usePageState } from '../../store/page_sate.js';
import { CONSTANT } from '../../constant'
import { useRouter, useRoute } from 'vue-router';
export default {
setup() {
const pageState = usePageState();
const router = useRouter();
const isCollapse = ref(false);
const menuData = reactive([
{
id: '1',
title: '发信日志',
path: '/sendlogs',
}, {
id: '2',
title: '发信任务',
path: '/sendtasks',
}, {
id: '3',
title: '发信渠道',
path: '/sendways',
}, {
id: '4',
title: '设置',
path: '/settings',
},
]);
const checkIsLogin = () => {
pageState.isLogin = Boolean(localStorage.getItem(CONSTANT.STORE_TOKEN_NAME));
};
const clickLogout = () => {
localStorage.removeItem(CONSTANT.STORE_TOKEN_NAME);
console.log('clickLogout')
router.push('/login', { replace: true }).then(() => { router.go() });
};
const loadLocalToken = () => {
pageState.setToken(localStorage.getItem(CONSTANT.STORE_TOKEN_NAME));
}
const currActivate = () => {
const cur_path = useRoute().path;
let result = '1';
menuData.forEach(element => {
if (element.path == cur_path) {
result = element.id;
};
});
return result;
}
onMounted(() => {
checkIsLogin();
loadLocalToken();
});
const handleSelect = () => { };
return {
isCollapse, handleSelect, menuData, pageState, clickLogout, currActivate
};
},
};
</script>
<style scoped>
.el-menu-item ul {
height: 15vh;
}
.el-menu-item {
width: 150px !important;
font-size: 15px;
justify-content: center;
align-items: center;
}
.el-menu-item li {
margin: 0 auto;
}
.logout-btn {
margin: auto 40px auto 40px;
float: right;
/* background-color: #f7efee;
border: none;
padding: 10px 20px;
text-align: center;
text-decoration: none;
font-size: 16px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s ease; */
}
</style>
+123
View File
@@ -0,0 +1,123 @@
<template>
<div class="login-center-container" v-if="!pageState.isLogin">
<div class="main-center-body">
<div class="container">
<img class="login-logo" src="../../../public/logo.svg" alt="login logo">
<p class="desc">A Message Way Hosted Site</p>
<div class="login-block">
<p class="login-text">账号</p>
<el-input style="width: 80%;" v-model="account" placeholder="请输入账号" />
</div>
<div class="login-block">
<p class="login-text">密码</p>
<el-input style="width: 80%;" v-model="passwd" type="password" placeholder="请输入密码" show-password />
</div>
<div class="btn-area">
<el-button id="custom-h2d-copy-button" type="success" @click="clickLogin()">登录</el-button>
<el-button type="primary" @click="clickRegister()">注册</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import { toRefs, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus'
import { request } from '../../api/api'
import { CONSTANT } from '../../constant'
import { usePageState } from '../../store/page_sate';
import { useRouter } from 'vue-router';
export default {
setup() {
const router = useRouter();
const pageState = usePageState();
const state = reactive({
account: 'admin',
passwd: '123456',
});
onMounted(() => {
});
// 登录
const clickLogin = async () => {
const rspe = await request.post('/auth', { username: state.account, passwd: state.passwd });
const rsp = rspe.data;
if (rsp.code != 200) {
ElMessage({ message: rsp.msg, type: 'error' });
} else {
pageState.setToken(rsp.data.token);
pageState.setIsLogin(true);
localStorage.setItem(CONSTANT.STORE_TOKEN_NAME, rsp.data.token);
router.push('/sendlogs', { replace: true })
}
};
// 注册
const clickRegister = () => {
ElMessage({ message: `暂未开放注册!`, type: 'error' })
};
return { ...toRefs(state), clickLogin, clickRegister, pageState };
}
}
</script>
<style scoped>
@import url('../../../src/assets/center_button_textarea.css');
.login-logo {
height: 200px !important;
}
.login-center-container {
text-align: center;
}
.login-center-container img {
margin: 0 auto;
display: block;
width: 300px;
height: 300px;
}
.desc {
margin: 0 auto;
display: block;
font-size: 25px;
margin-bottom: 80px;
/* color: rgb(64, 87, 45); */
}
.login-text {
font-size: 13px;
/* width: 20px; */
width: 20%;
display: inline;
/* display: flex; */
/* justify-content: right; */
/* align-items: center; */
}
.login-block {
margin-top: 20px;
}
.btn-area {
margin-top: 30px;
}
</style>
@@ -0,0 +1,155 @@
<template>
<div class="main-center-body">
<div class="container">
<div class="search-input-sendways">
<el-input v-model="search" size="small" placeholder="根据任务名字搜索相应日志" @change="filterFunc()" />
</div>
<hr />
<div ref="refContainer">
<el-table :data="tableData" empty-text="发信日志为空" :row-style="rowStyle()">
<el-table-column label="ID" prop="id" width="100px" />
<el-table-column label="任务名" prop="task_name" show-overflow-tooltip width="150px" />
<el-table-column label="发信日志" prop="log">
<template #default="scope">
<el-tooltip placement="top">
<template #content>
<div v-html="TransHtml(scope.row.log)"></div>
</template>
<span class="log-overflow">{{ scope.row.log }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="created_on" width="160px" />
<el-table-column label="状态" prop="status" width="60px">
<template #default="scope">
<el-tag v-if="scope.row.status == 0" type="danger">失败</el-tag>
<el-tag v-if="scope.row.status == 1" type="success">成功</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-block">
<el-pagination layout="prev, pager, next" :total="total" :page-size="pageSize" @current-change="handPageChange" />
<el-text class="total-tip" size="small">{{ total }}</el-text>
</div>
</div>
</div>
</template>
<script >
import { reactive, toRefs, onMounted } from 'vue'
import { request } from '../../../api/api'
import { copyToClipboard } from '../../../util/clipboard.js';
import { useRouter, useRoute } from 'vue-router';
import { CONSTANT } from '@/constant'
export default {
components: {
},
setup() {
const router = useRoute();
const state = reactive({
search: '',
optionValue: '',
tableData: [],
total: CONSTANT.TOTAL,
pageSize: CONSTANT.PAGE_SIZE,
currPage: CONSTANT.PAGE,
});
const handleDelete = async (index, row) => {
const rsp = await request.post('/sendways/delete', { id: row.id });
if (rsp.status == 200) {
state.tableData.splice(index, 1);
}
}
const TransHtml = (raw) => {
if (raw) {
return raw.replace(/\n/g, '<br />')
}
return ''
}
const handPageChange = async (pageNum) => {
state.currPage = pageNum;
await queryListData(pageNum, state.pageSize);
}
const rowStyle = () => {
return {
'font-size': '13px',
}
}
const filterFunc = async () => {
await queryListData(state.currPage, state.pageSize, state.search, state.optionValue);
}
const queryListData = async (page, size, name = '', taskid = '') => {
let params = { page: page, size: size, name: name, taskid: taskid };
const rsp = await request.get('/sendlogs/list', { params: params });
state.tableData = await rsp.data.data.lists;
state.total = await rsp.data.data.total;
}
onMounted(async () => {
state.search = router.query.name;
await queryListData(1, state.pageSize, router.query.name, router.query.taskid);
});
return {
...toRefs(state), handleDelete, TransHtml,
rowStyle, handPageChange, filterFunc, copyToClipboard
};
}
}
</script>
<style scoped>
hr {
color: #FAFCFF;
background-color: #FAFCFF;
border-color: #FAFCFF;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
max-width: 1000px;
width: 100%;
margin-top: -10vh;
}
.pagination-block {
margin-top: 30px;
display: flex;
justify-content: flex-end;
}
.total-tip {
display: inline-block;
}
.search-input-sendways {
width: 200px;
display: inline-flex;
}
.log-overflow {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
</style>
@@ -0,0 +1,261 @@
<template>
<el-dialog v-model="isShow" width="58%" :close-on-press-escape="false" :before-close="() => { }" :show-close="false">
<template #header="">
<el-text class="mx-1">添加发信任务(绑定关联实例)</el-text>
<el-tooltip placement="top">
<template #content>
一个任务可以关联创建多个实例
<br />
选择不同的渠道填写的实例信息也不一样
<br />
一个任务可以绑定一个实例也可以绑定多个实例多个实例意味着一个消息可以推送给多个消息渠道
</template>
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</template>
<div class="add-top">
<el-input v-model="currTaskInput.taskName" placeholder="请输入任务名" size="small" class="taskNameInput"></el-input>
</div>
<div class="dashed" />
<div class="ins-area">
<div class="ins-add">
<el-input v-model="searchWayID" placeholder="请输入要添加的渠道id" size="small" @change="searchID"
class="searchInput"></el-input>
<el-button @click="searchID()" size="small" type="primary" style="margin-left: 20px;">查询</el-button>
<div class="store-area" v-if="isShowAddBox">
<div class="display-label">
<el-text class="mx-1" size="small">渠道名{{ currWayTmp.name }}</el-text><br />
<el-text class="mx-1" size="small">渠道类型{{ currWayTmp.type }}</el-text> <br />
<el-text class="mx-1" size="small">渠道创建时间{{ currWayTmp.created_on }}</el-text><br />
</div>
<el-radio-group v-model="currInsInput.content_type" class="ml-4">
<el-radio label="text" size="small">text</el-radio>
<el-radio label="html" size="small">html</el-radio>
</el-radio-group>
<div>
<el-input v-model="currInsInput.toAccount" placeholder="目的邮箱账号(发给谁)" size="small"
style="width: 200px; margin: 10px 40px 5px 0;" class="searchInput"></el-input>
<el-input v-model="currInsInput.title" placeholder="邮箱标题" size="small"
style="width: 200px; margin: 0px 40px 5px 0;" class="searchInput"></el-input>
<el-button @click="clickStore()" size="small" style="width: 200px">暂存</el-button>
</div>
</div>
</div>
<div class="ins-table">
<el-table :data="insTableData" empty-text="发信实例为空" style="width: 100%" max-height="300"
:row-style="insRowStyle()">
<el-table-column prop="way_name" label="渠道名" />
<el-table-column prop="way_type" label="渠道+内容类型">
<template #default="scope">
{{ scope.row.way_type }}+{{ scope.row.content_type }}
</template>
</el-table-column>
<el-table-column prop="way_type" label="额外信息">
<template #default="scope">
{{ formatExtraInfo(scope) }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="60px">
<template #default="scope">
<el-button link type="primary" size="small" @click.prevent="insTableData.splice(scope.$index, 1)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancer()" size="small">取消</el-button>
<el-button type="primary" size="small" @click="handleSubmit()">
确定添加
</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { defineComponent, onMounted, watch, reactive, toRefs } from 'vue';
import { _ } from 'lodash';
import { QuestionFilled } from '@element-plus/icons-vue'
import { v4 as uuidv4 } from 'uuid';
import { usePageState } from '../../../store/page_sate.js';
import { request } from '../../../api/api'
export default defineComponent({
components: {
QuestionFilled,
},
props: {
componentName: String
},
setup(props) {
const pageState = usePageState();
const state = reactive({
insTableData: [],
isShow: false,
isShowAddBox: false,
searchWayID: '',
currWayTmp: {},
currInsInput: {
content_type: 'text'
},
currTaskInput: {
taskName: '',
taskId: uuidv4(),
},
});
watch(pageState.ShowDialogData, (newValue, oldValue) => {
if (newValue[props.componentName]) {
state.isShow = pageState.ShowDialogData[props.componentName].isShow;
resetPageInitData();
}
});
const handleCancer = () => {
if (pageState.ShowDialogData[props.componentName]) {
pageState.ShowDialogData[props.componentName].isShow = false;
}
}
// 页面每次弹出,重置数据
const resetPageInitData = () => {
state.insTableData = [];
state.currInsInput = { content_type: 'text' };
state.currWayTmp = {};
state.searchWayID = '';
state.isShowAddBox = false;
state.currTaskInput = {
taskName: '',
taskId: uuidv4(),
}
}
const insRowStyle = () => {
return {
'font-size': '12px'
}
}
// 点击暂存实例
const clickStore = () => {
let insData = {
id: uuidv4(),
task_id: state.currTaskInput.taskId,
way_id: state.currWayTmp.id,
way_type: state.currWayTmp.type,
way_name: state.currWayTmp.name,
content_type: state.currInsInput.content_type,
config: JSON.stringify({ to_account: state.currInsInput.toAccount, title: state.currInsInput.title })
};
state.insTableData.push(insData);
}
const searchID = async () => {
const rsp = await request.get('/sendways/get', { params: { id: state.searchWayID } });
let data = await rsp.data;
state.isShowAddBox = Boolean(data.data);
if (data.data) {
state.currWayTmp = data.data;
}
}
const getFinalData = () => {
let postData = { id: state.currTaskInput.taskId, name: state.currTaskInput.taskName }
postData.ins_data = state.insTableData
return postData
}
const handleSubmit = async () => {
let postData = getFinalData();
const rsp = await request.post('/sendtasks/ins/addmany', postData);
if (await rsp.data.code == 200) {
handleCancer();
window.location.reload();
}
}
const formatExtraInfo = (scope) => {
if (!scope.row.config) {
return ""
}
let config = JSON.parse(scope.row.config)
let info = `发送账号:${config.to_account}\n标题:${config.title}`
return info
}
return {
...toRefs(state), handleCancer, handleSubmit,
searchID, formatExtraInfo,
clickStore, insRowStyle
};
},
});
</script>
<style scoped>
::v-deep(.el-table .cell) {
white-space: pre-line !important;
}
.dashed {
border-top: 2px dashed var(--el-border-color);
margin-bottom: 20px;
}
.ins-area {
display: flex;
max-width: 900px;
}
.add-top {
margin-bottom: 20px;
}
.ins-add {
flex: 45%;
}
.ins-table {
flex: 55%;
}
.searchInput {
max-width: 200px;
}
.wayTitleInput {
max-width: 200px;
}
.taskNameInput {
/* width: 80%; */
max-width: 200px;
}
.display-label {
margin-top: 10px;
margin-bottom: 10px;
}
</style>
@@ -0,0 +1,263 @@
<template>
<el-dialog v-model="isShow" width="58%" :close-on-press-escape="false" :before-close="() => { }" :show-close="false">
<template #header="">
<el-text class="mx-1">编辑发信任务</el-text>
<el-tooltip placement="top">
</el-tooltip>
</template>
<div class="dashed" />
<div class="ins-area">
<div class="ins-add">
<el-input v-model="searchWayID" placeholder="请输入要添加的渠道id" size="small" @change="searchID"
class="searchInput"></el-input>
<el-button @click="searchID()" size="small" type="primary" style="margin-left: 20px;">查询</el-button>
<div class="store-area" v-if="isShowAddBox">
<div class="display-label">
<el-text class="mx-1" size="small">渠道名{{ currWayTmp.name }}</el-text><br />
<el-text class="mx-1" size="small">渠道类型{{ currWayTmp.type }}</el-text> <br />
<el-text class="mx-1" size="small">渠道创建时间{{ currWayTmp.created_on }}</el-text><br />
</div>
<el-radio-group v-model="currInsInput.content_type" class="ml-4">
<el-radio label="text" size="small">text</el-radio>
<el-radio label="html" size="small">html</el-radio>
</el-radio-group>
<div>
<el-input v-model="currInsInput.toAccount" placeholder="目的邮箱账号(发给谁)" size="small"
style="width: 200px; margin: 10px 40px 5px 0;" class="searchInput"></el-input>
<el-input v-model="currInsInput.title" placeholder="邮箱标题" size="small"
style="width: 200px; margin: 0px 40px 5px 0;" class="searchInput"></el-input>
</div>
<div>
<el-button @click="handleAddSubmit()" size="small" style="width: 200px">添加</el-button>
</div>
</div>
</div>
<div class="ins-table">
<el-table :data="insTableData" empty-text="发信实例为空" style="width: 100%" max-height="300"
:row-style="insRowStyle()">
<el-table-column prop="way_name" label="渠道名" />
<el-table-column prop="way_type" label="渠道+内容类型">
<template #default="scope">
{{ scope.row.way_type }}+{{ scope.row.content_type }}
</template>
</el-table-column>
<el-table-column prop="way_type" label="额外信息">
<template #default="scope">
{{ formatExtraInfo(scope) }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="60px">
<template #default="scope">
<tableDeleteButton @customHandleDelete="handleDelete(scope.$index, scope.row)" />
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancer()" size="small">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { defineComponent, onMounted, watch, reactive, toRefs } from 'vue';
import { _ } from 'lodash';
import { QuestionFilled } from '@element-plus/icons-vue'
import { v4 as uuidv4 } from 'uuid';
import { usePageState } from '@/store/page_sate.js';
import { request } from '@/api/api'
import tableDeleteButton from '@/views/common/tableDeleteButton.vue'
export default defineComponent({
components: {
QuestionFilled,
tableDeleteButton,
},
props: {
componentName: String
},
setup(props) {
const pageState = usePageState();
const state = reactive({
insTableData: [],
isShow: false,
isShowAddBox: false,
searchWayID: '',
currWayTmp: {},
currInsInput: {},
currTaskInput: {
taskName: '',
taskId: '',
},
});
watch(pageState.ShowDialogData, (newValue, oldValue) => {
if (newValue[props.componentName]) {
state.isShow = pageState.ShowDialogData[props.componentName].isShow;
resetPageInitData();
state.currTaskInput.taskId = pageState.ShowDialogData[props.componentName].rowData.id;
queryListData();
}
});
const queryListData = async () => {
let params = { id: state.currTaskInput.taskId };
const rsp = await request.get('/sendtasks/ins/gettask', { params: params });
state.insTableData = await rsp.data.data.ins_data;
state.total = await rsp.data.data.total;
}
const handleCancer = () => {
if (pageState.ShowDialogData[props.componentName]) {
pageState.ShowDialogData[props.componentName].isShow = false;
}
}
// 页面每次弹出,重置数据
const resetPageInitData = () => {
state.insTableData = [];
state.currInsInput = {};
state.currWayTmp = {};
state.searchWayID = '';
state.isShowAddBox = false;
state.currTaskInput = {
taskName: '',
// taskId: uuidv4(),
}
}
const insRowStyle = () => {
return {
'font-size': '12px'
}
}
const handleDelete = async (index, row) => {
const rsp = await request.post('/sendtasks/ins/delete', { id: row.id });
if (rsp.status == 200) {
state.insTableData.splice(index, 1);
}
}
// // 点击暂存实例
// const clickStore = () => {
// let insData = {
// id: uuidv4(),
// task_id: state.currTaskInput.taskId,
// way_id: state.currWayTmp.id,
// way_type: state.currWayTmp.type,
// way_name: state.currWayTmp.name,
// content_type: state.currInsInput.content_type
// };
// }
const formatExtraInfo = (scope) => {
if (!scope.row.config) {
return ""
}
let config = JSON.parse(scope.row.config)
let info = `发送账号:${config.to_account}\n标题:${config.title}`
return info
}
const searchID = async () => {
const rsp = await request.get('/sendways/get', { params: { id: state.searchWayID } });
let data = await rsp.data;
state.isShowAddBox = Boolean(data.data);
if (data.data) {
state.currWayTmp = data.data;
}
}
const getFinalData = () => {
let postData = {
id: uuidv4(),
task_id: state.currTaskInput.taskId,
way_id: state.currWayTmp.id,
way_type: state.currWayTmp.type,
way_name: state.currWayTmp.name,
content_type: state.currInsInput.content_type,
config: JSON.stringify({ to_account: state.currInsInput.toAccount, title: state.currInsInput.title })
}
return postData
}
const handleAddSubmit = async () => {
let postData = getFinalData();
const rsp = await request.post('/sendtasks/ins/addone', postData);
if (await rsp.data.code == 200) {
state.insTableData.push(postData);
}
}
return {
...toRefs(state), handleCancer, handleAddSubmit,
searchID, handleDelete, insRowStyle, formatExtraInfo
};
},
});
</script>
<style scoped>
::v-deep(.el-table .cell) {
white-space: pre-line !important;
}
.dashed {
border-top: 2px dashed var(--el-border-color);
margin-bottom: 20px;
}
.ins-area {
display: flex;
max-width: 900px;
}
/* .add-top {
margin-bottom: 20px;
}
*/
.ins-add {
flex: 45%;
}
.ins-table {
flex: 55%;
}
.searchInput {
max-width: 200px;
}
.wayTitleInput {
max-width: 200px;
}
.taskNameInput {
/* width: 80%; */
max-width: 200px;
}
.display-label {
margin-top: 10px;
margin-bottom: 10px;
}
</style>
@@ -0,0 +1,204 @@
<template>
<div class="main-center-body">
<div class="container">
<div class="search-input-sendways">
<el-input v-model="search" size="small" placeholder="搜索" @change="filterFunc()" />
</div>
<div class="search-box">
<el-button size="small" type="primary" @click="clickAdd">新增任务</el-button>
</div>
<hr />
<div ref="refContainer">
<el-table :data="tableData" empty-text="发信任务为空" :row-style="rowStyle()">
<el-table-column label="ID" prop="id" width="320px" />
<el-table-column label="任务名" prop="name" />
<el-table-column label="创建时间" prop="created_on" />
<el-table-column fixed="right" label="操作" width="190px">
<template #default="scope">
<el-button link size="small" type="primary" @click="handleViewAPI(scope.$index, scope.row)">接口</el-button>
<el-button link size="small" type="primary" @click="handleViewLogs(scope.$index, scope.row)">日志</el-button>
<el-button link size="small" type="primary" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<tableDeleteButton @customHandleDelete="handleDelete(scope.$index, scope.row)" />
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-block">
<el-pagination layout="prev, pager, next" :total="total" :page-size="pageSize" @current-change="handPageChange" />
<el-text class="total-tip" size="small">{{ total }}</el-text>
</div>
</div>
</div>
<addTaskPopUpComponent :componentName="addTaskPopUpComponentName" />
<editTaskPopUpComponent :componentName="editTaskPopUpComponentName" />
<viewTaskAPIPopUpComponent :componentName="viewTaskAPIPopUpComponentName" />
</template>
<script >
import { computed, ref, reactive, toRefs, onMounted } from 'vue'
// import { InfoFilled } from '@element-plus/icons-vue'
import { usePageState } from '../../../store/page_sate';
import { request } from '../../../api/api'
import addTaskPopUpComponent from './addTaskPopUp.vue'
import editTaskPopUpComponent from './editTaskPopUp.vue'
import viewTaskAPIPopUpComponent from './viewTaskAPIPopUp.vue'
import tableDeleteButton from '@/views/common/tableDeleteButton.vue'
import { useRouter } from 'vue-router';
import { CONSTANT } from '@/constant'
export default {
components: {
addTaskPopUpComponent,
editTaskPopUpComponent,
tableDeleteButton,
viewTaskAPIPopUpComponent,
// InfoFilled,
},
setup() {
const pageState = usePageState();
const router = useRouter();
const state = reactive({
addTaskPopUpComponentName: 'addTaskPopUpComponentName',
editTaskPopUpComponentName: 'editTaskPopUpComponentName',
viewTaskAPIPopUpComponentName: 'viewTaskAPIPopUpComponentName',
search: '',
optionValue: '',
dialogFormVisible: false,
// confirmBtnVisible: false,
tableData: [],
total: CONSTANT.TOTAL,
pageSize: CONSTANT.PAGE_SIZE,
currPage: CONSTANT.PAGE,
displayCols: [
{ 'col': 'id', 'label': '任务ID' },
{ 'col': 'name', 'label': '任务名' },
{ 'col': 'type', 'label': '发信任务' },
{ 'col': 'created_on', 'label': '创建时间' },
],
options: [
{ label: '邮箱', value: 'Email' },
{ label: '钉钉', value: 'Dtalk' }
]
});
const handleEdit = (index, row) => {
console.log(index, row)
let name = state.editTaskPopUpComponentName;
pageState.ShowDialogData[name] = {};
pageState.ShowDialogData[name].isShow = true;
pageState.ShowDialogData[name].rowData = row;
}
const handleDelete = async (index, row) => {
const rsp = await request.post('/sendtasks/delete', { id: row.id });
if (rsp.status == 200) {
state.tableData.splice(index, 1);
}
}
const handleViewAPI = (index, row) => {
console.log(index, row)
let name = state.viewTaskAPIPopUpComponentName;
pageState.ShowDialogData[name] = {};
pageState.ShowDialogData[name].isShow = true;
pageState.ShowDialogData[name].rowData = row;
}
const handPageChange = async (pageNum) => {
state.currPage = pageNum;
await queryListData(pageNum, state.pageSize);
}
const rowStyle = () => {
return {
'font-size': '13px',
}
}
const handleViewLogs = (index, row) => {
router.push('/sendlogs?name=' + row.name, { replace: true });
}
const filterFunc = async () => {
await queryListData(state.currPage, state.pageSize, state.search, state.optionValue);
}
const clickAdd = () => {
pageState.ShowDialogData[state.addTaskPopUpComponentName] = {};
pageState.ShowDialogData[state.addTaskPopUpComponentName].isShow = true;
}
const queryListData = async (page, size, name = '') => {
let params = { page: page, size: size, name: name };
const rsp = await request.get('/sendtasks/list', { params: params });
state.tableData = await rsp.data.data.lists;
state.total = await rsp.data.data.total;
}
onMounted(async () => {
await queryListData(1, state.pageSize);
});
return {
...toRefs(state), handleEdit, handleDelete, handleViewAPI, handleViewLogs,
clickAdd, rowStyle, handPageChange, filterFunc
};
}
}
</script>
<style scoped>
hr {
color: #FAFCFF;
background-color: #FAFCFF;
border-color: #FAFCFF;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
max-width: 1000px;
width: 100%;
margin-top: -10vh;
}
.pagination-block {
margin-top: 30px;
display: flex;
justify-content: flex-end;
}
.total-tip {
display: inline-block;
}
.search-box {
float: right;
}
:global(.select .el-input__inner) {
width: 80px;
}
.search-input-sendways {
/* margin-left: 40px; */
width: 150px;
display: inline-flex;
}
.op-col {
text-align: left;
}
</style>
@@ -0,0 +1,142 @@
<template>
<el-dialog v-model="isShow" width="58%" :close-on-press-escape="false" :before-close="() => { }" :show-close="false">
<template #header="">
<el-text class="mx-1">查看接入API</el-text>
<el-tooltip placement="top">
<template #content>
一个任务可能关联多个不同渠道的实例
<br />
实例的内容类型大体上可以可以分为texthtmlmarkdown
<br />
发送的消息会优先现在相应的类型消息进行发送如果没有将使用传的text消息进行发送
<br />
*text节点必传
</template>
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</template>
<el-tabs v-model="activeName" @tab-click="renderApiString">
<el-tab-pane label="curl" name="curl">
<pre><code class="language-shell line-numbers">{{ currCode }}</code></pre>
</el-tab-pane>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancer()" size="small">取消</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { defineComponent, onMounted, watch, reactive, toRefs, onUpdated } from 'vue';
import { _ } from 'lodash';
import { usePageState } from '@/store/page_sate.js';
import Prism from "prismjs";
import { ApiStrGenerate } from "@/util/viewApi.js";
import { QuestionFilled } from '@element-plus/icons-vue'
export default defineComponent({
components: {
QuestionFilled,
},
props: {
componentName: String,
},
setup(props) {
const pageState = usePageState();
const state = reactive({
isShow: false,
currCode: '',
activeName: 'curl',
language: "python",
});
watch(pageState.ShowDialogData, (newValue, oldValue) => {
if (newValue[props.componentName]) {
state.isShow = pageState.ShowDialogData[props.componentName].isShow;
renderApiString();
}
});
const handleCancer = () => {
if (pageState.ShowDialogData[props.componentName]) {
pageState.ShowDialogData[props.componentName].isShow = false;
}
}
const renderApiString = () => {
console.log('renderApiString', state.activeName)
let task_id = pageState.ShowDialogData[props.componentName].rowData.id;
if (state.activeName == 'curl') {
state.currCode = ApiStrGenerate.getCurlString(task_id, {});
}
setTimeout(() => {
Prism.highlightAll()
}, 100)
}
return {
...toRefs(state), handleCancer, renderApiString
};
},
});
</script>
<style scoped>
.language-javascript {
background-color: #f0f0f0;
}
/* pre {
overflow: hidden !important;
code{
display: inline-block;
padding-bottom: 20px;
position: relative;
top: 20px;
}
&::before {
content: "";
position: absolute;
background: red;
width: 10px;
height: 10px;
border-radius: 50%;
top: 10px;
left: 15px;
transform: translate(-50%);
}
&::after {
content: "";
position: absolute;
background: sandybrown;
width: 10px;
height: 10px;
border-radius: 50%;
top: 10px;
left: 30px;
transform: translate(-50%);
}
code:first-child{
&::after{
content: "";
position: absolute;
background: limegreen;
width: 10px;
height: 10px;
border-radius: 50%;
top: -24px;
left: -7px;
transform: translate(-50%);
}
}
} */
</style>
@@ -0,0 +1,141 @@
<template>
<el-dialog v-model="isShow" width="500px" :close-on-press-escape="false" :before-close="() => { }" :show-close="false">
<template #header="">
<el-text class="mx-1">新增发信渠道</el-text>
</template>
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-form label-width="100px" v-for="item in waysLabelData">
<el-tab-pane :label="item.label" :name="item.type">
<el-form-item :label="one.subLabel" v-for="one in item.inputs">
<el-input v-model="one.value" />
</el-form-item>
</el-tab-pane>
</el-form>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button @click="testMseeageDialogVisible = true" size="small">测试发信</el-button>
<el-button @click="handleCancer()" size="small">取消</el-button>
<el-button type="primary" size="small" @click="handleSubmit()">
确定添加
</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="testMseeageDialogVisible" width="50%" align-center>
<span>将发送一条测试信息将注意查收</span>
<template #footer>
<span class="dialog-footer">
<el-button size="small" @click="testMseeageDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="(testMseeageDialogVisible = false) || handleTest()">
确定发送
</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { defineComponent, onMounted, watch, reactive, toRefs } from 'vue';
import { usePageState } from '../../../store/page_sate.js';
import { request } from '../../../api/api'
import { CONSTANT } from '../../../constant'
import { _ } from 'lodash';
import { ElMessage } from 'element-plus'
export default defineComponent({
props: {
componentName: String
},
setup(props) {
const pageState = usePageState();
const state = reactive({
isShow: false,
testMseeageDialogVisible: false,
activeName: "Email",
waysLabelData: _.cloneDeep(CONSTANT.WAYS_DATA),
});
watch(pageState.ShowDialogData, (newValue, oldValue) => {
if (newValue[props.componentName]) {
state.isShow = pageState.ShowDialogData[props.componentName].isShow;
}
});
const handleCancer = () => {
if (pageState.ShowDialogData[props.componentName]) {
pageState.ShowDialogData[props.componentName].isShow = false;
}
}
const handleClick = () => {
}
const getInputData = (type) => {
for (const element of state.waysLabelData) {
if (element.type == type) {
const data = {};
for (const item of element.inputs) {
data[item.col] = item.value;
}
return data;
}
}
return {};
}
const getFinalData = () => {
const inputData = getInputData(state.activeName);
const { name, ...nameObject } = inputData;
let postData = { name: name }
const { name: _, ...auth } = inputData;
if (state.activeName == 'Email') {
auth.port = Number(auth.port)
};
postData.auth = JSON.stringify(auth);
postData.type = state.activeName;
return postData
}
const handleTest = async () => {
let postData = getFinalData();
const rsp = await request.post('/sendways/test', postData).data;
if (await rsp.data.code == 200) {
ElMessage({ message: response.data.msg, type: 'success' })
}
}
const handleSubmit = async () => {
let postData = getFinalData();
const rsp = await request.post('/sendways/add', postData).data;
if (rsp.code == 200) {
handleCancer();
}
}
return {
...toRefs(state), handleCancer, handleSubmit, handleClick, handleTest
};
},
});
</script>
<style scoped>
/* :global(.el-dialog) {
width: 500px;
} */
/* :global(.el-dialog__title) {
font-size: 14px;
}
:global(.el-dialog label) {
font-size: 13px;
} */
</style>
@@ -0,0 +1,171 @@
<template>
<el-dialog v-model="isShow" width="500px" :close-on-press-escape="false" :before-close="() => { }" :show-close="false">
<template #header="">
<el-text class="mx-1">编辑发信渠道</el-text>
</template>
<el-tabs class="demo-tabs" @tab-click="handleClick">
<el-form label-width="100px" v-for="item in waysLabelData">
<!-- <el-tab-pane :label="item.label" :name="item.type"> -->
<el-form-item :label="one.subLabel" v-for="one in item.inputs">
<el-input v-model="one.value" />
</el-form-item>
<!-- </el-tab-pane> -->
</el-form>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button @click="testMseeageDialogVisible = true" size="small">测试发信</el-button>
<el-button @click="handleCancer()" size="small">取消</el-button>
<el-button type="primary" size="small" @click="handleSubmit()">
确定编辑
</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="testMseeageDialogVisible" width="50%" align-center>
<span>将发送一条测试信息将注意查收</span>
<template #footer>
<span class="dialog-footer">
<el-button size="small" @click="testMseeageDialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="(testMseeageDialogVisible = false) || handleTest()">
确定发送
</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { defineComponent, onMounted, watch, reactive, toRefs } from 'vue';
import { usePageState } from '../../../store/page_sate.js';
import { request } from '../../../api/api'
import { CONSTANT } from '../../../constant'
import { _ } from 'lodash';
import { ElMessage } from 'element-plus'
export default defineComponent({
props: {
componentName: String
},
setup(props) {
const pageState = usePageState();
const state = reactive({
isShow: false,
testMseeageDialogVisible: false,
waysLabelData: [],
editData: {},
});
const dealDisplayData = () => {
}
// 监测父页面传过来的数据
watch(pageState.ShowDialogData, (newValue, oldValue) => {
if (newValue[props.componentName]) {
// 弹出编辑框
state.isShow = pageState.ShowDialogData[props.componentName].isShow;
// 展示编辑框
if (newValue[props.componentName].rowData) {
const row = pageState.ShowDialogData[props.componentName].rowData;
let nowData = [];
_.cloneDeep(CONSTANT.WAYS_DATA).forEach(element => {
if (element.type == row.type) {
// 填充输入框的值
state.editData = row;
element.inputs.forEach(one => {
let newRow = Object.assign(row, JSON.parse(row.auth));
if (newRow[one.col]) {
one.value = newRow[one.col];
};
});
nowData.push(element);
};
});
state.waysLabelData = nowData;
}
};
});
const handleCancer = () => {
if (pageState.ShowDialogData[props.componentName]) {
pageState.ShowDialogData[props.componentName].isShow = false;
}
}
const handleClick = () => {
}
const getEditData = (type) => {
for (const element of state.waysLabelData) {
// if (element.type == type) {
const data = {};
for (const item of element.inputs) {
data[item.col] = item.value;
}
return data;
// }
}
return {};
}
const getFinalData = () => {
const editData = getEditData(state.waysLabelData);
const { name, ...nameObject } = editData;
let postData = { name: name }
const { name: _, ...auth } = editData;
if (state.editData.type == 'Email') {
auth.port = Number(auth.port)
};
postData.auth = JSON.stringify(auth);
postData.type = state.editData.type;
postData.id = state.editData.id;
return postData
}
const handleTest = async () => {
let postData = getFinalData();
const rsp = await request.post('/sendways/test', postData);
if (await rsp.data.code == 200) {
ElMessage({ message: response.data.msg, type: 'success' })
}
}
const handleSubmit = async () => {
let postData = getFinalData();
const rsp = await request.post('/sendways/edit', postData);
// console.log('edit res',rsp)
if (await rsp.data.code == 200) {
handleCancer();
}
}
return {
...toRefs(state), handleCancer, handleSubmit, handleClick, handleTest
};
},
});
</script>
<style scoped>
/* :global(.el-dialog) {
width: 500px;
} */
/* :global(.el-dialog__title) {
font-size: 14px;
} */
/* :global(.el-dialog label) {
font-size: 13px;
} */
</style>
@@ -0,0 +1,218 @@
<template>
<div class="main-center-body">
<div class="container">
<el-select v-model="optionValue" class="select" placeholder="渠道筛选" size="small" @change="filterFunc()">
<el-option v-for="item in options" :label="item.label" :value="item.value" />
</el-select>
<div class="search-input-sendways">
<el-input v-model="search" size="small" placeholder="搜索" @change="filterFunc()" />
</div>
<!-- <td class="line">
<div />
</td> -->
<div class="search-box">
<el-button size="small" type="primary" @click="clickAdd">新增渠道</el-button>
</div>
<hr />
<div ref="refContainer">
<el-table :data="tableData" empty-text="发信渠道为空" :row-style="rowStyle()">
<el-table-column label="ID" width="320px">
<template #default="scope">
{{ scope.row.id }}
<el-icon>
<CopyDocument @click="copyToClipboard(scope.row.id)" />
</el-icon>
</template>
</el-table-column>
<el-table-column label="渠道名" prop="name" />
<el-table-column label="发信渠道" prop="type" />
<el-table-column label="创建时间" prop="created_on" />
<el-table-column fixed="right" label="操作" width="100px">
<template #default="scope">
<!-- <el-button link size="small" type="primary" @click="handleView(scope.$index, scope.row)">查看</el-button> -->
<el-button link size="small" type="primary" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<tableDeleteButton @customHandleDelete="handleDelete(scope.$index, scope.row)" />
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-block">
<el-pagination layout="prev, pager, next" :total="total" :page-size="pageSize" @current-change="handPageChange" />
<el-text class="total-tip" size="small">{{ total }}</el-text>
</div>
<addWayComponent :componentName="addWayComponentName" />
<editWayComponent :componentName="editWayComponentName" />
</div>
</div>
</template>
<script >
import { reactive, toRefs, onMounted } from 'vue'
import addWayComponent from './addWayPopUp.vue'
import editWayComponent from './editWayPopUp.vue'
import { usePageState } from '../../../store/page_sate.js';
import { request } from '../../../api/api'
import { CopyDocument } from '@element-plus/icons-vue'
import { copyToClipboard } from '../../../util/clipboard.js';
import tableDeleteButton from '@/views/common/tableDeleteButton.vue'
import { CONSTANT } from '@/constant'
export default {
components: {
addWayComponent,
editWayComponent,
CopyDocument,
tableDeleteButton,
},
setup() {
const pageState = usePageState();
const state = reactive({
addWayComponentName: 'addWayComponent',
editWayComponentName: 'editWayComponent',
search: '',
optionValue: '',
dialogFormVisible: false,
// confirmBtnVisible: false,
tableData: [],
total: CONSTANT.TOTAL,
pageSize: CONSTANT.PAGE_SIZE,
currPage: CONSTANT.PAGE,
displayCols: [
{ 'col': 'id', 'label': '渠道ID' },
{ 'col': 'name', 'label': '渠道名' },
{ 'col': 'type', 'label': '发信渠道' },
{ 'col': 'created_on', 'label': '创建时间' },
],
options: [
{ label: '邮箱', value: 'Email' },
{ label: '钉钉', value: 'Dtalk' }
]
});
const handleEdit = (index, row) => {
console.log(index, row)
let name = state.editWayComponentName;
pageState.ShowDialogData[name] = {};
pageState.ShowDialogData[name].isShow = true;
pageState.ShowDialogData[name].rowData = row;
}
const handleDelete = async (index, row) => {
const rsp = await request.post('/sendways/delete', { id: row.id });
if (rsp.status == 200 && await rsp.data.code == 200) {
state.tableData.splice(index, 1);
}
}
const handPageChange = async (pageNum) => {
console.log('pageNum', pageNum)
state.currPage = pageNum;
await queryListData(pageNum, state.pageSize);
}
const rowStyle = () => {
return {
'font-size': '13px',
}
}
const filterFunc = async () => {
console.log('state.optionValue', state.optionValue);
await queryListData(state.currPage, state.pageSize, state.search, state.optionValue);
}
const clickAdd = () => {
pageState.ShowDialogData[state.addWayComponentName] = {};
pageState.ShowDialogData[state.addWayComponentName].isShow = true;
console.log('clickAdd', pageState.ShowDialogData[state.addWayComponentName].isShow)
}
const queryListData = async (page, size, name = '', type = '') => {
let params = { page: page, size: size, name: name, type: type };
const rsp = await request.get('/sendways/list', { params: params });
state.tableData = await rsp.data.data.lists;
state.total = await rsp.data.data.total;
}
onMounted(async () => {
await queryListData(1, state.pageSize);
});
return {
...toRefs(state), handleEdit, handleDelete,
clickAdd, rowStyle, handPageChange, filterFunc, copyToClipboard
};
}
}
</script>
<style scoped>
hr {
color: #FAFCFF;
background-color: #FAFCFF;
border-color: #FAFCFF;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
max-width: 1000px;
width: 100%;
margin-top: -10vh;
}
.el-table th,
.el-table .cell {
font-size: 10px;
/* 调整字体大小 */
}
/* .search-input {
margin-right: 0px;
margin-left: 0px;
padding-left: 0px;
} */
.el-dialog__title {
font-size: 14px !important;
}
.pagination-block {
margin-top: 30px;
display: flex;
justify-content: flex-end;
}
.total-tip {
display: inline-block;
}
.search-box {
float: right;
}
:global(.select .el-input__inner) {
width: 80px;
}
.search-input-sendways {
margin-left: 40px;
width: 150px;
display: inline-flex;
}
.op-col {
text-align: left;
}
</style>
@@ -0,0 +1,92 @@
<template>
<div class="main-center-body">
<div class="container">
<div class="setting-main" style="display: flex;">
<div style="width: 120px;">
<el-menu :default-active="currTab" mode="vertical" width="100px" @select="handleSelect">
<el-menu-item index="resetPasswd">
重置密码
</el-menu-item>
<el-menu-item index="logCleanSetting">
日志清理
</el-menu-item>
<el-menu-item index="siteCustomSetting">
站点设置
</el-menu-item>
<el-menu-item index="aboutSetting">
站点关于
</el-menu-item>
</el-menu>
</div>
<div class="setting-right">
<resetPasswd v-if="currTab == 'resetPasswd'" />
<setLogClean v-if="currTab == 'logCleanSetting'" />
<siteCustom v-if="currTab == 'siteCustomSetting'" />
<aboutSetting v-if="currTab == 'aboutSetting'" />
</div>
</div>
</div>
</div>
</template>
<script >
import { reactive, toRefs, onMounted } from 'vue'
import { copyToClipboard } from '@/util/clipboard.js';
import resetPasswd from './view/resetPasswd.vue'
import setLogClean from './view/setLogClean.vue'
import siteCustom from './view/siteCustom.vue'
import aboutSetting from './view/about.vue'
export default {
components: {
resetPasswd,
setLogClean,
siteCustom,
aboutSetting,
},
setup() {
const state = reactive({
currTab: 'resetPasswd',
});
const handleSelect = (index) => {
state.currTab = index;
}
onMounted(async () => {
});
return {
...toRefs(state), copyToClipboard, handleSelect
};
}
}
</script>
<style scoped>
hr {
color: #FAFCFF;
background-color: #FAFCFF;
border-color: #FAFCFF;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
max-width: 800px;
width: 100%;
margin-top: -10vh;
}
/* .setting-main {
} */
.setting-right {
width: 100%;
}
</style>
@@ -0,0 +1,76 @@
<template>
<div class="setting-container">
<el-text size="small">当前版本v1.0.0</el-text>
<div class="buttom">
<div class="tips">
<el-text size="small">版本功能说明</el-text>
<el-tooltip placement="top">
<template #content>
logo请输入svg文本替换后登录页面ico导航栏logo将全部一起更换
<br />
*将在下一次登录的时候生效
</template>
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
</div>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue';
import { ElMessage } from 'element-plus'
import { request } from '@/api/api'
import { QuestionFilled } from '@element-plus/icons-vue'
export default defineComponent({
components: {
QuestionFilled,
},
props: {
},
methods: {
},
setup() {
const state = reactive({
oldPasswd: '',
newPasswd: '',
});
const handleChange = async () => {
let postData = { old_passwd: state.oldPasswd, new_passwd: state.newPasswd };
const rsp = await request.post('/settings/setpasswd', postData);
if (await rsp.data.code == 200) {
let msg = await rsp.data.msg;
ElMessage({ message: msg, type: 'success' })
}
}
return {
...toRefs(state), handleChange
}
}
});
</script>
<style scoped>
:deep(.el-input .el-input__wrapper) {
margin-top: 10px;
}
:deep(.el-button) {
float: right !important;
margin-top: 10px;
}
.setting-container {
width: 200px;
margin: 50px auto;
}
.buttom {
margin-top: 30px;
}
</style>
@@ -0,0 +1,60 @@
<template>
<div class="setting-container">
<el-input v-model="oldPasswd" size="small" placeholder="请输入旧密码" type="password" show-password>
<template #prepend>旧密码</template>
</el-input>
<el-input v-model="newPasswd" size="small" placeholder="请输入新密码" type="password" show-password
style=" margin-top: 15px;">
<template #prepend>新密码</template>
</el-input>
<el-button @click="handleChange" size="small" type="primary">确定</el-button>
</div>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue';
import { ElMessage } from 'element-plus'
import { request } from '@/api/api'
export default defineComponent({
props: {
},
methods: {
},
setup() {
const state = reactive({
oldPasswd: '',
newPasswd: '',
});
const handleChange = async () => {
let postData = { old_passwd: state.oldPasswd, new_passwd: state.newPasswd };
const rsp = await request.post('/settings/setpasswd', postData);
if (await rsp.data.code == 200) {
let msg = await rsp.data.msg;
ElMessage({ message: msg, type: 'success' })
}
}
return {
...toRefs(state), handleChange
}
}
});
</script>
<style scoped>
/* :deep(.el-input .el-input__wrapper) {
margin-top: 10px;
} */
:deep(.el-button) {
float: right !important;
margin-top: 10px;
}
.setting-container {
width: 300px;
margin: 50px auto;
}
</style>
@@ -0,0 +1,97 @@
<template>
<div class="setting-container">
<div>
<el-input v-model="cron" size="small" placeholder="请输入定时日志清除的Cron表达式">
<template size="small" #prepend>cron://</template>
</el-input>
<el-input v-model.number="keepNum" size="small" placeholder="请输入要保留的最近的日志条数" style="margin-top: 15px;">
<template size="small" #prepend>保留数</template>
</el-input>
</div>
<div class="buttom">
<div class="tips">
<el-text size="small">说明</el-text>
<el-tooltip placement="top">
<template #content>
cron如果不设置默认是在每天的0点1分进行清理
<br />
保留数目如果不设置默认保留最近1000条
</template>
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
<el-button @click="handleView" size="small">查看日志</el-button>
<el-button @click="handleSubmit" size="small" type="primary">确定</el-button>
</div>
</div>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue';
import { ElMessage } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router';
import { request } from '@/api/api'
import { CONSTANT } from '@/constant'
export default defineComponent({
components: {
QuestionFilled,
},
props: {
},
methods: {
},
setup() {
const router = useRouter();
const state = reactive({
cron: '',
keepNum: 1000,
});
const handleSubmit = async () => {
let postData = { old_passwd: state.oldPasswd, new_passwd: state.newPasswd };
const rsp = await request.post('/settings/setpasswd', postData);
if (await rsp.data.code == 200) {
let msg = await rsp.data.msg;
ElMessage({ message: msg, type: 'success' })
}
}
const handleView = async () => {
router.push('/sendlogs?taskid=' + CONSTANT.LOG_TASK_ID, { replace: true });
}
return {
...toRefs(state), handleSubmit, handleView
}
}
});
</script>
<style scoped>
/* :deep(.el-input .el-input__wrapper) {
margin-top: 10px;
} */
:deep(.el-button) {
float: right !important;
margin-top: 10px;
}
.buttom {
display: flex;
width: 300px;
}
.tips {
width: 300px;
margin-top: 10px;
}
.setting-container {
width: 300px;
margin: 40px auto;
}
</style>
@@ -0,0 +1,99 @@
<template>
<div class="setting-container">
<div>
<el-input v-model="title" size="small" placeholder="请输入自定义的网站标题">
<template size="small" #prepend>站点标题</template>
</el-input>
<el-input v-model.number="slogan" size="small" placeholder="请输入自定义的网站slogan" style="margin-top: 15px;">
<template size="small" #prepend>站点标语</template>
</el-input>
<el-input v-model.number="logo" size="small" placeholder="请输入自定义的网站logosvg文本)" style="margin-top: 15px;">
<template size="small" #prepend>站点图标</template>
</el-input>
</div>
<div class="buttom">
<div class="tips">
<el-text size="small">说明</el-text>
<el-tooltip placement="top">
<template #content>
1. logo请输入svg文本替换后登录页面ico导航栏logo将全部一起更换
<br />
2. slogan将在登录页面展示
<br />
*将在下一次登录的时候生效
</template>
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
<el-button @click="handleView" size="small">查看日志</el-button>
<el-button @click="handleSubmit" size="small" type="primary">确定</el-button>
</div>
</div>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue';
import { ElMessage } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router';
import { request } from '@/api/api'
import { CONSTANT } from '@/constant'
export default defineComponent({
components: {
QuestionFilled,
},
props: {
},
methods: {
},
setup() {
const router = useRouter();
const state = reactive({
title: '',
slogan: '',
logo: '',
});
const handleSubmit = async () => {
let postData = { old_passwd: state.oldPasswd, new_passwd: state.newPasswd };
const rsp = await request.post('/settings/setpasswd', postData);
if (await rsp.data.code == 200) {
let msg = await rsp.data.msg;
ElMessage({ message: msg, type: 'success' })
}
}
const handleView = async () => {
router.push('/sendlogs?taskid=' + CONSTANT.LOG_TASK_ID, { replace: true });
}
return {
...toRefs(state), handleSubmit, handleView
}
}
});
</script>
<style scoped>
:deep(.el-button) {
float: right !important;
margin-top: 10px;
}
.buttom {
display: flex;
width: 300px;
}
.tips {
width: 300px;
margin-top: 10px;
}
.setting-container {
width: 300px;
margin: 40px auto;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { prismjsPlugin } from 'vite-plugin-prismjs'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
prismjsPlugin({
languages: 'all', // 语言
plugins: ['line-numbers', 'show-language', 'copy-to-clipboard', 'inline-color'],
theme: 'default',// 主题
css: true,
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})