#21 add tool - view log

This commit is contained in:
新亮
2021-04-04 21:14:15 +08:00
parent 84cc0c9cbc
commit 66a5e29c9c
16 changed files with 847 additions and 14 deletions

View File

@@ -79,10 +79,10 @@
const hash_id = {{ .HashID }}
$("input#request_api").maxlength({
warningClass: "badge badge-info",
limitReachedClass: "badge badge-warning"
});
$("input#request_api").maxlength({
warningClass: "badge badge-info",
limitReachedClass: "badge badge-warning"
});
// 加载列表页数据
getListData();
@@ -91,12 +91,28 @@
$.get("/api/authorized_list", {id: hash_id}, function (data) {
// 成功
if (data.list.length > 0) {
var badgeMethodClass = "";
$.each(data.list, function (index, value) {
if (value.method === "GET") {
badgeMethodClass = "badge-primary";
} else if (value.method === "POST") {
badgeMethodClass = "badge-success";
} else if (value.method === "DELETE") {
badgeMethodClass = "badge-danger";
} else if (value.method === "PUT") {
badgeMethodClass = "badge-yellow";
} else if (value.method === "PATCH") {
badgeMethodClass = "badge-cyan";
} else {
badgeMethodClass = "badge-dark";
}
const p = '<p>\n' +
'<a href="#!" data-id="' + value.hash_id + '" data-api="' + value.api + '" class="del">' +
'<span class="badge badge-secondary"><i class="mdi mdi-window-close"></i></span>\n' +
'<span class="badge badge-dark"><i class="mdi mdi-window-close"></i></span>\n' +
'</a>\n' +
'<span class="badge badge-success">' + value.method + '</span>\n' + value.api
'<span class="badge ' + badgeMethodClass + '">' + value.method + '</span>\n' + value.api
;
$(".apis").append(p);

View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
<link href="../../bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="../../bootstrap/css/materialdesignicons.min.css" rel="stylesheet">
<link href="../../bootstrap/css/style.min.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid p-t-15">
<div class="row">
<div class="col-lg-12">
<div class="card">
<header class="card-header">
<div class="card-title">如何给调用方开通 KEY 和 SECRET</div>
</header>
<div class="card-body">
<p>1. 新增调用方,输入调用方标识、调用方对接人、备注等信息;</p>
<p>2. 授权调用方可调用的接口;</p>
<p>3. 查看详情,将调用方的 <code>KEY</code><code>SECRET</code> 发给调用方;</p>
</div>
</div>
</div>
<div class="col-lg-12">
<div class="card">
<header class="card-header">
<div class="card-title">调用方如何传递 Token</div>
</header>
<div class="card-body">
<p>1. 基于 HTTP Header 中的两个参数 <code>Authorization</code><code>Date</code> 存储签名信息Authorization 存储签名信息,格式:调用方 KEY + 空格分隔符 + 摘要(加密串),例如:</p>
<pre>Authorization:blog MjJjMDE1MWFkZjMwOWFmYjFlNzViNDFjYjYwMWFlMmM=</pre>
<p>2. Date 存储时间信息格式GMT 格林尼治标准时间,使用 <code>Asia/Shanghai</code> 时区,例如;</p>
<pre>Date:Sat, 03 Apr 2021 10:14:43 GMT</pre>
</div>
</div>
</div>
<div class="col-lg-12">
<div class="card">
<header class="card-header"><div class="card-title">不同语言生成签名的方法,供参考</div></header>
<div class="card-body">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#go" aria-selected="true">Go 语言</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#php" aria-selected="false">PHP 语言</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade active show" id="go">
<pre>
func New(key, secret string, ttl time.Duration) Signature {
return &signature{
key: key,
secret: secret,
ttl: ttl,
}
}
// Generate
// path 请求的路径 (不附带 querystring)
func (s *signature) Generate(path string, method string, params url.Values) (authorization, date string, err error) {
if path == "" {
err = errors.New("path required")
return
}
if method == "" {
err = errors.New("method required")
return
}
methodName := strings.ToUpper(method)
if !methods[methodName] {
err = errors.New("method param error")
return
}
// Date
date = time_parse.GMTLayoutString()
// Encode() 方法中自带 sorted by key
sortParamsEncode := params.Encode()
// 加密字符串规则
buffer := bytes.NewBuffer(nil)
buffer.WriteString(path)
buffer.WriteString(delimiter)
buffer.WriteString(methodName)
buffer.WriteString(delimiter)
buffer.WriteString(sortParamsEncode)
buffer.WriteString(delimiter)
buffer.WriteString(date)
// 对数据进行 hmac 加密,并进行 base64 encode
hash := hmac.New(sha256.New, []byte(s.secret))
hash.Write(buffer.Bytes())
digest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
authorization = fmt.Sprintf("%s %s", s.key, digest)
return
}
// 模拟数据
const (
key = "blog"
secret = "i1ydX9RtHyuJTrw7frcu"
ttl = time.Minute * 10
)
func TestSignature_Generate(t *testing.T) {
path := "/echo"
method := "POST"
params := url.Values{}
params.Add("a", "a1")
params.Add("d", "d1")
params.Add("c", "c1")
authorization, date, err := New(key, secret, ttl).Generate(path, method, params)
t.Log("authorization:", authorization)
t.Log("date:", date)
t.Log("err:", err)
}
</pre>
</div>
<div class="tab-pane fade" id="php">
<pre>
// 模拟数据
$key = "blog";
$secret = "i1ydX9RtHyuJTrw7frcu";
$path = "/echo";
$method = "POST";
$params['a'] = "a1";
$params['d'] = "d1";
$params['c'] = "c1";
// 对 params key 进行排序
ksort($params);
// 对 sortParams 进行 Encode
$sortParamsEncode = http_build_query($params);
// GMT 格林尼治标准时间,使用 Asia/Shanghai 时区
$date = gmdate('D, d M Y H:i:s T',time() + 8*3600);
// 加密字符串规则
$encryptStr = $path."|".strtoupper($method)."|".$sortParamsEncode."|".$date;
// 对数据进行 sha256 加密,并进行 base64 encode
$digest = base64_encode(hash_hmac("sha256", $encryptStr, $secret, true));
$authorization = $key." ".$digest;
echo "authorization:{$authorization}";
echo "---";
echo "date:{$date}";
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="../../bootstrap/js/jquery.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/popper.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/main.min.js"></script>
</body>
</html>

View File

@@ -48,6 +48,15 @@
<a href="javascript:void(0)"><i class="mdi mdi-playlist-check"></i> <span>授权调用方</span></a>
<ul class="nav nav-subnav">
<li> <a class="multitabs" href="/authorized/list">调用方</a> </li>
<li> <a class="multitabs" href="/authorized/demo">使用说明</a> </li>
</ul>
</li>
<li class="nav-item nav-item-has-subnav">
<a href="javascript:void(0)"><i class="mdi mdi-tools"></i> <span>工具箱</span></a>
<ul class="nav nav-subnav">
<li> <a class="multitabs" href="/tool/hashids">Hashids</a> </li>
<li> <a class="multitabs" href="/tool/logs">调用日志</a> </li>
</ul>
</li>
@@ -55,13 +64,6 @@
<li class="nav-item"> <a href="/graphql" target="_blank" ><i class="mdi mdi-file-document-box-search"></i> <span>GraphQL</span></a> </li>
<li class="nav-item"> <a href="/metrics" target="_blank" ><i class="mdi mdi-speedometer"></i> <span>接口指标</span></a> </li>
<li class="nav-item nav-item-has-subnav">
<a href="javascript:void(0)"><i class="mdi mdi-tools"></i> <span>工具箱</span></a>
<ul class="nav nav-subnav">
<li> <a class="multitabs" href="/tool/hashids">hashids</a> </li>
</ul>
</li>
</ul>
</nav>
</div>

View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
<link href="../../bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="../../bootstrap/css/materialdesignicons.min.css" rel="stylesheet">
<link href="../../bootstrap/css/style.min.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid p-t-15">
<div class="alert alert-info alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
<p>推荐使用: <code>ELK 组件</code> ,本功能仅仅是读取文本进行展示。</p>
</div>
<div class="card">
<header class="card-header">
<div class="card-title">日志列表 <code>仅展示最新的 100 条日志。</code></div>
<ul class="card-actions">
<li>
<a href="#!" onclick="location.reload();" data-toggle="tooltip" title="" data-original-title="刷新"><i class="mdi mdi-refresh"></i></a>
</li>
</ul>
</header>
<div class="card-body">
<div class="accordion">
{{range $key, $value := .Logs}}
{{$badgeLevelClass := ""}}
{{$badgeMethodClass := ""}}
{{$badgeCodeClass := ""}}
<div class="card">
<div class="card-header">
<div class="card-title">
{{if eq $value.Level "info"}}
{{$badgeLevelClass = "badge-info"}}
{{else if eq $value.Level "error"}}
{{$badgeLevelClass = "badge-danger"}}
{{else if eq $value.Level "warn"}}
{{$badgeLevelClass = "badge-warning"}}
{{else}}
{{$badgeLevelClass = "badge-dark"}}
{{end}}
{{if eq $value.Method "GET"}}
{{$badgeMethodClass = "badge-primary"}}
{{else if eq $value.Method "POST"}}
{{$badgeMethodClass = "badge-success"}}
{{else if eq $value.Method "DELETE"}}
{{$badgeMethodClass = "badge-danger"}}
{{else if eq $value.Method "PUT"}}
{{$badgeMethodClass = "badge-yellow"}}
{{else if eq $value.Method "PATCH"}}
{{$badgeMethodClass = "badge-cyan"}}
{{else}}
{{$badgeMethodClass = "badge-dark"}}
{{end}}
{{if eq $value.HTTPCode 200}}
{{$badgeCodeClass = "badge-success"}}
{{else}}
{{$badgeCodeClass = "badge-dark"}}
{{end}}
{{if eq $value.HTTPCode 0}}
<a data-toggle="collapse" data-target="#collapse{{$key}}" aria-expanded="false" href="#!" class="collapsed">
<span class="badge {{$badgeLevelClass}}">{{$value.Level}}</span>
<span class="badge badge-brown">{{$value.Time}}</span>
<code>{{$value.Msg}}</code>
</a>
{{else}}
<a data-toggle="collapse" data-target="#collapse{{$key}}" aria-expanded="false" href="#!" class="collapsed">
<span class="badge {{$badgeLevelClass}}">{{$value.Level}}</span>
<span class="badge badge-brown">{{$value.Time}}</span>
<span class="badge {{$badgeMethodClass}}">{{$value.Method}}</span>
<span class="badge {{$badgeCodeClass}}">{{$value.HTTPCode}}</span>
<span class="badge badge-muted">{{$value.TraceID}}</span>
<code>{{$value.Path}}</code>
</a>
{{end}}
</div>
</div>
<div id="collapse{{$key}}" class="collapse">
<div class="card-body">
<pre>{{$value.Content}}</pre>
</div>
</div>
</div>
{{end}}
</div>
</div>
</div>
</div>
<script type="text/javascript" src="../../bootstrap/js/jquery.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/popper.min.js"></script>
<script type="text/javascript" src="../../bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

View File

@@ -43,9 +43,11 @@ func setWebRouter(r *resource) {
web.GET("/authorized/list", authorizedHandler.ListView())
web.GET("/authorized/add", authorizedHandler.AddView())
web.GET("/authorized/api/:id", authorizedHandler.ApiView())
web.GET("/authorized/demo", authorizedHandler.DemoView())
// 工具箱
web.GET("/tool/hashids", toolHandler.HashIdsView())
web.GET("/tool/logs", toolHandler.LogsView())
}
}

View File

@@ -0,0 +1,9 @@
package authorized_handler
import "github.com/xinliangnote/go-gin-api/internal/pkg/core"
func (h *handler) DemoView() core.HandlerFunc {
return func(c core.Context) {
c.HTML("authorized_demo", nil)
}
}

View File

@@ -16,6 +16,7 @@ type Handler interface {
AddView() core.HandlerFunc
ApiView() core.HandlerFunc
ListView() core.HandlerFunc
DemoView() core.HandlerFunc
}
type handler struct {

View File

@@ -0,0 +1,78 @@
package tool_handler
import (
"encoding/json"
"github.com/xinliangnote/go-gin-api/configs"
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
"github.com/xinliangnote/go-gin-api/pkg/file"
"go.uber.org/zap"
)
type logsViewResponse struct {
Logs []logData `json:"logs"`
}
type logData struct {
Level string `json:"level"`
Time string `json:"time"`
Path string `json:"path"`
HTTPCode int `json:"http_code"`
Method string `json:"method"`
Msg string `json:"msg"`
TraceID string `json:"trace_id"`
Content string `json:"content"`
}
func (h *handler) LogsView() core.HandlerFunc {
type logParseData struct {
Level string `json:"level"`
Time string `json:"time"`
Caller string `json:"caller"`
Msg string `json:"msg"`
Domain string `json:"domain"`
Method string `json:"method"`
Path string `json:"path"`
HTTPCode int `json:"http_code"`
BusinessCode int `json:"business_code"`
Success bool `json:"success"`
CostSeconds float64 `json:"cost_seconds"`
TraceID string `json:"trace_id"`
}
return func(c core.Context) {
readLineFromEnd, err := file.NewReadLineFromEnd(configs.ProjectLogFile())
if err != nil {
h.logger.Error("NewReadLineFromEnd err", zap.Error(err))
}
logSize := 100
obj := new(logsViewResponse)
obj.Logs = make([]logData, logSize)
for i := 0; i < logSize; i++ {
content, _ := readLineFromEnd.ReadLine()
var logParse logParseData
_ = json.Unmarshal(content, &logParse)
data := logData{
Content: string(content),
Level: logParse.Level,
Time: logParse.Time,
Path: logParse.Path,
Method: logParse.Method,
Msg: logParse.Msg,
HTTPCode: logParse.HTTPCode,
TraceID: logParse.TraceID,
}
if string(content) != "" {
obj.Logs[i] = data
}
}
c.HTML("tool_logs", obj)
}
}

View File

@@ -14,6 +14,7 @@ type Handler interface {
i()
HashIdsView() core.HandlerFunc
LogsView() core.HandlerFunc
}
type handler struct {

183
pkg/file/file.go Normal file
View File

@@ -0,0 +1,183 @@
package file
import (
"bytes"
"fmt"
"io"
"os"
)
var (
buffSize = 1 << 20
)
// ReadLineFromEnd --
type ReadLineFromEnd struct {
f *os.File
fileSize int
bwr *bytes.Buffer
lineBuff []byte
swapBuff []byte
isFirst bool
}
// NewReadLineFromEnd
func NewReadLineFromEnd(filename string) (rd *ReadLineFromEnd, err error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
info, err := f.Stat()
if err != nil {
return nil, err
}
if info.IsDir() {
return nil, fmt.Errorf("not file")
}
fileSize := int(info.Size())
rd = &ReadLineFromEnd{
f: f,
fileSize: fileSize,
bwr: bytes.NewBuffer([]byte{}),
lineBuff: make([]byte, 0),
swapBuff: make([]byte, buffSize),
isFirst: true,
}
return rd, nil
}
// ReadLine 结尾包含'\n'
func (c *ReadLineFromEnd) ReadLine() (line []byte, err error) {
var ok bool
for {
ok, err = c.buff()
if err != nil {
return nil, err
}
if ok {
break
}
}
line, err = c.bwr.ReadBytes('\n')
if err == io.EOF && c.fileSize > 0 {
err = nil
}
return line, err
}
// Close --
func (c *ReadLineFromEnd) Close() (err error) {
return c.f.Close()
}
func (c *ReadLineFromEnd) buff() (ok bool, err error) {
if c.fileSize == 0 {
return true, nil
}
if c.bwr.Len() >= buffSize {
return true, nil
}
offset := 0
if c.fileSize > buffSize {
offset = c.fileSize - buffSize
}
_, err = c.f.Seek(int64(offset), 0)
if err != nil {
return false, err
}
n, err := c.f.Read(c.swapBuff)
if err != nil && err != io.EOF {
return false, err
}
if c.fileSize < n {
n = c.fileSize
}
if n == 0 {
return true, nil
}
for {
m := bytes.LastIndex(c.swapBuff[:n], []byte{'\n'})
if m == -1 {
break
}
if m < n-1 {
err = c.writeLine(c.swapBuff[m+1 : n])
if err != nil {
return false, err
}
ok = true
} else if m == n-1 && !c.isFirst {
err = c.writeLine(nil)
if err != nil {
return false, err
}
ok = true
}
n = m
if n == 0 {
break
}
}
if n > 0 {
reverseBytes(c.swapBuff[:n])
c.lineBuff = append(c.lineBuff, c.swapBuff[:n]...)
}
if offset == 0 {
err = c.writeLine(nil)
if err != nil {
return false, err
}
ok = true
}
c.fileSize = offset
if c.isFirst {
c.isFirst = false
}
return ok, nil
}
func (c *ReadLineFromEnd) writeLine(b []byte) (err error) {
if len(b) > 0 {
_, err = c.bwr.Write(b)
if err != nil {
return err
}
}
if len(c.lineBuff) > 0 {
reverseBytes(c.lineBuff)
_, err = c.bwr.Write(c.lineBuff)
if err != nil {
return err
}
c.lineBuff = c.lineBuff[:0]
}
_, err = c.bwr.Write([]byte{'\n'})
if err != nil {
return err
}
return nil
}
func reverseBytes(b []byte) {
n := len(b)
if n <= 1 {
return
}
for i := 0; i < n; i++ {
k := n - 1
if k != i {
b[i], b[k] = b[k], b[i]
}
n--
}
}

View File

@@ -0,0 +1,52 @@
package signature
import (
"net/http"
"net/url"
"time"
)
var _ Signature = (*signature)(nil)
const (
delimiter = "|"
)
// 合法的 Methods
var methods = map[string]bool{
http.MethodGet: true,
http.MethodPost: true,
http.MethodHead: true,
http.MethodPut: true,
http.MethodPatch: true,
http.MethodDelete: true,
http.MethodConnect: true,
http.MethodOptions: true,
http.MethodTrace: true,
}
type Signature interface {
i()
// 生成签名
Generate(path string, method string, params url.Values) (authorization, date string, err error)
// 验证签名
Verify(authorization, date string, path string, method string, params url.Values) (ok bool, err error)
}
type signature struct {
key string
secret string
ttl time.Duration
}
func New(key, secret string, ttl time.Duration) Signature {
return &signature{
key: key,
secret: secret,
ttl: ttl,
}
}
func (s *signature) i() {}

View File

@@ -0,0 +1,61 @@
package signature
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strings"
"github.com/xinliangnote/go-gin-api/pkg/time_parse"
"github.com/pkg/errors"
)
// Generate
// path 请求的路径 (不附带 querystring)
func (s *signature) Generate(path string, method string, params url.Values) (authorization, date string, err error) {
if path == "" {
err = errors.New("path required")
return
}
if method == "" {
err = errors.New("method required")
return
}
methodName := strings.ToUpper(method)
if !methods[methodName] {
err = errors.New("method param error")
return
}
// Date
date = time_parse.GMTLayoutString()
// Encode() 方法中自带 sorted by key
sortParamsEncode := params.Encode()
// 加密字符串规则
buffer := bytes.NewBuffer(nil)
buffer.WriteString(path)
buffer.WriteString(delimiter)
buffer.WriteString(methodName)
buffer.WriteString(delimiter)
buffer.WriteString(sortParamsEncode)
buffer.WriteString(delimiter)
buffer.WriteString(date)
fmt.Println(string(buffer.Bytes()))
// 对数据进行 sha256 加密,并进行 base64 encode
hash := hmac.New(sha256.New, []byte(s.secret))
hash.Write(buffer.Bytes())
digest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
authorization = fmt.Sprintf("%s %s", s.key, digest)
return
}

View File

@@ -0,0 +1,45 @@
package signature
import (
"net/url"
"testing"
"time"
)
const (
key = "blog"
secret = "i1ydX9RtHyuJTrw7frcu"
ttl = time.Minute * 10
)
func TestSignature_Generate(t *testing.T) {
path := "/echo"
method := "POST"
params := url.Values{}
params.Add("a", "a1")
params.Add("d", "d1")
params.Add("c", "c1")
authorization, date, err := New(key, secret, ttl).Generate(path, method, params)
t.Log("authorization:", authorization)
t.Log("date:", date)
t.Log("err:", err)
}
func TestSignature_Verify(t *testing.T) {
authorization := "blog d3+XC7l0lproeO4Z/WHd2VWZsBct0dy9HUndNOxmPu0="
date := "Fri, 02 Apr 2021 20:31:28 GMT"
path := "/echo"
method := "post"
params := url.Values{}
params.Add("a", "a1")
params.Add("d", "d1")
params.Add("c", "c1")
ok, err := New(key, secret, ttl).Verify(authorization, date, path, method, params)
t.Log(ok)
t.Log(err)
}

View File

@@ -0,0 +1,70 @@
package signature
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strings"
"time"
"github.com/xinliangnote/go-gin-api/pkg/time_parse"
"github.com/pkg/errors"
)
func (s *signature) Verify(authorization, date string, path string, method string, params url.Values) (ok bool, err error) {
if date == "" {
err = errors.New("date required")
return
}
if path == "" {
err = errors.New("path required")
return
}
if method == "" {
err = errors.New("method required")
return
}
methodName := strings.ToUpper(method)
if !methods[methodName] {
err = errors.New("method param error")
return
}
ts, err := time_parse.ParseGMTInLocation(date)
if err != nil {
err = errors.New("date must follow 'Mon, 02 Jan 2006 15:04:05 GMT'")
return
}
if time_parse.SubInLocation(ts) > float64(s.ttl/time.Second) {
err = errors.Errorf("date exceeds limit %v", s.ttl)
return
}
// Encode() 方法中自带 sorted by key
sortParamsEncode := params.Encode()
buffer := bytes.NewBuffer(nil)
buffer.WriteString(path)
buffer.WriteString(delimiter)
buffer.WriteString(methodName)
buffer.WriteString(delimiter)
buffer.WriteString(sortParamsEncode)
buffer.WriteString(delimiter)
buffer.WriteString(date)
// 对数据进行 hmac 加密,并进行 base64 encode
hash := hmac.New(sha256.New, []byte(s.secret))
hash.Write(buffer.Bytes())
digest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
ok = authorization == fmt.Sprintf("%s %s", s.key, digest)
return
}

View File

@@ -1,6 +1,10 @@
package time_parse
import "time"
import (
"math"
"net/http"
"time"
)
var (
cst *time.Location
@@ -43,3 +47,19 @@ func CSTLayoutStringToUnix(cstLayoutString string) (int64, error) {
}
return stamp.Unix(), nil
}
// GMTLayoutString 格式化时间
// 返回 "Mon, 02 Jan 2006 15:04:05 GMT" 格式的时间
func GMTLayoutString() string {
return time.Now().In(cst).Format(http.TimeFormat)
}
// ParseGMTInLocation 格式化时间
func ParseGMTInLocation(date string) (time.Time, error) {
return time.ParseInLocation(http.TimeFormat, date, cst)
}
// SubInLocation 计算时间差
func SubInLocation(ts time.Time) float64 {
return math.Abs(time.Now().In(cst).Sub(ts).Seconds())
}

View File

@@ -13,3 +13,7 @@ func TestCSTLayoutString(t *testing.T) {
func TestCSTLayoutStringToUnix(t *testing.T) {
t.Log(CSTLayoutStringToUnix("2020-01-24 21:11:11"))
}
func TestGMTLayoutString(t *testing.T) {
t.Log(GMTLayoutString())
}