Browse Source

ADD:冷链运输单pdf

zoie 11 tháng trước cách đây
mục cha
commit
3698884df7

+ 869 - 1
app/admin/controller/waybill.go

@@ -6,18 +6,31 @@ import (
 	"cold-logistics/app/admin/service"
 	"cold-logistics/app/admin/service/dto"
 	"cold-logistics/common/actions"
+	"cold-logistics/common/lib"
+	"cold-logistics/common/nats/nats_server"
 	"errors"
 	"fmt"
 	"github.com/beego/beego/v2/core/logs"
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin/binding"
+	"github.com/signintech/gopdf"
+	"github.com/wcharczuk/go-chart/v2"
+	"github.com/wcharczuk/go-chart/v2/drawing"
 	"github.com/xuri/excelize/v2"
 	"gogs.baozhida.cn/zoie/OAuth-core/api"
 	"gogs.baozhida.cn/zoie/OAuth-core/pkg/jwtauth/user"
 	_ "gogs.baozhida.cn/zoie/OAuth-core/pkg/response"
+	"gonum.org/v1/plot"
+	"gonum.org/v1/plot/plotter"
+	"gonum.org/v1/plot/vg"
+	"gonum.org/v1/plot/vg/draw"
+	"image/color"
+	"math"
 	"net/url"
 	"os"
 	"path"
+	"sort"
+	"sync"
 	"time"
 )
 
@@ -192,7 +205,7 @@ func (e WaybillController) Home(c *gin.Context) {
 	e.OK(res, "查询成功")
 }
 
-// GetPage 获取运单列表
+// GetAppletPage 获取运单列表
 // @Summary 获取运单列表
 // @Description 获取运单列表
 // @Tags 运单
@@ -229,6 +242,53 @@ func (e WaybillController) GetAppletPage(c *gin.Context) {
 	e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
 }
 
+// GetAppletCount 获取app运单统计数量
+// @Summary 获取app运单统计数量
+// @Description 获取app运单统计数量
+// @Tags 运单
+// @Param no query string false "运单号"
+// @Success 200 {object} response.Response{data=response.Page{list=[]model.Waybill}} "{"code": 200, "data": [...]}"
+// @Router /api/waybill/applet-count [get]
+// @Security Bearer
+func (e WaybillController) GetAppletCount(c *gin.Context) {
+	s := service.Waybill{}
+	req := dto.WaybillGetAppletPageReq{}
+	err := e.MakeContext(c).
+		MakeOrm().
+		Bind(&req, binding.Query).
+		MakeService(&s.Service).
+		Errors
+	if err != nil {
+		e.Logger.Error(err)
+		e.Error(500, err, err.Error())
+		return
+	}
+
+	//数据权限检查
+	p := actions.GetPermissionFromContext(c)
+
+	list := make([]model.Waybill, 0)
+	var count int64
+	err = s.GetAppletCount(&list, &count, p)
+	if err != nil {
+		e.Error(500, err, err.Error())
+		return
+	}
+
+	var statusCount = make(map[int]int)
+	statusCount[0] = int(count)
+	for _, waybill := range list {
+		if  _, ok := statusCount[waybill.Status]; ok {
+			statusCount[waybill.Status] += 1
+		}else {
+			statusCount[waybill.Status] = 1
+		}
+	}
+
+	e.OK(statusCount, "查询成功")
+}
+
+
 // Get 通过id获取运单
 // @Summary 通过id获取运单
 // @Description 通过id获取运单
@@ -955,3 +1015,811 @@ func (e WaybillController) ExportTemplate(c *gin.Context) {
 	c.File(filePath)
 
 }
