Session和Cookie

session与cookie属于一种会话控制技术,常用在身份识别,登录验证,数据传输等。

cookie,是在本地计算机保存一些用户操作的历史信息(当然包括登录信息),并在用户再次访问该站点时浏览器通过 HTTP 协议将本地 cookie 内容发送给服务器,从而完成验证,或继续上一步操作。

依赖注入

go.uber.org/dig

依赖注入,抽象接口,依赖于抽象接口,而不依赖于具体的对象,实现接口的相互依赖。(可解决同级包的循环引用问题)

引用包:go.uber.org/dig

工具类方法汇总

循环定时功能

业务场景:如某个提醒功能,可以设定重复定时,设定周一某个时间点循环提醒。

重复定时截图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 计算出 weekdays 里距离 compareTime 最近的一个未来时间点
// weekdays: [1,2,3,4,5,6,7]周一、周二...周日, houMinute: 时分,compareTime:对比时间点,一般设置为time.Now()
func LatestTimeByWeekday(weekdays []int, hourMinute string, compareTime time.Time) (time.Time, error) {
    if len(weekdays) <= 0 {
        return time.Time{}, errors.New("weekdays参数不能为空")
    }

    // 对比时间点为周几,默认周日设置为7
    weekday := int(compareTime.Weekday())
    if weekday == 0 {
        weekday = 7
    }

    sort.Slice(weekdays, func(i, j int) bool {
        return weekdays[i] < weekdays[j]
    })

    getTime := func(addDay int) (time.Time, error) {
        date := compareTime.AddDate(0, 0, addDay).Format("2006-01-02 ") + hourMinute + ":00"
        return time.ParseInLocation("2006-01-02 15:04:05", date, time.Local)
    }

    var tmpTimeArr []time.Time
    for _, v := range weekdays {
        if weekday == v {
            // 检查当天时间点符不符合条件
            tmpTime, err := getTime(0)
            if err != nil {
                return time.Time{}, err
            }

            // 当天时间点不符合条件,直接取下一周的时间
            if tmpTime.Before(compareTime) || tmpTime.Equal(compareTime) {
                tmpTime, err := getTime(7)
                if err != nil {
                    return time.Time{}, err
                }
                tmpTimeArr = append(tmpTimeArr, tmpTime)
            } else {
                return tmpTime, nil
            }
        } else if weekday < v {
            tmpTime, err := getTime(v - weekday)
            if err != nil {
                return time.Time{}, err
            }
            return tmpTime, nil
        } else {
            tmpTime, err := getTime(7 - weekday + v)
            if err != nil {
                return time.Time{}, err
            }
            tmpTimeArr = append(tmpTimeArr, tmpTime)
        }
    }

    if len(tmpTimeArr) > 0 {
        return tmpTimeArr[0], nil
    }

    return time.Time{}, errors.New("异常错误")
}

Go 性能分析工具

一、PProf 简介

pprof 是用于可视化和分析性能分析数据的工具。

Golang是一个非常注重性能的语言,因此语言的内置库里就自带了性能分析库pprof。

性能分析和采集在计算机性能调试领域使用的术语是profile,或者有时候也会使用profiling代表性能分析这个行为。所以pprof名字里的prof来源于对单词profile的缩写,profile这个单词的原意是画像,那么针对程序性能的画像就是应用使用 CPU 和内存等等这些资源的情况。

简单的excel导入导出

excel导入导出代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package tool

import (
    "errors"
    "github.com/360EntSecGroup-Skylar/excelize/v2"
    "mime/multipart"
    "strconv"
)

const (
    maxCharCount     = 26       // maxCharCount 最多26个字符A-Z
    defaultSheetName = "Sheet1" // 默认Sheet名称
)

type Excel struct {
    SheetName string          // sheetName,默认为Sheet1
    Headers   []string        // 表头
    Rows      [][]interface{} // 表数据
}

func (m *Excel) SetDefaultSheetName() {
    if m.SheetName == "" {
        m.SheetName = defaultSheetName
    }
}

// 从文件流读取 Excel Sheet1 的所有数据
func (Excel) ReadExcelFromStream(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    ff, err := excelize.OpenReader(src)
    if err != nil {
        if err.Error() == "zip: not a valid zip file" {
            return rows, errors.New("无效文件,注意不支持加密的EXCEL文件")
        }
        return rows, errors.New(err.Error())
    }

    // 获取 Sheet1 上所有单元格
    rows, err = ff.GetRows(ff.GetSheetName(0))
    if err != nil {
        return rows, errors.New(err.Error())
    }

    return rows, nil
}

// 导出excel到浏览器
func (m Excel) ExportToWeb() ([]byte, error) {
    f, err := m.exportExcel()
    if err != nil {
        return nil, err
    }
    buffer, err := f.WriteToBuffer()
    if err != nil {
        return nil, err
    }
    return buffer.Bytes(), nil
}

// 导出excel到本地服务器
// path 文件导出路径,建议使用绝对路径
// filename 文件名,需带上后缀
func (m Excel) ExportToPath(path, filename string) (string, error) {
    var filePath string
    f, err := m.exportExcel()
    if err != nil {
        return filePath, err
    }
    filePath = path + "/" + filename
    if err := f.SaveAs(filePath); err != nil {
        return filePath, err
    }
    return filePath, nil
}

// exportExcel 导出Excel文件
// sheetName 工作表名称
// headers 列名切片, 表头
// rows 数据切片,是一个二维数组
func (m Excel) exportExcel() (*excelize.File, error) {
    m.SetDefaultSheetName()

    f := excelize.NewFile()
    sheetIndex := f.NewSheet(m.SheetName)
    maxColumnRowNameLen := 1 + len(strconv.Itoa(len(m.Rows)))
    columnCount := len(m.Headers)
    if columnCount > maxCharCount {
        maxColumnRowNameLen++
    } else if columnCount > maxCharCount*maxCharCount {
        maxColumnRowNameLen += 2
    }
    columnNames := make([][]byte, 0, columnCount)
    for i, header := range m.Headers {
        columnName := m.getColumnName(i, maxColumnRowNameLen)
        columnNames = append(columnNames, columnName)
        // 初始化excel表头,这里的index从1开始要注意
        curColumnName := m.getColumnRowName(columnName, 1)
        err := f.SetCellValue(m.SheetName, curColumnName, header)
        if err != nil {
            return nil, err
        }
    }
    for rowIndex, row := range m.Rows {
        for columnIndex, columnName := range columnNames {
            // 从第二行开始
            err := f.SetCellValue(m.SheetName, m.getColumnRowName(columnName, rowIndex+2), row[columnIndex])
            if err != nil {
                return nil, err
            }
        }
    }
    f.SetActiveSheet(sheetIndex)
    return f, nil
}

// getColumnName 生成列名
// Excel的列名规则是从A-Z往后排;超过Z以后用两个字母表示,比如AA,AB,AC;两个字母不够以后用三个字母表示,比如AAA,AAB,AAC
// 这里做数字到列名的映射:0 -> A, 1 -> B, 2 -> C
// maxColumnRowNameLen 表示名称框的最大长度,假设数据是10行,1000列,则最后一个名称框是J1000(如果有表头,则是J1001),是4位
// 这里根据 maxColumnRowNameLen 生成切片,后面生成名称框的时候可以复用这个切片,而无需扩容
func (m Excel) getColumnName(column, maxColumnRowNameLen int) []byte {
    const A = 'A'
    if column < maxCharCount {
        // 第一次就分配好切片的容量
        slice := make([]byte, 0, maxColumnRowNameLen)
        return append(slice, byte(A+column))
    } else {
        // 递归生成类似AA,AB,AAA,AAB这种形式的列名
        return append(m.getColumnName(column/maxCharCount-1, maxColumnRowNameLen), byte(A+column%maxCharCount))
    }
}

// getColumnRowName 生成名称框
// Excel的名称框是用A1,A2,B1,B2来表示的,这里需要传入前一步生成的列名切片,然后直接加上行索引来生成名称框,就无需每次分配内存
func (Excel) getColumnRowName(columnName []byte, rowIndex int) (columnRowName string) {
    l := len(columnName)
    columnName = strconv.AppendInt(columnName, int64(rowIndex), 10)
    columnRowName = string(columnName)
    // 将列名恢复回去
    columnName = columnName[:l]
    return
}

WEB端调用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package controller

import (
    "bytes"
    "github.com/labstack/echo/v4"
    "xxx/lib/tool"
    "net/http"
)

type xxxController struct{}

var XxxController = new(xxxController)

func init() {
    Router.GET("/export", AuthCorpController.Export).Name = "导出excel"
}

func (xxxController) Export(c echo.Context) error {
    var (
        excelHeaders = []string{"姓名", "性别", "年龄"}
        excelRows    = [][]interface{}{
            {"张三", "女", 18},
            {"李四", "男", 19},
        }
    )
    b, _ := tool.Excel{Headers: excelHeaders, Rows: excelRows}.ExportToWeb()
    c.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename=students.xlsx")
    return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(b))
}

csv导入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package tool

import (
    "bufio"
    "bytes"
    "encoding/csv"
    "errors"
    "golang.org/x/text/encoding/simplifiedchinese"
    "golang.org/x/text/transform"
    "io"
    "mime/multipart"
    "strconv"
    "strings"
)

type Csv struct{}

// 从文件流读取 csv 的所有数据
func (m Csv) ReadCsvFromStream(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    // 初始化一个 csv reader,并通过这个 reader 从 csv 文件读取数据
    reader := csv.NewReader(transform.NewReader(bufio.NewReader(src), simplifiedchinese.GBK.NewDecoder()))

    // 设置返回记录中每行数据期望的字段数,-1 表示返回所有字段
    reader.FieldsPerRecord = -1
    // 通过 readAll 方法返回 csv 文件中的所有内容
    rows, err = reader.ReadAll()
    if err != nil {
        return rows, err
    }

    return rows, nil
}

// 从文件流读取不规范 csv 的所有数据
func (m Csv) ReadIllegalCsvFromSteam(f *multipart.FileHeader) ([][]string, error) {
    var rows [][]string

    src, err := f.Open()
    if err != nil {
        return rows, err
    }
    defer src.Close()

    // 兆字节转为字节进行比较大小
    maxFileSize := int64(2) // 默认最大上传 2 M
    if f.Size > maxFileSize*1e6 {
        return rows, errors.New("文件大小不能超过 " + strconv.FormatInt(maxFileSize, 10) + " M")
    }

    // 直接复制文件内容
    var buf bytes.Buffer
    if _, err = io.Copy(&buf, transform.NewReader(bufio.NewReader(src), simplifiedchinese.GBK.NewDecoder())); err != nil {
        return rows, err
    }
    contents := buf.String()
    slice := strings.Split(contents, "\n")

    for _, v := range slice {
        records := strings.Split(strings.ReplaceAll(v, "=\"", "\""), ",") // 去掉不合法的等号
        for kk, vv := range records {
            records[kk] = strings.Trim(vv, "\"") // 去除前后的双引号
        }
        rows = append(rows, records)
    }
    return rows, nil
}

线上事故记录

一、数据库宕机

  1. 事故时间:2020-08-25

  2. 事故描述:

    1. 大量慢查询
    2. 阿里云数据库RDS,一主三从一备胎主库(共 5 个数据库),主从读写分离失效,大量请求涌入主库,主库承受不住请求压力,主库宕机,备胎主库也失效。
    3. 主从延迟严重,从库失效,全部查询堆在主库,这样也会导致主库宕机。(比如删除大量数据、更新大量数据的时候)
  3. 事故错误日志:

多系统对接-签名校验案例(GO)

接口通讯规范

  • 签名规则:

    1
    2
    3
    4
    5
    6
    7
    8
    
    // 签名有效期暂定为 600 秒
    var platform = "baidu"     // 对接平台,正式测试环境保持一致
    var secret = "test_123456"   // 测试环境密钥,生产环境密钥私聊
    var requestTime = 1640163102 // 秒级时间戳
    var loginStaffId = 123 // 请求员工ID,便于权限控制
    
    // 生成签名
    sign := Md5(fmt.Sprintf("%d-%s-%s-%d", requestTime, platform, secret, loginStaffId))
    
  • 数据规范:

企业微信自建应用代开发

企业微信开发汇总

关于企业微信的所有,如开源企业微信sdk

企业微信开发自建应用代开发篇

企业微信开发第三方应用开发篇

企业微信开发自建内部应用开发篇

自建应用代开发上线流程

应用代开发上线流程,只看此文就够了(上)

应用代开发上线流程,只看此文就够了(中)

应用代开发上线流程,只看此文就够了(下)

疑问

  1. 自建应用代开发,服务商怎么获取授权企业的会话存档接口?

稳定排序和不稳定排序

首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。

Git分支管理

GIT分支管理及发版合并流程

主要分支:master(线上分支), uat(承接分支), release(预发布分支), develop(开发分支), yourname_feature(功能分支), yourname_hotfix(修复分支)