+
+// TemperaturePDF 导出温度记录
+// @Summary  导出温度记录
+// @Description  导出温度记录
+// @Tags 运单
+// @Accept  application/json
+// @Product application/json
+// @Param data body dto.WaybillInsertReq true "data"
+// @Success 200 {string} string	"{"code": 200, "message": "添加成功"}"
+// @Success 200 {string} string	"{"code": -1, "message": "添加失败"}"
+// @Router /api/waybill/temperature-pdf [post]
+// @Security Bearer
+func (e WaybillController) TemperaturePDF(c *gin.Context) {
+	s := service.Waybill{}
+	req := dto.WaybillGetByWaybillNoReq{}
+	err := e.MakeContext(c).
+		MakeOrm().
+		Bind(&req, binding.Query).
+		MakeService(&s.Service).
+		Errors
+	if err != nil {
+		e.Logger.Error(err)
+		e.Error(500, err, err.Error())
+		return
+	}
+	var waybill model.Waybill
+	p := actions.GetPermissionFromContext(c)
+	err = s.GetByWaybillNo(&req, &waybill, p)
+	if err != nil {
+		e.Error(500, err, err.Error())
+		return
+	}
+	DeviceSensor_data, waybillPDF, err := s.GetAllData(&req)
+	// 最高温度、最低温度、最高湿度、最低湿度
+	var maxTemp, minTemp, maxHumidity, minHumidity float32
+	// 最高温度时间、最低温度时间、最高湿度时间、最低湿度时间
+	var maxTempTime, minTempTime, maxHumidityTime, minHumidityTime string
+	// 总温度 总湿度
+	var totalTemp, totalHumidity float32
+	// 平均温度 平均湿度
+	var avgTemp, avgHumidity float32
+	// 温度阈值,湿度阈值
+	var tempThreshold, humidityThreshold string
+	// 记录开始时间,记录结束时间
+	var s_time, e_time string
+
+	var lastTime string
+	var isFirst, isSecond = true, false
+	var first_column, second_column []nats_server.DeviceData_R
+
+	if len(DeviceSensor_data) > 0 {
+		tempThreshold = fmt.Sprintf("%.1f-%.1f", DeviceSensor_data[0].T_tl, DeviceSensor_data[0].T_tu)
+		humidityThreshold = fmt.Sprintf("%.1f-%.1f", DeviceSensor_data[0].T_rhl, DeviceSensor_data[0].T_rhu)
+		s_time = DeviceSensor_data[0].T_time
+		e_time = DeviceSensor_data[len(DeviceSensor_data)-1].T_time
+		// 最高温度及时刻
+		maxTemp = DeviceSensor_data[0].T_t
+		maxTempTime = DeviceSensor_data[0].T_time
+		// 最低温度及时刻
+		minTemp = DeviceSensor_data[0].T_t
+		minTempTime = DeviceSensor_data[0].T_time
+
+		// 最高湿度及时刻
+		maxHumidity = DeviceSensor_data[0].T_rh
+		maxHumidityTime = DeviceSensor_data[0].T_time
+		// 获取最低湿度及时刻
+		minHumidity = DeviceSensor_data[0].T_rh
+		minHumidityTime = DeviceSensor_data[0].T_time
+		for i := 0; i < len(DeviceSensor_data); i++ {
+			data := DeviceSensor_data[i]
+			if data.T_t > maxTemp {
+				maxTemp = data.T_t
+				maxTempTime = data.T_time
+			}
+			if data.T_t < minTemp {
+				minTemp = data.T_t
+				minTempTime = data.T_time
+			}
+			totalTemp += data.T_t
+
+			if data.T_rh > maxHumidity {
+				maxHumidity = data.T_rh
+				maxHumidityTime = data.T_time
+			}
+			if data.T_rh < minHumidity {
+				minHumidity = data.T_rh
+				minHumidityTime = data.T_time
+			}
+			totalHumidity += data.T_rh
+
+		}
+		var sn string
+		for _, w := range waybillPDF {
+			for _, data := range w.Data {
+				if len(lastTime) > 0 {
+					if lastTime != data.T_time && isFirst == true {
+						isFirst = false
+						isSecond = true
+					} else if lastTime != data.T_time && isSecond == true {
+						isFirst = true
+						isSecond = false
+					}
+				}
+				if len(sn) > 0 && sn != data.T_sn {
+					isFirst, isSecond = isSecond, isFirst
+				}
+				if isFirst {
+					first_column = append(first_column, data)
+					lastTime = data.T_time
+					sn = data.T_sn
+				}
+				if isSecond {
+					second_column = append(second_column, data)
+					lastTime = data.T_time
+					sn = data.T_sn
+				}
+			}
+		}
+
+		// 平均温度
+		avgTemp = totalTemp / float32(len(DeviceSensor_data))
+		// 平均湿度
+		avgHumidity = totalHumidity / float32(len(DeviceSensor_data))
+	}
+
+	// -------------------获取最高温湿度、温蒂温湿度、平均温湿度结束
+
+	pdf := &gopdf.GoPdf{}
+	pdf.Start(gopdf.Config{PageSize: gopdf.Rect{W: 595.28, H: 841.89}}) //595.28, 841.89 = A4
+
+	err = pdf.AddTTFFont("wts", "static/fonts/MiSans-Medium.ttf")
+	if err != nil {
+		return
+	}
+	err = pdf.SetFont("wts", "", 20)
+	if err != nil {
+		return
+	}
+
+	pdf.SetGrayFill(0.5)
+
+	pdf.SetMargins(0, 20, 0, 20)
+	pdf.AddPage()
+
+	title := "运单" + req.WaybillNo + "温湿度记录"
+
+	var y float64 = 40
+	textw, _ := pdf.MeasureTextWidth(title)
+	pdf.SetX((595 / 2) - (textw / 2))
+	pdf.SetY(y)
+	pdf.Text(title)
+
+	//y += 30
+	//pdf.SetFont("wts", "", 16)
+	//pdf.SetXY(10, y)
+	//pdf.Text("实施设备信息")
+	//// 线
+	//y += 10
+	//pdf.SetLineWidth(0.5)
+	//pdf.SetStrokeColor(169, 169, 169)
+	//pdf.Line(10, y, 585, y)
+	//pdf.SetFont("wts", "", 10)
+	//y += 20
+	//pdf.SetXY(10, y)
+	//pdf.Text(fmt.Sprintf("主机名称:%s", device.T_devName))
+	//pdf.SetXY(300, y)
+	//pdf.Text(fmt.Sprintf("主机编号:%s", device.T_sn))
+
+	y += 35
+	pdf.SetFont("wts", "", 16)
+	pdf.SetXY(10, y)
+	pdf.Text("记录概要信息")
+	// 线
+	y += 10
+	pdf.SetLineWidth(0.5)
+	pdf.SetStrokeColor(169, 169, 169)
+	pdf.Line(10, y, 585, y)
+
+	y += 20
+	pdf.SetFont("wts", "", 10)
+	pdf.SetXY(10, y)
+	pdf.Text(fmt.Sprintf("记录开始时间:%s", s_time))
+
+	pdf.SetXY(240, y)
+	pdf.Text(fmt.Sprintf("记录结束时间:%s", e_time))
+
+	sTime, _ := lib.TimeStrToTime(s_time)
+	eTime, _ := lib.TimeStrToTime(e_time)
+	pdf.SetXY(470, y)
+	minutes := int(eTime.Sub(sTime).Minutes())
+	hours := minutes / 60
+	remainingMinutes := minutes % 60
+	pdf.Text(fmt.Sprintf("记录总时间:%dh%dmin", hours, remainingMinutes))
+
+	// -------------最高温/湿度 最低温/湿度 平均温/湿度
+	y += 15
+	pdf.SetXY(10, y)
+	pdf.Text(fmt.Sprintf("最高温度:%.1f℃,%s", lib.RoundToDecimal(float64(maxTemp), 1), maxTempTime))
+
+	pdf.SetXY(240, y)
+	pdf.Text(fmt.Sprintf("最低温度:%.1f℃,%s", lib.RoundToDecimal(float64(minTemp), 1), minTempTime))
+
+	pdf.SetXY(470, y)
+	pdf.Text(fmt.Sprintf("平均温度:%.1f℃", lib.RoundToDecimal(float64(avgTemp), 1)))
+
+	y += 15
+	pdf.SetXY(10, y)
+	pdf.Text(fmt.Sprintf("最高湿度:%.1f%%RH,%s", lib.RoundToDecimal(float64(maxHumidity), 1), maxHumidityTime))
+
+	pdf.SetXY(240, y)
+	pdf.Text(fmt.Sprintf("最低湿度:%.1f%%RH,%s", lib.RoundToDecimal(float64(minHumidity), 1), minHumidityTime))
+
+	pdf.SetXY(470, y)
+	pdf.Text(fmt.Sprintf("平均湿度:%.1f%%RH", lib.RoundToDecimal(float64(avgHumidity), 1)))
+
+	// -------------温/湿度阈值
+
+	y += 15
+	pdf.SetXY(10, y)
+	pdf.Text(fmt.Sprintf("温度阈值:%s℃", tempThreshold))
+
+	pdf.SetXY(240, y)
+	pdf.Text(fmt.Sprintf("温度阈值:%s%%", humidityThreshold))
+
+	//-------------发货单位,收货单位,备注
+	y += 15
+	pdf.SetXY(10, y)
+	T_forwarding_unit_temp := []rune(waybill.SenderAddressName)
+	if len(T_forwarding_unit_temp) > 17 {
+		pdf.Text(fmt.Sprintf("发货人:%s", string(T_forwarding_unit_temp[0:17])))
+		pdf.SetXY(60, y+15)
+		pdf.Text(fmt.Sprintf("%s", string(T_forwarding_unit_temp[17:])))
+	} else {
+		pdf.Text(fmt.Sprintf("发货人:%s", string(T_forwarding_unit_temp)))
+	}
+
+	pdf.SetXY(240, y)
+	T_consignee_unit_temp := []rune(waybill.ConsigneeAddressName)
+	if len(T_consignee_unit_temp) > 17 {
+		pdf.Text(fmt.Sprintf("收货人:%s", string(T_consignee_unit_temp[0:17])))
+		pdf.SetXY(290, y+15)
+		pdf.Text(fmt.Sprintf("%s", string(T_consignee_unit_temp[17:])))
+	} else {
+		pdf.Text(fmt.Sprintf("收货人:%s", string(T_consignee_unit_temp)))
+
+	}
+
+	y += 15
+	pdf.SetXY(10, y)
+	T_remark_temp := []rune(waybill.Remark)
+	if len(waybill.Remark) > 35 {
+		pdf.Text(fmt.Sprintf("备注:%s", string(T_remark_temp[0:35])))
+		pdf.SetXY(10, y+15)
+		pdf.Text(fmt.Sprintf("%s", string(T_remark_temp[35:])))
+	} else {
+		pdf.Text(fmt.Sprintf("备注: %s", string(T_remark_temp)))
+	}
+
+	y += 35
+	pdf.SetFont("wts", "", 16)
+	pdf.SetXY(10, y)
+	pdf.Text("记录曲线信息")
+	// 线
+	y += 10
+	pdf.SetLineWidth(0.5)
+	pdf.SetStrokeColor(169, 169, 169)
+	pdf.Line(10, y, 585, y)
+	y += 1
+
+	var tempFilepath string
+	tempFilepath, err = DeviceDataTemperatureJPG(s_time, e_time, waybillPDF)
+	if err == nil {
+		imgH, _ := gopdf.ImageHolderByPath(tempFilepath)
+		pdf.ImageByHolder(imgH, 10, y, &gopdf.Rect{W: 575, H: 315})
+		y += 315
+	}
+	var humidityFilepath string
+	humidityFilepath, err = DeviceDataHumidityJPG(s_time, e_time, waybillPDF)
+	if err == nil {
+		imgH, _ := gopdf.ImageHolderByPath(humidityFilepath)
+		pdf.ImageByHolder(imgH, 10, y, &gopdf.Rect{W: 575, H: 315})
+		y += 315
+	}
+
+	if y > 841.89 {
+		// 图片结束直接分页
+		pdf.AddPage()
+		y = 40
+	}
+
+	pdf.SetFont("wts", "", 16)
+	pdf.SetXY(10, y)
+	pdf.Text("记录数据信息")
+	// 线
+	y += 10
+	pdf.SetLineWidth(0.5)
+	pdf.SetStrokeColor(169, 169, 169)
+	pdf.Line(10, y, 585, y)
+	y += 10
+	pdf.SetFont("wts", "", 10)
+	var x float64 = 10
+	var w float64 = 112
+	lib.RectFillColor(pdf, "时间", 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+	x = x + w
+	w = 101
+	lib.RectFillColor(pdf, "名称", 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+	x = x + w
+	w = 37
+	lib.RectFillColor(pdf, "温度℃", 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+	x = x + w
+	w = 37
+	lib.RectFillColor(pdf, "湿度%", 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+	x = x + w
+	w = 112
+	lib.RectFillColor(pdf, "时间", 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+	x = x + w
+	w = 101
+	lib.RectFillColor(pdf, "名称", 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+	x = x + w
+	w = 37
+	lib.RectFillColor(pdf, "温度℃", 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+	x = x + w
+	w = 37
+	lib.RectFillColor(pdf, "湿度%", 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+	y += 20
+	var textH float64 = 25 // if text height is 25px.
+	for i, v := range first_column {
+		pdf.SetNewY(y, textH)
+		y = pdf.GetY()
+		x, w = 10, 112
+
+		lib.RectFillColor(pdf, v.T_time, 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+		x = x + w
+		w = 101
+		lib.RectFillColor(pdf, v.T_name, 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+		// 显示温湿度
+
+		x = x + w
+		w = 37
+		lib.RectFillColor(pdf, fmt.Sprintf(" %.1f ", v.T_t), 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+		x = x + w
+		w = 37
+		lib.RectFillColor(pdf, fmt.Sprintf(" %.1f ", v.T_rh), 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+		if i < len(second_column) {
+			x = x + w
+			w = 112
+			lib.RectFillColor(pdf, second_column[i].T_time, 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+			x = x + w
+			w = 101
+			lib.RectFillColor(pdf, second_column[i].T_name, 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+			x = x + w
+			w = 37
+			lib.RectFillColor(pdf, fmt.Sprintf(" %.1f ", second_column[i].T_t), 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+			x = x + w
+			w = 37
+			lib.RectFillColor(pdf, fmt.Sprintf(" %.1f ", second_column[i].T_rh), 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+		}
+
+		y += 20
+	}
+	if len(second_column) > len(first_column) {
+		for i := len(first_column); i < len(second_column); i++ {
+			pdf.SetNewY(y, textH)
+			y = pdf.GetY()
+			x, w = 297, 112
+			lib.RectFillColor(pdf, second_column[i].T_time, 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+			x = x + w
+			w = 101
+			lib.RectFillColor(pdf, second_column[i].T_name, 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+			x = x + w
+			w = 37
+			lib.RectFillColor(pdf, fmt.Sprintf(" %.1f ", second_column[i].T_t), 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+			x = x + w
+			w = 37
+			lib.RectFillColor(pdf, fmt.Sprintf(" %.1f ", second_column[i].T_rh), 12, x, y, w, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+
+			y += 20
+		}
+	}
+	filename := "运单" + req.WaybillNo + "温湿度记录" + time.Now().Format("20060102150405") + ".pdf"
+	filePath := "ofile/" + filename
+
+	err = pdf.WritePdf(filePath)
+	if err != nil {
+		return
+	}
+	//defer func() {
+	//	os.Remove(filePath)
+	//}()
+
+	c.Header("Content-Disposition", "attachment; filename="+url.PathEscape(filename))
+	c.Header("Content-Transfer-Encoding", "binary")
+	c.File(filePath)
+
+}
+
+// 获取温度图片
+func DeviceDataTemperatureJPG(startTime, endTime string, waybillPDF []service.WaybillPDF) (string, error) {
+
+	if len(waybillPDF) == 0 {
+		return "", errors.New("暂无数据可生成图片")
+	}
+
+	// 创建一个新的绘图
+	p := plot.New()
+
+	// 设置绘图标题和标签
+	p.Title.Text = "temperature"
+	//p.Legend.ThumbnailWidth = 20
+
+	deviceSensorList := []nats_server.DeviceSensor_R{}
+	dataList := []nats_server.DeviceData_R{}
+
+	for _, w := range waybillPDF {
+		deviceSensorList = append(deviceSensorList, w.DeviceSensorList...)
+		dataList = append(dataList, w.Data...)
+	}
+
+	TemperatureMin := deviceSensorList[0].T_DeviceSensorParameter.T_Tlower
+	TemperatureMax := deviceSensorList[0].T_DeviceSensorParameter.T_Tupper
+
+	var ymin, ymax float32
+	for i, r := range dataList {
+		if i == 0 {
+			ymin = r.T_t
+			ymax = r.T_t
+		}
+		if ymin > r.T_t {
+			ymin = r.T_t
+		}
+		if ymax < r.T_t {
+			ymax = r.T_t
+		}
+	}
+
+	var chData = make(chan int, 10)
+	var jobGroup sync.WaitGroup
+	// 创建温度线
+	for i := 0; i < len(deviceSensorList); i++ {
+		chData <- 1
+		jobGroup.Add(1)
+		go func(index int) {
+			defer func() {
+				<-chData        // 完成时chan取出1个
+				jobGroup.Done() // 完成时将等待组值减1
+			}()
+			sn, id := deviceSensorList[index].T_sn, deviceSensorList[index].T_id
+			r_maps := []nats_server.DeviceData_R{}
+			for _, data := range dataList {
+				if data.T_sn == sn && data.T_id == id {
+					r_maps = append(r_maps, data)
+				}
+			}
+			if len(r_maps) == 0 {
+				return
+			}
+			sort.Slice(r_maps, func(i, j int) bool {
+				return r_maps[i].T_time < r_maps[j].T_time
+			})
+
+			pts := make(plotter.XYs, len(r_maps))
+			for j, d := range r_maps {
+				t, _ := lib.TimeStrToTime(d.T_time)
+				pts[j].X = float64(t.Unix())
+				pts[j].Y = float64(d.T_t)
+			}
+
+			line, err := plotter.NewLine(pts)
+			if err != nil {
+				return
+			}
+			line.Color = randomColor(index)
+			p.Add(line)
+		}(i)
+	}
+	jobGroup.Wait()
+
+	st, _ := lib.TimeStrToTime(startTime)
+	et, _ := lib.TimeStrToTime(endTime)
+	xmin, xmax := float64(st.Unix()), float64(et.Unix())
+	// 添加最高,最低标准线 用红色虚线标识
+	p.Add(horizontalLine(xmin, xmax, float64(TemperatureMin)))
+	p.Add(horizontalLine(xmin, xmax, float64(TemperatureMax)))
+
+	if ymax < TemperatureMax {
+		ymax = TemperatureMax
+	}
+	if ymin > 0 {
+		ymin = 0
+	}
+
+	p.Y.Min, p.Y.Max = float64(ymin), float64(ymax)
+
+	p.X.Min, p.X.Max = xmin, xmax
+	p.Y.Tick.Marker = commaTicks{}
+	p.X.Tick.Marker = timeTicks{}
+	p.X.Tick.Label.Rotation = math.Pi / 5
+	p.X.Tick.Label.YAlign = draw.YCenter
+	p.X.Tick.Label.XAlign = draw.XRight
+
+	filepath := "ofile/" + "temperature" + deviceSensorList[0].T_sn + ".jpg"
+	// 保存文件
+	if err := p.Save(10*vg.Inch, 4*vg.Inch, filepath); err != nil {
+
+		logs.Error(lib.FuncName(), "生成图片失败", err)
+		return "", err
+	}
+
+	return filepath, nil
+
+}
+
+// 获取湿度图片
+func DeviceDataHumidityJPG(startTime, endTime string, waybillPDF []service.WaybillPDF) (string, error) {
+
+	if len(waybillPDF) == 0 {
+		return "", errors.New("暂无数据可生成图片")
+	}
+
+	// 创建一个新的绘图
+	p := plot.New()
+
+	// 设置绘图标题和标签
+	p.Title.Text = "humidity"
+
+	deviceSensorList := []nats_server.DeviceSensor_R{}
+	dataList := []nats_server.DeviceData_R{}
+
+	for _, w := range waybillPDF {
+		deviceSensorList = append(deviceSensorList, w.DeviceSensorList...)
+		dataList = append(dataList, w.Data...)
+	}
+
+	humidityMin := deviceSensorList[0].T_DeviceSensorParameter.T_RHlower
+	humidityMax := deviceSensorList[0].T_DeviceSensorParameter.T_RHupper
+
+	var ymin, ymax float32
+	for i, r := range dataList {
+		if i == 0 {
+			ymin = r.T_rh
+			ymax = r.T_rh
+		}
+		if ymin > r.T_rh {
+			ymin = r.T_rh
+		}
+		if ymax < r.T_rh {
+			ymax = r.T_rh
+		}
+	}
+
+	var chData = make(chan int, 10)
+	var jobGroup sync.WaitGroup
+	// 创建温度线
+	for i := 0; i < len(deviceSensorList); i++ {
+		chData <- 1
+		jobGroup.Add(1)
+		go func(index int) {
+			defer func() {
+				<-chData        // 完成时chan取出1个
+				jobGroup.Done() // 完成时将等待组值减1
+			}()
+			sn, id := deviceSensorList[index].T_sn, deviceSensorList[index].T_id
+			r_maps := []nats_server.DeviceData_R{}
+			for _, data := range dataList {
+				if data.T_sn == sn && data.T_id == id {
+					r_maps = append(r_maps, data)
+				}
+			}
+			if len(r_maps) == 0 {
+				return
+			}
+			sort.Slice(r_maps, func(i, j int) bool {
+				return r_maps[i].T_time < r_maps[j].T_time
+			})
+
+			pts := make(plotter.XYs, len(r_maps))
+			for j, d := range r_maps {
+				t, _ := lib.TimeStrToTime(d.T_time)
+				pts[j].X = float64(t.Unix())
+				pts[j].Y = float64(d.T_rh)
+			}
+
+			line, err := plotter.NewLine(pts)
+			if err != nil {
+				return
+			}
+			line.Color = randomColor(index)
+			p.Add(line)
+		}(i)
+	}
+	jobGroup.Wait()
+
+	st, _ := lib.TimeStrToTime(startTime)
+	et, _ := lib.TimeStrToTime(endTime)
+	xmin, xmax := float64(st.Unix()), float64(et.Unix())
+	// 添加最高,最低标准线 用红色虚线标识
+	p.Add(horizontalLine(xmin, xmax, float64(humidityMin)))
+	p.Add(horizontalLine(xmin, xmax, float64(humidityMax)))
+
+	if ymax < humidityMax {
+		ymax = humidityMax
+	}
+	if ymin > 0 {
+		ymin = 0
+	}
+
+	p.Y.Min, p.Y.Max = float64(ymin), float64(ymax)
+
+	p.X.Min, p.X.Max = xmin, xmax
+	p.Y.Tick.Marker = commaTicks{}
+	//p.X.Tick.Marker = plot.TimeTicks{Format: "2006-01-02 15:04:05"}
+	p.X.Tick.Marker = timeTicks{}
+	p.X.Tick.Label.Rotation = math.Pi / 5
+	p.X.Tick.Label.YAlign = draw.YCenter
+	p.X.Tick.Label.XAlign = draw.XRight
+
+	filepath := "ofile/" + "humidity" + deviceSensorList[0].T_sn + ".jpg"
+	// 保存文件
+	if err := p.Save(10*vg.Inch, 4*vg.Inch, filepath); err != nil {
+
+		logs.Error(lib.FuncName(), "生成图片失败", err)
+		return "", err
+	}
+
+	return filepath, nil
+
+}
+func horizontalLine(xmin, xmax, y float64) *plotter.Line {
+	pts := make(plotter.XYs, 2)
+	pts[0].X = xmin
+	pts[0].Y = y
+	pts[1].X = xmax
+	pts[1].Y = y
+	line, err := plotter.NewLine(pts)
+	if err != nil {
+		panic(err)
+	}
+	line.LineStyle.Dashes = []vg.Length{vg.Points(8), vg.Points(5), vg.Points(1), vg.Points(5)}
+	line.Color = color.RGBA{R: 255, A: 255}
+	return line
+}
+
+type timeTicks struct{}
+
+func (timeTicks) Ticks(min, max float64) []plot.Tick {
+	tks := plot.TimeTicks{}.Ticks(min, max)
+	for i, t := range tks {
+		//if t.Label == "" { // Skip minor ticks, they are fine.
+		//	continue
+		//}
+		tks[i].Label = time.Unix(int64(t.Value), 0).Format("2006-01-02 15:04:05")
+	}
+
+	return tks
+}
+
+type commaTicks struct{}
+
+// Ticks computes the default tick marks, but inserts commas
+// into the labels for the major tick marks.
+func (commaTicks) Ticks(min, max float64) []plot.Tick {
+	tks := plot.DefaultTicks{}.Ticks(min, max)
+	for i, t := range tks {
+		//if t.Label == "" { // Skip minor ticks, they are fine.
+		//	continue
+		//}
+		tks[i].Label = fmt.Sprintf("%.0f", t.Value)
+	}
+
+	return tks
+}
+
+// 生成随机颜色的辅助函数
+func randomColor(i int) color.RGBA {
+	var colors []color.RGBA
+	colors = append(colors,
+		color.RGBA{R: 52, G: 152, B: 219, A: 255},
+		color.RGBA{R: 230, G: 126, B: 34, A: 255},
+		color.RGBA{R: 142, G: 68, B: 173, A: 255},
+		color.RGBA{R: 211, G: 84, B: 0, A: 255},
+		color.RGBA{R: 231, G: 76, B: 60, A: 255},
+		color.RGBA{R: 26, G: 188, B: 156, A: 255},
+		color.RGBA{R: 243, G: 156, B: 18, A: 255},
+		color.RGBA{R: 22, G: 160, B: 133, A: 255},
+		color.RGBA{R: 46, G: 204, B: 113, A: 255},
+		color.RGBA{R: 39, G: 174, B: 96, A: 255},
+		color.RGBA{R: 41, G: 128, B: 185, A: 255},
+		color.RGBA{R: 155, G: 89, B: 182, A: 255},
+		color.RGBA{R: 192, G: 57, B: 43, A: 255},
+		color.RGBA{R: 241, G: 196, B: 15, A: 255},
+	)
+
+	return colors[i%len(colors)]
+}
+func randomColor2(i int) drawing.Color {
+	var colors []drawing.Color
+	colors = append(colors,
+		drawing.Color{R: 52, G: 152, B: 219, A: 255},
+		drawing.Color{R: 230, G: 126, B: 34, A: 255},
+		drawing.Color{R: 142, G: 68, B: 173, A: 255},
+		drawing.Color{R: 211, G: 84, B: 0, A: 255},
+		drawing.Color{R: 231, G: 76, B: 60, A: 255},
+		drawing.Color{R: 26, G: 188, B: 156, A: 255},
+		drawing.Color{R: 243, G: 156, B: 18, A: 255},
+		drawing.Color{R: 22, G: 160, B: 133, A: 255},
+		drawing.Color{R: 46, G: 204, B: 113, A: 255},
+		drawing.Color{R: 39, G: 174, B: 96, A: 255},
+		drawing.Color{R: 41, G: 128, B: 185, A: 255},
+		drawing.Color{R: 155, G: 89, B: 182, A: 255},
+		drawing.Color{R: 192, G: 57, B: 43, A: 255},
+		drawing.Color{R: 241, G: 196, B: 15, A: 255},
+	)
+	return colors[i%len(colors)]
+}
+
+func DeviceDataHumidityJPG2(startTime, endTime string, waybillPDF []service.WaybillPDF) (string, error) {
+
+	graph := chart.Chart{
+		// 设置标题
+		Title: "温度曲线图",
+		// 设置X轴的标签
+		XAxis: chart.XAxis{
+			Name: "时间",
+			// 设置时间轴
+			ValueFormatter: chart.TimeValueFormatterWithFormat("2006-01-02 15:04:05"),
+		},
+		// 设置Y轴的标签
+		YAxis: chart.YAxis{
+			Name: "温度℃",
+		},
+	}
+	deviceSensorList := []nats_server.DeviceSensor_R{}
+	dataList := []nats_server.DeviceData_R{}
+
+	for _, w := range waybillPDF {
+		deviceSensorList = append(deviceSensorList, w.DeviceSensorList...)
+		dataList = append(dataList, w.Data...)
+	}
+
+	var chData = make(chan int, 10)
+	var jobGroup sync.WaitGroup
+	// 创建温度线
+	for i := 0; i < len(deviceSensorList); i++ {
+		chData <- 1
+		jobGroup.Add(1)
+		go func(index int) {
+			defer func() {
+				<-chData        // 完成时chan取出1个
+				jobGroup.Done() // 完成时将等待组值减1
+			}()
+			sn, id := deviceSensorList[index].T_sn, deviceSensorList[index].T_id
+			r_maps := []nats_server.DeviceData_R{}
+			for _, data := range dataList {
+				if data.T_sn == sn && data.T_id == id {
+					r_maps = append(r_maps, data)
+				}
+			}
+			if len(r_maps) == 0 {
+				return
+			}
+			sort.Slice(r_maps, func(i, j int) bool {
+				return r_maps[i].T_time < r_maps[j].T_time
+			})
+			var XValues []float64
+			var YValues []float64
+			for _, d := range r_maps {
+				t, _ := lib.TimeStrToTime(d.T_time)
+				XValues = append(XValues, float64(t.Unix()))
+				YValues = append(YValues, float64(d.T_t))
+			}
+			// 添加第一条折线系列
+			graph.Series = append(graph.Series, chart.ContinuousSeries{
+				Name: deviceSensorList[index].T_name,
+				Style: chart.Style{
+					StrokeColor: randomColor2(index),
+					StrokeWidth: 2.0,
+				},
+				XValues: XValues,
+				YValues: YValues,
+			})
+		}(i)
+	}
+	jobGroup.Wait()
+
+	filepath := "ofile/" + "humidity" + deviceSensorList[0].T_sn + ".jpg"
+	// 输出图表到PNG文件
+	f, err := os.Create(filepath)
+	if err != nil {
+		panic(err)
+	}
+	defer f.Close()
+	err = graph.Render(chart.PNG, f)
+	if err != nil {
+		panic(err)
+	}
+
+	return filepath, nil
+
+}

+ 2 - 0
app/admin/router/waybill.go

@@ -33,10 +33,12 @@ func registerWaybillRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle
 		r.POST("/customer", cont.CustomerInsert)    // 客户下单
 
 		r.GET("/applet", cont.GetAppletPage)           // app 运单列表
+		r.GET("/applet-count", cont.GetAppletCount)    // app 运单-统计
 		r.POST("/import", cont.Import)                 // 导入运单
 		r.GET("/export-template", cont.ExportTemplate) // 导出运单模板
 		r.GET("/home", cont.Home)                      // 首页统计
 		r.GET("/export", cont.Export)                  // 运单管理-导出
 		r.GET("/customer/export", cont.CustomerExport) // 客户下单
+		r.GET("/temperature-pdf", cont.TemperaturePDF) // 导出温湿度pdf
 	}
 }

+ 2 - 0
app/admin/service/dto/sys_user.go

@@ -112,6 +112,7 @@ func (s *SysUserInsertReq) GetId() interface{} {
 type SysUserUpdateReq struct {
 	Id   int    `json:"id"  comment:"用户ID"`                // 用户ID
 	Name string `json:"name" vd:"@:len($)>0;msg:'姓名不能为空'"` // 姓名
+	Type int    `json:"type" example:"2"`                  // 管理员1 仓管2 司机3
 
 	model2.ControlBy `swaggerignore:"true"`
 }
@@ -121,6 +122,7 @@ func (s *SysUserUpdateReq) Generate(userModel *model.SysUser) {
 		userModel.Id = s.Id
 	}
 	userModel.NickName = s.Name
+	userModel.Type = s.Type
 	if s.ControlBy.UpdateBy != 0 {
 		userModel.UpdateBy = s.UpdateBy
 	}

+ 4 - 0
app/admin/service/dto/waybill.go

@@ -222,3 +222,7 @@ type WaybillStatsRes struct {
 		Num  int64  `json:"num"`  // 数量
 	} `json:"stats"`
 }
+
+type WaybillGetByWaybillNoReq struct {
+	WaybillNo string `form:"waybillNo" vd:"len($)>0;msg:'订单编号不能为空'"` // 运单编号-必填
+}

+ 5 - 2
app/admin/service/sys_user.go

@@ -244,9 +244,12 @@ func (e *SysUser) Remove(c *dto.SysUserDeleteReq, p *actions.DataPermission) err
 	db := e.Orm.Model(&data).
 		Scopes(actions.UserPermission(data.TableName(), p)).
 		Find(&data, c.GetId())
-	if data.CreateBy == 1 && data.Type == 1 {
-		return errors.New("禁止删除管理员")
+	if p.UserId != 1 {
+		if data.CreateBy == 1 && data.Type == 1 {
+			return errors.New("禁止删除管理员")
+		}
 	}
+
 	if err = db.Error; err != nil {
 		e.Log.Errorf("db error: %s", err)
 		return err

+ 123 - 8
app/admin/service/waybill.go

@@ -13,6 +13,7 @@ import (
 	"gogs.baozhida.cn/zoie/OAuth-core/pkg/utils"
 	"gogs.baozhida.cn/zoie/OAuth-core/service"
 	"gorm.io/gorm"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -103,20 +104,32 @@ func (e *Waybill) GetAppletPage(c *dto.WaybillGetAppletPageReq, list *[]model.Wa
 		Joins("left join waybill_logistics on waybill.waybill_no = waybill_logistics.waybill_no").
 		Find(&list).Limit(-1).Offset(-1).Count(count).Error
 
-	//err = e.Orm.Model(&data).
-	//	Scopes(
-	//		cDto.MakeCondition(c.GetNeedSearch()),
-	//		cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
-	//		actions.Permission(data.TableName(), p),
-	//	).
-	//	Find(list).Limit(-1).Offset(-1).
-	//	Count(count).Error
 	if err != nil {
 		e.Log.Errorf("db error: %s", err)
 		return global.GetFailedErr
 	}
 	return nil
 }
+
+func (e *Waybill) GetAppletCount(list *[]model.Waybill, count *int64, p *actions.DataPermission) error {
+	var err error
+	//var data model.Waybill
+	var logistics model.WaybillLogistics
+	err = e.Orm.Table("waybill").
+		Select("waybill.*,waybill_logistics.status as status").
+		Scopes(
+			actions.Permission(logistics.TableName(), p)).
+		Where("waybill_logistics.id in (SELECT MAX(id) FROM waybill_logistics where user_id = ? group by waybill_no )", p.UserId).
+		Joins("left join waybill_logistics on waybill.waybill_no = waybill_logistics.waybill_no").
+		Find(list).Count(count).Error
+
+	if err != nil {
+		e.Log.Errorf("db error: %s", err)
+		return global.GetFailedErr
+	}
+	return nil
+}
+
 func (e *Waybill) GetCustomerPage(c *dto.WaybillGetCustomerPageReq, list *[]model.Waybill, count *int64, p *actions.DataPermission) error {
 	var err error
 	var data model.Waybill
@@ -169,6 +182,21 @@ func (e *Waybill) Get(d *dto.WaybillGetReq, waybillModel *model.Waybill, p *acti
 
 	return nil
 }
+func (e *Waybill) GetByWaybillNo(d *dto.WaybillGetByWaybillNoReq, waybillModel *model.Waybill, p *actions.DataPermission) error {
+	err := e.Orm.
+		Scopes(actions.Permission(waybillModel.TableName(), p)).
+		Where("waybill_no = ?", d.WaybillNo).
+		First(waybillModel).Error
+	if err != nil {
+		e.Log.Errorf("db error: %s", err)
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return global.GetNotFoundOrNoPermissionErr
+		}
+		return global.GetFailedErr
+	}
+
+	return nil
+}
 
 // Insert 创建Waybill对象
 func (e *Waybill) Insert(c *dto.WaybillInsertReq) error {
@@ -526,6 +554,10 @@ func (e *Waybill) WarehouseIn(c *dto.WaybillInOutReq, p *actions.DataPermission)
 			err = errors.New(fmt.Sprintf("运单号%s状态为%s,无法入库!", waybillNo, model.WaybillStatusMap[waybillModel.Status]))
 			return err
 		}
+		if waybillModel.Status == model.WaybillStatusWaitTruck {
+			err = errors.New(fmt.Sprintf("运单号%s状态为%s,无法入库!", waybillNo, model.WaybillStatusMap[waybillModel.Status]))
+			return err
+		}
 		var lng, lat string
 		lng, lat, err = e.GetSite(p.DeptId, warehouse.Sn, c.StartTime.String())
 		if err != nil {
@@ -780,6 +812,10 @@ func (e *Waybill) CarIn(c *dto.WaybillInOutReq, p *actions.DataPermission) error
 			err = errors.New(fmt.Sprintf("运单号%s状态为%s,无法装车!", waybillNo, model.WaybillStatusMap[waybillModel.Status]))
 			return err
 		}
+		if waybillModel.Status == model.WaybillStatusWaitStorage {
+			err = errors.New(fmt.Sprintf("运单号%s状态为%s,无法装车!", waybillNo, model.WaybillStatusMap[waybillModel.Status]))
+			return err
+		}
 		if waybillModel.Status == model.WaybillStatusWaitTruck {
 			waybillModel.DeliveryTime = c.StartTime
 		}
@@ -1232,3 +1268,82 @@ func (e *Waybill) GetBasicsStats(c *dto.WaybillStatsReq, p *actions.DataPermissi
 
 	return res
 }
+
+// 获取运单所有温湿度素具
+func (e *Waybill) GetAllData(c *dto.WaybillGetByWaybillNoReq) ([]nats_server.DeviceData_R, []WaybillPDF, error) {
+	var err error
+	var data model.WaybillTask
+	var waybill model.Waybill
+	var taskList []model.WaybillTask
+	var waybillPDF []WaybillPDF
+	dataList := make([]nats_server.DeviceData_R, 0)
+
+	err = e.Orm.Model(&waybill).Where("waybill_no = ?", c.WaybillNo).First(&waybill).Error
+	if err != nil {
+		e.Log.Errorf("db error: %s", err)
+		return dataList, waybillPDF, errors.New("获取运单信息失败")
+	}
+
+	// 未签收,不返回数据
+	if waybill.Status != model.WaybillStatusReceipt {
+		return dataList, waybillPDF, nil
+	}
+
+	// 获取公司秘钥
+	var company model.SysDept
+	company, err = model.GetCompanyById(waybill.DeptId)
+	if err != nil {
+		e.Log.Errorf("db error: %s", err)
+		return dataList, waybillPDF, model.GetCompanyKeyErr
+	}
+
+	err = e.Orm.Model(&data).
+		Where("waybill_no = ?", c.WaybillNo).
+		Order("id desc").
+		Find(&taskList).Error
+	if err != nil {
+		e.Log.Errorf("db error: %s", err)
+		return dataList, waybillPDF, global.GetFailedErr
+	}
+
+	for i := 0; i < len(taskList); i++ {
+		// 获取传感器信息
+		deviceSensorList, _, _ := nats_server.Cold_CompanyDeviceSensor_List_ByKey(taskList[i].Sn, company.ColdKey)
+		var T_snid string
+		var list []nats_server.DeviceData_R
+		for _, r := range deviceSensorList {
+			T_snid += fmt.Sprintf("%s,%d|", r.T_sn, r.T_id)
+		}
+		list, _, err = nats_server.Cold_ReadDeviceDataListBy_T_snid(T_snid, taskList[i].StartTime.String(), taskList[i].EndTime.String(), 0, 9999)
+		if err != nil {
+			e.Log.Errorf("nats 获取温湿度信息失败: %s", err)
+			return dataList, waybillPDF, global.GetFailedErr
+		}
+		dataList = append(dataList, list...)
+		waybillPDF = append(waybillPDF, WaybillPDF{
+			Data:             list,
+			DeviceSensorList: deviceSensorList,
+			Task:             taskList[i],
+		})
+	}
+	// 创建名称到权重的映射
+	orderMap := make(map[string]int)
+	for i, v := range taskList {
+		orderMap[v.Sn] = i
+	}
+	sort.Slice(dataList, func(i, j int) bool {
+		if dataList[i].T_time == dataList[j].T_time {
+			// 如果时间相同,则按预设顺序排序
+			return orderMap[dataList[i].T_sn] < orderMap[dataList[j].T_sn]
+		}
+		return dataList[i].T_time < dataList[j].T_time
+	})
+
+	return dataList, waybillPDF, nil
+}
+
+type WaybillPDF struct {
+	Data             []nats_server.DeviceData_R   `json:"data"`
+	DeviceSensorList []nats_server.DeviceSensor_R `json:"deviceSensorList"`
+	Task             model.WaybillTask            `json:"task"`
+}

+ 50 - 0
common/lib/GoPDF.go

@@ -0,0 +1,50 @@
+package lib
+
+import "github.com/signintech/gopdf"
+
+const (
+	ValignTop    = 1
+	ValignMiddle = 2
+	ValignBottom = 3
+)
+
+const (
+	AlignLeft   = 4
+	AlignCenter = 5
+	AlignRight  = 6
+)
+
+func RectFillColor(pdf *gopdf.GoPdf,
+	text string,
+	fontSize int,
+	x, y, w, h float64,
+	r, g, b uint8,
+	align, valign int,
+) {
+
+
+	pdf.SetLineWidth(0.1)
+	pdf.SetFillColor(r, g, b) //setup fill color
+	pdf.SetLineType("")       // 线条样式
+	pdf.RectFromUpperLeftWithStyle(x, y, w, h, "FD")
+	pdf.SetFillColor(0, 0, 0)
+
+	if align == AlignCenter {
+		textw, _ := pdf.MeasureTextWidth(text)
+		x = x + (w / 2) - (textw / 2)
+	} else if align == AlignRight {
+		textw, _ := pdf.MeasureTextWidth(text)
+		x = x + w - textw
+	}
+
+	pdf.SetX(x)
+
+	if valign == ValignMiddle {
+		y = y + (h / 2) - (float64(fontSize) / 2)
+	} else if valign == ValignBottom {
+		y = y + h - float64(fontSize)
+	}
+
+	pdf.SetY(y)
+	pdf.Cell(nil, text)
+}

+ 428 - 0
common/lib/lib.go

@@ -0,0 +1,428 @@
+package lib
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/nats-io/nats.go"
+	"math/rand"
+	"os"
+	"path/filepath"
+	"runtime"
+	"sync"
+
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var Run_My_Server = false // 运行当期服务
+
+// var DeviceRealSnMap map[string]int /*创建集合 */
+var DeviceRealSnMap *sync.Map /*创建集合 */
+var Nats *nats.Conn
+
+func init() {
+	//DeviceRealSnMap = make(map[string]int)
+	DeviceRealSnMap = new(sync.Map)
+}
+
+type JSONS struct {
+	//必须的大写开头
+	Code int16
+	Msg  string
+	Data interface{} // 泛型
+}
+type R_JSONS_List struct {
+	//必须的大写开头
+	Data      []interface{}
+	Num       int64
+	Page      int
+	Page_size int
+}
+type R_JSONS struct {
+	//必须的大写开头
+	Data      interface{}
+	Num       int64
+	Page      int
+	Page_size int
+}
+
+type R1_JSONS struct {
+	//必须的大写开头
+	List      interface{}
+	Num       int
+	Page      int
+	Page_size int
+	Pages     []Page_T
+}
+
+// 登录验证
+//func Verification(GetCookie string, GetString string) (bool, Account.Admin) {
+//	Run_My_Server = true // 运行当期服务
+//	// 自适应 参数
+//	User_tokey := GetCookie
+//	if len(User_tokey) == 0 {
+//		User_tokey = GetString
+//	}
+//	if len(User_tokey) == 0 {
+//		return false, Account.Admin{}
+//	}
+//	tokey, is := Account.Redis_Tokey_Get(User_tokey)
+//	T_uuid := strings.Split(tokey, "|")[0]
+//	if !is {
+//		return false, Account.Admin{}
+//	}
+//	var err error
+//	admin_r, err := Account.Read_Admin_ByUuid(T_uuid)
+//	if err != nil {
+//		return false, Account.Admin{}
+//	}
+//	Admin_r = &admin_r
+//	log.Println("登录 Admin_name 为:", Admin_r.T_name)
+//	return true, admin_r
+//}
+
+// func_page 分页   [{3 1} {4 2} {4 3} {4 4} {4 5} {4 6} {4 7} {4 8} {4 9} {5 2}]-
+type Page_T struct {
+	A int
+	V int64
+}
+
+func Func_page(Page int64, Page_size int64) (page_t_list []Page_T) {
+	if Page > 1 {
+		page_t_list = append(page_t_list, Page_T{A: 1, V: Page - 1})
+	}
+	i := int64(0)
+	for aa := int64(1); aa < 5; aa++ {
+		if Page-aa <= 0 {
+			break
+		}
+		page_t_list = append(page_t_list, Page_T{A: 2, V: Page - aa})
+		i++
+	}
+	page_t_list = append(page_t_list, Page_T{A: 3, V: Page})
+
+	for aa := int64(1); aa < 10-i; aa++ {
+		if Page_size < Page+aa {
+			break
+		}
+		page_t_list = append(page_t_list, Page_T{A: 4, V: Page + aa})
+	}
+	sort.Slice(page_t_list, func(i, j int) bool {
+		if page_t_list[i].V < page_t_list[j].V {
+			return true
+		}
+		return false
+	})
+	sort.Slice(page_t_list, func(i, j int) bool {
+		if page_t_list[i].A < page_t_list[j].A {
+			return true
+		}
+		return false
+
+	})
+	if Page < Page_size {
+		page_t_list = append(page_t_list, Page_T{A: 5, V: Page + 1})
+	}
+
+	return page_t_list
+}
+
+func Strval(value interface{}) string {
+	var key string
+	if value == nil {
+		return key
+	}
+
+	switch value.(type) {
+	case float64:
+		ft := value.(float64)
+		key = strconv.FormatFloat(ft, 'f', -1, 64)
+	case float32:
+		ft := value.(float32)
+		key = strconv.FormatFloat(float64(ft), 'f', -1, 64)
+	case int:
+		it := value.(int)
+		key = strconv.Itoa(it)
+	case uint:
+		it := value.(uint)
+		key = strconv.Itoa(int(it))
+	case int8:
+		it := value.(int8)
+		key = strconv.Itoa(int(it))
+	case uint8:
+		it := value.(uint8)
+		key = strconv.Itoa(int(it))
+	case int16:
+		it := value.(int16)
+		key = strconv.Itoa(int(it))
+	case uint16:
+		it := value.(uint16)
+		key = strconv.Itoa(int(it))
+	case int32:
+		it := value.(int32)
+		key = strconv.Itoa(int(it))
+	case uint32:
+		it := value.(uint32)
+		key = strconv.Itoa(int(it))
+	case int64:
+		it := value.(int64)
+		key = strconv.FormatInt(it, 10)
+	case uint64:
+		it := value.(uint64)
+		key = strconv.FormatUint(it, 10)
+	case string:
+		key = value.(string)
+	case []byte:
+		key = string(value.([]byte))
+	default:
+		newValue, _ := json.Marshal(value)
+		key = string(newValue)
+	}
+
+	return key
+}
+
+func To_int(value interface{}) int {
+	var key int
+	if value == nil {
+		return key
+	}
+	switch value.(type) {
+	case float64:
+		key = int(value.(float64))
+	case float32:
+		key = int(value.(float32))
+	case int:
+		key = int(value.(int))
+	case uint:
+		key = int(value.(uint))
+	case int8:
+		key = int(value.(int8))
+	case uint8:
+		key = int(value.(uint8))
+	case int16:
+		key = int(value.(int16))
+	case uint16:
+		key = int(value.(uint16))
+	case int32:
+		key = int(value.(int32))
+	case uint32:
+		key = int(value.(uint32))
+	case int64:
+		key = int(value.(int64))
+	case uint64:
+		key = int(value.(uint64))
+	case string:
+		key, _ = strconv.Atoi(value.(string))
+	case []byte:
+		key, _ = strconv.Atoi(string(value.([]byte)))
+	default:
+		newValue, _ := json.Marshal(value)
+		key, _ = strconv.Atoi(string(newValue))
+	}
+	return key
+}
+
+func To_float32(value interface{}) float32 {
+	var key float32
+	if value == nil {
+		return key
+	}
+
+	switch value.(type) {
+	case float64:
+		key = float32(value.(float64))
+	case float32:
+		key = float32(value.(float32))
+	case int:
+		key = float32(value.(int))
+	case uint:
+		key = float32(value.(uint))
+	case int8:
+		key = float32(value.(int8))
+	case uint8:
+		key = float32(value.(uint8))
+	case int16:
+		key = float32(value.(int16))
+	case uint16:
+		key = float32(value.(uint16))
+	case int32:
+		key = float32(value.(int32))
+	case uint32:
+		key = float32(value.(uint32))
+	case int64:
+		key = float32(value.(int64))
+	case uint64:
+		key = float32(value.(uint64))
+	case string:
+		key_float64, _ := strconv.ParseFloat(value.(string), 32/64)
+		key = float32(key_float64)
+	case []byte:
+		key_float64, _ := strconv.ParseFloat(string(value.([]byte)), 32/64)
+		key = float32(key_float64)
+	default:
+		newValue, _ := json.Marshal(value)
+		key_float64, _ := strconv.ParseFloat(string(newValue), 32/64)
+		key = float32(key_float64)
+	}
+
+	key_float64, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", key), 32/64)
+	key = float32(key_float64)
+
+	return key
+}
+
+func To_string(value interface{}) string {
+	var key string
+	if value == nil {
+		return key
+	}
+
+	switch value.(type) {
+	case float64:
+		ft := value.(float64)
+		key = strconv.FormatFloat(ft, 'f', -1, 64)
+	case float32:
+		ft := value.(float32)
+		key = strconv.FormatFloat(float64(ft), 'f', -1, 64)
+	case int:
+		it := value.(int)
+		key = strconv.Itoa(it)
+	case uint:
+		it := value.(uint)
+		key = strconv.Itoa(int(it))
+	case int8:
+		it := value.(int8)
+		key = strconv.Itoa(int(it))
+	case uint8:
+		it := value.(uint8)
+		key = strconv.Itoa(int(it))
+	case int16:
+		it := value.(int16)
+		key = strconv.Itoa(int(it))
+	case uint16:
+		it := value.(uint16)
+		key = strconv.Itoa(int(it))
+	case int32:
+		it := value.(int32)
+		key = strconv.Itoa(int(it))
+	case uint32:
+		it := value.(uint32)
+		key = strconv.Itoa(int(it))
+	case int64:
+		it := value.(int64)
+		key = strconv.FormatInt(it, 10)
+	case uint64:
+		it := value.(uint64)
+		key = strconv.FormatUint(it, 10)
+	case string:
+		key = value.(string)
+	case []byte:
+		key = string(value.([]byte))
+	default:
+		newValue, _ := json.Marshal(value)
+		key = string(newValue)
+	}
+	return key
+}
+
+func Float32_to_string(value *float32) string {
+	var key string
+	if value == nil {
+		return key
+	}
+	return fmt.Sprintf("%.1f", *value)
+}
+
+func Random(min, max int) int {
+	rand.Seed(time.Now().Unix()) //Seed生成的随机数
+	return rand.Intn(max-min) + min
+}
+
+// 取文本(字符串)中间
+func GetBetweenStr(str, start, end string) string {
+	n := strings.Index(str, start)
+	if n == -1 {
+		n = 0
+	} else {
+		n = n + len(start) // 增加了else,不加的会把start带上
+	}
+	str = string([]byte(str)[n:])
+	m := strings.Index(str, end)
+	if m == -1 {
+		m = len(str)
+	}
+	str = string([]byte(str)[:m])
+	return str
+}
+
+// getYearMonthToDay 查询指定年份指定月份有多少天
+// @params year int 指定年份
+// @params month int 指定月份
+func GetYearMonthToDay(year int, month int) int {
+	// 有31天的月份
+	day31 := map[int]bool{
+		1:  true,
+		3:  true,
+		5:  true,
+		7:  true,
+		8:  true,
+		10: true,
+		12: true,
+	}
+	if day31[month] == true {
+		return 31
+	}
+	// 有30天的月份
+	day30 := map[int]bool{
+		4:  true,
+		6:  true,
+		9:  true,
+		11: true,
+	}
+	if day30[month] == true {
+		return 30
+	}
+	// 计算是平年还是闰年
+	if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
+		// 得出2月的天数
+		return 29
+	}
+	// 得出2月的天数
+	return 28
+}
+
+func Decimal(value float64) float64 {
+	value, _ = strconv.ParseFloat(fmt.Sprintf("%.1f", value), 64)
+	return value
+}
+
+func Strconv_Atoi(string string) int {
+	int, _ := strconv.Atoi(string)
+	return int
+}
+
+// golang获取程序运行路径
+func GetCurrentDirectory() string {
+	dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
+
+	return strings.Replace(dir, "\\", "/", -1)
+}
+
+// 获取正在运行的函数名
+func FuncName() string {
+	pc := make([]uintptr, 1)
+	runtime.Callers(2, pc)
+	f := runtime.FuncForPC(pc[0])
+	return f.Name()
+}
+
+// 获取两个时间相差的天数,0表同一天,正数表t1>t2,负数表t1<t2
+func GetDiffDays(t1, t2 time.Time) int {
+	t1 = time.Date(t1.Year(), t1.Month(), t1.Day(), 0, 0, 0, 0, time.Local)
+	t2 = time.Date(t2.Year(), t2.Month(), t2.Day(), 0, 0, 0, 0, time.Local)
+
+	return int(t1.Sub(t2).Hours() / 24)
+}

+ 172 - 0
common/lib/libString.go

@@ -0,0 +1,172 @@
+package lib
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"math"
+	"math/rand"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// #取得随机字符串:通过打乱slice来操作
+func GetRandstring(length int, char string, rand_x int64) string {
+	if length < 1 {
+		return ""
+	}
+
+	if len(char) <= 6 || len(char) <= length {
+		char = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+	}
+
+	charArr := strings.Split(char, "")
+	ran := rand.New(rand.NewSource(time.Now().Unix() + rand_x))
+
+	l := len(charArr)
+	for i := l - 1; i > 0; i-- {
+		r := ran.Intn(i)
+		charArr[r], charArr[i] = charArr[i], charArr[r]
+	}
+	rchar := charArr[:length]
+	return strings.Join(rchar, "")
+}
+
+// 返回一个32位md5加密后的字符串
+func Md5(str string) string {
+	h := md5.New()
+	h.Write([]byte(str))
+	return hex.EncodeToString(h.Sum(nil))
+}
+
+// 转化
+func TimeStrToTime(T_time string) (time.Time, bool) {
+
+	stamp, err := time.ParseInLocation("2006-01-02 15:04:05", T_time, time.Local)
+	if err != nil {
+		return time.Time{}, false
+	}
+
+	return stamp, true
+}
+
+// 转化
+func DateStrToDate(T_date string) (time.Time, bool) {
+
+	stamp, err := time.ParseInLocation("2006-01-02", T_date, time.Local)
+
+	if err != nil {
+		return time.Time{}, false
+	}
+
+	return stamp, true
+}
+
+func SplitStringIds(str string, prefix string) (r []string) {
+	if len(str) == 0 {
+		return
+	}
+	Ids_str := strings.TrimRight(str, "|")
+	Ids := strings.Split(Ids_str, "|")
+	for _, v := range Ids {
+		r = append(r, strings.TrimLeft(v, prefix))
+	}
+	return r
+}
+
+func SplitStringToDotStr(str string, prefix string) (r string) {
+	Ids_str := strings.TrimRight(str, "|")
+	Ids := strings.Split(Ids_str, "|")
+	for _, v := range Ids {
+		r += strings.TrimLeft(v, prefix) + ","
+	}
+	r = strings.TrimRight(r, ",")
+	return r
+}
+
+func SplitStringToIntIds(str string, prefix string) (r []int) {
+	Ids_str := strings.TrimRight(str, "|")
+	Ids := strings.Split(Ids_str, "|")
+	for _, v := range Ids {
+		id, _ := strconv.Atoi(strings.TrimLeft(v, prefix))
+		r = append(r, id)
+	}
+	return r
+}
+
+func IntIdsDistinct(Ids []int) (result []int) {
+	distinctMap := make(map[int]int, len(Ids))
+	for _, Id := range Ids {
+		if _, ok := distinctMap[Id]; !ok {
+			distinctMap[Id] = 1
+			result = append(result, Id)
+		}
+	}
+	return result
+}
+
+func StringListToDotStr(str []string) (r string) {
+	//for _, v := range str {
+	//	r += v + ","
+	//}
+	//r = strings.TrimRight(r, ",")
+
+	return strings.Join(str, ",")
+}
+
+func IntListToDotStr(list []int) (r string) {
+	for _, v := range list {
+		r += fmt.Sprintf("%d,", v)
+	}
+	r = strings.TrimRight(r, ",")
+	return r
+}
+
+// ["a","b","c"] ==> "'a','b','c'"
+func StringListToQuotesDotStr(str []string) (r string) {
+	for _, v := range str {
+		r += fmt.Sprintf("'%s',", v)
+	}
+	r = strings.TrimRight(r, ",")
+	return r
+}
+
+// 四舍五入保留小数点后几位
+func RoundToDecimal(num float64, decimalPlaces int) float64 {
+	shift := math.Pow(10, float64(decimalPlaces))
+	return math.Round(num*shift) / shift
+}
+
+// 检查sn是否都相同
+// 2023103074298131,0|2023143741715643,0|
+func IsSNAllSame(T_snid string) (string, []int, bool) {
+
+	entries := strings.Split(strings.Trim(T_snid, "|"), "|")
+
+	if len(entries) == 1 {
+		fields := strings.Split(entries[0], ",")
+		if len(fields) == 2 {
+			return fields[0], []int{To_int(fields[1])}, true // 只有一个条目时,认为全相同
+		} else {
+			return "", []int{}, false
+		}
+	}
+
+	firstSN := ""
+	ids := []int{}
+	for _, entry := range entries {
+		fields := strings.Split(entry, ",")
+		if len(fields) == 2 {
+			currSN := fields[0]
+			ids = append(ids, To_int(fields[1]))
+			if firstSN == "" {
+				firstSN = currSN
+			} else if currSN != firstSN {
+				return "", []int{}, false // 发现不相同的 SN 值,直接返回 false
+			}
+		}
+	}
+
+	return firstSN, ids, true
+}

+ 2 - 2
common/nats/nats_server/NatsColdApi.go

@@ -85,9 +85,9 @@ func Cold_ReadDeviceDataListBy_T_snid(T_snid, startTime, endTime string, page, p
 	}
 	list := t_R.Data
 	sort.Slice(list, func(i, j int) bool {
-		// 先按 T_time 字段排序,如果 T_time 相同则按 T_id 字段排序
+		// 先按 T_time 字段排序,如果 T_time 相同则按 T_name 字段排序
 		if list[i].T_time == list[j].T_time {
-			return list[i].T_id < list[j].T_id
+			return list[i].T_name < list[j].T_name
 		}
 		return list[i].T_time > list[j].T_time
 	})

+ 2 - 2
common/nats/nats_server/models.go

@@ -52,8 +52,8 @@ type DeviceSensor_R struct {
 	//T_link     int    // 0:断开/故障 1连接 实时数据
 	//T_State    int    // 0 屏蔽   1 正常  (屏蔽后 只有内部管理员才能看到,用户 输入SN\名称 搜索时 也能看到)
 
-	T_DeviceSensorData DeviceData_R2 // 传感器最新数据
-	//T_DeviceSensorParameter DeviceSensorParameter_R //  设备参数
+	T_DeviceSensorData      DeviceData_R2           // 传感器最新数据
+	T_DeviceSensorParameter DeviceSensorParameter_R //  设备参数
 }
 type DeviceData_R2 struct {
 	T_t    float32 // 温度

+ 12 - 1
go.mod

@@ -37,13 +37,16 @@ require (
 )
 
 require (
+	git.sr.ht/~sbinet/gg v0.5.0 // indirect
 	github.com/BurntSushi/toml v1.1.0 // indirect
 	github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
 	github.com/KyleBanks/depth v1.2.1 // indirect
 	github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
+	github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect
 	github.com/bitly/go-simplejson v0.5.0 // indirect
 	github.com/bsm/redislock v0.5.0 // indirect
 	github.com/bytedance/sonic v1.8.0 // indirect
+	github.com/campoy/embedmd v1.0.0 // indirect
 	github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973 // indirect
 	github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
@@ -54,11 +57,14 @@ require (
 	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f // indirect
+	github.com/go-fonts/liberation v0.3.2 // indirect
+	github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
 	github.com/go-openapi/jsonreference v0.20.0 // indirect
 	github.com/go-openapi/spec v0.20.7 // indirect
 	github.com/go-openapi/swag v0.22.1 // indirect
+	github.com/go-pdf/fpdf v0.9.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.11.2 // indirect
@@ -92,6 +98,8 @@ require (
 	github.com/nsqio/go-nsq v1.0.8 // indirect
 	github.com/nyaruka/phonenumbers v1.0.55 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/richardlehane/mscfb v1.0.4 // indirect
 	github.com/richardlehane/msoleps v1.0.3 // indirect
 	github.com/robinjoseph08/redisqueue/v2 v2.1.0 // indirect
@@ -99,6 +107,7 @@ require (
 	github.com/shamsher31/goimgext v1.0.0 // indirect
 	github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
 	github.com/shirou/gopsutil v2.19.12+incompatible // indirect
+	github.com/signintech/gopdf v0.25.0 // indirect
 	github.com/spf13/cast v1.3.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df // indirect
@@ -106,18 +115,20 @@ require (
 	github.com/ugorji/go/codec v1.2.9 // indirect
 	github.com/urfave/cli v1.22.1 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+	github.com/wcharczuk/go-chart/v2 v2.1.1 // indirect
 	github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
 	github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.7.0 // indirect
 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
-	golang.org/x/image v0.14.0 // indirect
+	golang.org/x/image v0.15.0 // indirect
 	golang.org/x/net v0.21.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
 	golang.org/x/sys v0.17.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
 	golang.org/x/tools v0.7.0 // indirect
+	gonum.org/v1/plot v0.14.0 // indirect
 	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/AlecAivazis/survey.v1 v1.8.5 // indirect
 	gopkg.in/kyokomi/emoji.v1 v1.5.1 // indirect

+ 38 - 0
go.sum

@@ -11,6 +11,8 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k
 cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
 contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8=
+git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo=
 github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
@@ -42,6 +44,10 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
 github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
+github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
 github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k=
 github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
@@ -85,6 +91,8 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1
 github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
 github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/caddyserver/certmagic v0.10.6/go.mod h1:Y8jcUBctgk/IhpAzlHKfimZNyXCkfGgRTC0orl8gROQ=
+github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
+github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
 github.com/casbin/casbin/v2 v2.47.2 h1:FVdlX0GEYWpYj7IdSThBpidLr8Bp+yfvlmVNec5INtw=
 github.com/casbin/casbin/v2 v2.47.2/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
 github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
@@ -169,6 +177,8 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
 github.com/go-acme/lego/v3 v3.4.0/go.mod h1:xYbLDuxq3Hy4bMUT1t9JIuz6GWIWb3m5X+TeTHYaT7M=
 github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
+github.com/go-fonts/liberation v0.3.2 h1:XuwG0vGHFBPRRI8Qwbi5tIvR3cku9LUfZGq/Ar16wlQ=
+github.com/go-fonts/liberation v0.3.2/go.mod h1:N0QsDLVUQPy3UYg9XAc3Uh3UDMp2Z7M1o4+X98dXkmI=
 github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
 github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
 github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
@@ -176,6 +186,8 @@ github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJA
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea h1:DfZQkvEbdmOe+JK2TMtBM+0I9GSdzE2y/L1/AmD8xKc=
+github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea/go.mod h1:Y7Vld91/HRbTBm7JwoI7HejdDB0u+e9AUBO9MB7yuZk=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
@@ -192,6 +204,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
 github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
 github.com/go-openapi/swag v0.22.1 h1:S6xFhsBKAtvfphnJwRzeCh3OEGsTL/crXdEetSxLs0Q=
 github.com/go-openapi/swag v0.22.1/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
+github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
@@ -503,6 +517,8 @@ github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
 github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 h1:zyWXQ6vu27ETMpYsEMAsisQ+GqJ4e1TPvSNfdOPF0no=
+github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -564,6 +580,8 @@ github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/
 github.com/shirou/gopsutil v2.19.12+incompatible h1:WRstheAymn1WOPesh+24+bZKFkqrdCR8JOc77v4xV3Q=
 github.com/shirou/gopsutil v2.19.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/signintech/gopdf v0.25.0 h1:w+C1RWe89yHqrdU9WZwMoUvmUeeQhNxrmJWfN2h6plQ=
+github.com/signintech/gopdf v0.25.0/go.mod h1:d23eO35GpEliSrF22eJ4bsM3wVeQJTjXTHq5x5qGKjA=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -634,6 +652,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21
 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
 github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
+github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE=
+github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14=
 github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
@@ -647,6 +667,7 @@ github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluU
 github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4=
 github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
@@ -697,6 +718,7 @@ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
@@ -709,8 +731,11 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
 golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
 golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
+golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
+golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -723,8 +748,10 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
 golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -752,10 +779,12 @@ golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
@@ -768,6 +797,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -812,7 +842,9 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -835,6 +867,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -866,8 +899,10 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -875,6 +910,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE=
+gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=
 google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -960,6 +997,7 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
 k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

BIN
static/fonts/MiSans-Medium.ttf


BIN
static/fonts/iconfont.eot


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 44 - 0
static/fonts/iconfont.svg


BIN
static/fonts/iconfont.ttf


BIN
static/fonts/iconfont.woff


BIN
static/fonts/三极行楷简体-粗.ttf


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác