Explorar el Código

Add 添加查询设备最新数据功能

YueYunyun hace 2 años
padre
commit
a176fa1dfb

+ 26 - 0
SERVER/IotAdmin/app/iot/apis/device.go

@@ -314,3 +314,29 @@ func (e IotDeviceApi) SetDeviceConfig(c *gin.Context) {
 	}
 	e.OK(nil, "设置成功")
 }
+
+// GetDeviceLastData 查询表计最新数据
+// @Summary 查询表计最新数据
+// @Description 查询表计最新数据
+// @Tags 设备管理
+// @Success 200 {object} response.Response{data=models.IotDevice} "{"code": 200, "data": [...]}"
+// @Router /api/iot-device/data/{id} [get]
+// @Security Bearer
+func (e IotDeviceApi) GetDeviceLastData(c *gin.Context) {
+	s := &service.IotDeviceService{}
+	req := dto.IotDeviceGetReq{}
+	err := e.MakeContext(c).MakeOrm().Bind(&req).MakeService(&s.Service).Errors
+	if err != nil {
+		e.Logger.Error(err)
+		e.Error(500, err, err.Error())
+		return
+	}
+	data := make(map[string]interface{})
+	p := permission.GetPermissionFromContext(c)
+	err = s.GetDeviceLastData(&req, p, &data)
+	if err != nil {
+		e.Error(500, err, err.Error())
+		return
+	}
+	e.OK(data, "查询成功")
+}

+ 1 - 1
SERVER/IotAdmin/app/iot/router/device.go

@@ -28,6 +28,6 @@ func registerIotDeviceRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMidd
 		r.GET("/report-protocols", api.GetReportProtocols)
 		r.GET("/config/:id", api.GetDeviceConfig)
 		r.PUT("/config", api.SetDeviceConfig)
+		r.GET("/data/:id", api.GetDeviceLastData)
 	}
-
 }

+ 34 - 22
SERVER/IotAdmin/app/iot/service/device.go

@@ -8,6 +8,7 @@ import (
 	"IotAdmin/core/tools/utils"
 	"IotAdmin/iot/constant"
 	iotInterface "IotAdmin/iot/interface"
+	iotLog "IotAdmin/iot/log"
 	iotMeter "IotAdmin/iot/meter"
 	iotProtocol "IotAdmin/iot/protocol"
 	iotProtocolHandler "IotAdmin/iot/protocol/handler"
@@ -181,20 +182,10 @@ func (e *IotDeviceService) Refresh() error {
 }
 
 func (e *IotDeviceService) GetDeviceConfig(d *dto.IotDeviceGetReq, p *permission.DataPermission, data *map[string]interface{}) error {
-	device := &models.IotDevice{}
-	err := e.Orm.Model(device).Scopes(
-		permission.Permission(device.TableName(), p),
-	).First(device, d.GetId()).Error
+	device, err := getMeterById(d.Id, p, e)
 	if err != nil {
-		e.Log.Errorf("IotDeviceService GetDeviceConfig error:%s \r\n", err)
 		return err
 	}
-	if device == nil {
-		return errors.New("设备不存在或没有设备权限")
-	}
-	if device.Type != constant.IotDeviceTypeMeter {
-		return errors.New("设备类型不支持此操作")
-	}
 	var cfg interface{}
 	sn := device.Sn
 	if strings.Contains(sn, "_") {
@@ -213,20 +204,10 @@ func (e *IotDeviceService) GetDeviceConfig(d *dto.IotDeviceGetReq, p *permission
 }
 
 func (e *IotDeviceService) SetDeviceConfig(d *dto.IotDeviceSetConfigReq, p *permission.DataPermission) error {
-	device := &models.IotDevice{}
-	err := e.Orm.Model(device).Scopes(
-		permission.Permission(device.TableName(), p),
-	).First(device, d.Id).Error
+	device, err := getMeterById(d.Id, p, e)
 	if err != nil {
-		e.Log.Errorf("IotDeviceService GetDeviceConfig error:%s \r\n", err)
 		return err
 	}
-	if device == nil {
-		return errors.New("设备不存在或没有设备权限")
-	}
-	if device.Type != constant.IotDeviceTypeMeter {
-		return errors.New("设备类型不支持此操作")
-	}
 	if err = verifyDeviceConfig(device, d.Config); err != nil {
 		return err
 	}
@@ -245,6 +226,37 @@ func (e *IotDeviceService) SetDeviceConfig(d *dto.IotDeviceSetConfigReq, p *perm
 	return err
 }
 
+func (e *IotDeviceService) GetDeviceLastData(d *dto.IotDeviceGetReq, p *permission.DataPermission, m *map[string]interface{}) error {
+	device, err := getMeterById(d.Id, p, e)
+	if err != nil {
+		return err
+	}
+	data, err := iotLog.GetLastData(device.Sn)
+	if err != nil {
+		return err
+	}
+	*m, err = utils.ToMap(*data)
+	return err
+}
+
+func getMeterById(id int, p *permission.DataPermission, e *IotDeviceService) (*models.IotDevice, error) {
+	device := &models.IotDevice{}
+	err := e.Orm.Model(device).Scopes(
+		permission.Permission(device.TableName(), p),
+	).First(device, id).Error
+	if err != nil {
+		e.Log.Errorf("IotDeviceService GetDeviceConfig error:%s \r\n", err)
+		return nil, err
+	}
+	if device == nil {
+		return nil, errors.New("设备不存在或没有设备权限")
+	}
+	if device.Type != constant.IotDeviceTypeMeter {
+		return nil, errors.New("设备类型不支持此操作")
+	}
+	return device, err
+}
+
 func updateDeviceMap(deviceId int, changeType int) error {
 	q := sdk.Runtime.GetMemoryQueue("")
 	mp := make(map[string]interface{})

+ 4 - 0
SERVER/IotAdmin/config/sql/db.sql

@@ -312,6 +312,7 @@ INSERT INTO sys_menu VALUES (216, 213, '修改网关', '#', 'pencil-square', '/0
 INSERT INTO sys_menu VALUES (217, 213, '删除网关', '#', 'dash-square', '/0/207/213/217', 'F', '', 'iot:device:remove', '', 'btn btn-light-danger', 'handleDelete@0', 4, 0, 0, 0, 1, 1, '2024-03-28 16:46:19.055', '2024-04-01 09:09:56.511', NULL);
 INSERT INTO sys_menu VALUES (218, 213, '重新加载', '#', 'arrow-repeat', '/0/207/213/218', 'F', '', 'iot:device:refresh', '', 'btn btn-light-info', 'handleRefresh', 5, 0, 0, 0, 1, 1, '2024-03-28 16:46:19.055', '2024-04-01 09:09:56.511', NULL);
 INSERT INTO sys_menu VALUES (219, 213, '更新配置', '#', 'gear', '/0/207/213/219', 'F', '', 'iot:device:config', '', '', '', 6, 0, 0, 0, 0, 1, '2024-03-28 16:46:19.055', '2024-04-01 09:09:56.511', NULL);
+INSERT INTO sys_menu VALUES (220, 213, '查询表计数据', '#', 'eye', '/0/207/213/220', 'F', '', 'iot:device:data', '', '', '', 7, 0, 0, 0, 0, 1, '2024-04-03 23:46:19.055', '2024-04-03 23:46:19.055', NULL);
 
 INSERT INTO sys_api VALUES (222, 'IotAdmin/app/iot/apis.(*IotGroupApi).GetPage-fm', '获取分组列表', '/api/iot-group', 'GET', 'BUS', 0, 0, '2024-03-28 16:46:11.552', '2024-04-01 09:09:56.520', NULL);
 INSERT INTO sys_api VALUES (223, 'IotAdmin/app/iot/apis.(*IotGroupApi).Get-fm', '获取分组详情', '/api/iot-group/:id', 'GET', 'BUS', 0, 0, '2024-03-28 16:46:11.671', '2024-04-01 09:09:56.520', NULL);
@@ -328,6 +329,8 @@ INSERT INTO sys_api VALUES (233, 'IotAdmin/app/iot/apis.(*IotDeviceApi).GetDevic
 INSERT INTO sys_api VALUES (234, 'IotAdmin/app/iot/apis.(*IotDeviceApi).GetReportProtocols-fm', '获取上报协议列表', '/api/iot-device/report-protocol', 'GET', 'BUS', 0, 0, '2024-03-28 16:46:18.950', '2024-04-01 09:09:56.520', NULL);
 INSERT INTO sys_api VALUES (235, 'IotAdmin/app/iot/apis.(*IotDeviceApi).GetDeviceConfig-fm', '获取设备表计配置', '/api/iot-device/config/:id', 'GET', 'BUS', 0, 0, '2024-04-01 13:34:18.320', '2024-04-01 13:34:18.320', NULL);
 INSERT INTO sys_api VALUES (236, 'IotAdmin/app/iot/apis.(*IotDeviceApi).SetDeviceConfig-fm', '更新设备表计配置', '/api/iot-device/config', 'PUT', 'BUS', 0, 0, '2024-04-01 13:34:18.540', '2024-04-01 13:34:18.540', NULL);
+INSERT INTO sys_api VALUES (237, 'IotAdmin/app/iot/apis.(*IotDeviceApi).GetDeviceData-fm', '查询表计最新数据', '/api/iot-device/data/:id', 'GET', 'BUS', 0, 0, '2024-04-03 23:34:18.320', '2024-04-03 23:34:18.320', NULL);
+
 
 INSERT INTO sys_menu_api VALUES (208, 222);
 INSERT INTO sys_menu_api VALUES (209, 222);
@@ -350,6 +353,7 @@ INSERT INTO sys_menu_api VALUES (213, 234);
 INSERT INTO sys_menu_api VALUES (214, 234);
 INSERT INTO sys_menu_api VALUES (219, 235);
 INSERT INTO sys_menu_api VALUES (219, 236);
+INSERT INTO sys_menu_api VALUES (220, 237);
 
 
 

+ 7 - 3
SERVER/IotAdmin/core/tools/writer/file.go

@@ -38,6 +38,9 @@ func NewFileWriter(opts ...Option) (*FileWriter, error) {
 			log.Fatalf("[file] create dir error: %s", err.Error())
 		}
 	}
+	if p.opts.timeFormat == "" {
+		p.opts.timeFormat = timeFormat
+	}
 	var filename string
 	var err error
 	for {
@@ -84,7 +87,8 @@ func (p *FileWriter) write() {
 
 func (p *FileWriter) checkFile() {
 	info, _ := p.file.Stat()
-	if strings.Index(p.file.Name(), time.Now().Format(timeFormat)) < 0 ||
+
+	if strings.Index(p.file.Name(), time.Now().Format(p.opts.timeFormat)) < 0 ||
 		(p.opts.cap > 0 && uint(info.Size()) > p.opts.cap) {
 		//生成新文件
 		if uint(info.Size()) > p.opts.cap {
@@ -122,12 +126,12 @@ func (p *FileWriter) getFilename() string {
 	if p.opts.cap == 0 {
 		return filepath.Join(p.opts.path,
 			fmt.Sprintf("%s.%s",
-				time.Now().Format(timeFormat),
+				time.Now().Format(p.opts.timeFormat),
 				p.opts.suffix))
 	}
 	return filepath.Join(p.opts.path,
 		fmt.Sprintf("%s_%d.%s",
-			time.Now().Format(timeFormat),
+			time.Now().Format(p.opts.timeFormat),
 			p.num+1,
 			p.opts.suffix))
 }

+ 9 - 3
SERVER/IotAdmin/core/tools/writer/options.go

@@ -2,9 +2,10 @@ package writer
 
 // Options 可配置参数
 type Options struct {
-	path   string
-	suffix string //文件扩展名
-	cap    uint
+	path       string
+	timeFormat string
+	suffix     string //文件扩展名
+	cap        uint
 }
 
 func setDefault() Options {
@@ -36,4 +37,9 @@ func WithCap(n uint) Option {
 	return func(o *Options) {
 		o.cap = n
 	}
+} // WithTimeFormat set timeFormat
+func WithTimeFormat(t string) Option {
+	return func(o *Options) {
+		o.timeFormat = t
+	}
 }

+ 80 - 0
SERVER/IotAdmin/iot/log/data.go

@@ -5,7 +5,10 @@ import (
 	"IotAdmin/core/tools/writer"
 	iotStruct "IotAdmin/iot/struct"
 	"encoding/json"
+	"fmt"
 	"log"
+	"os"
+	"path/filepath"
 	"strings"
 	"time"
 )
@@ -55,6 +58,7 @@ func getWrite(data *iotStruct.CollectData) *writer.FileWriter {
 			// 不切割文件
 			writer.WithCap(0),
 			writer.WithSuffix("data"),
+			writer.WithTimeFormat("2006010215"),
 		)
 		if err != nil {
 			log.Printf("Report Log setup error: %s \r\n", err.Error())
@@ -73,3 +77,79 @@ func getDataLogPath() string {
 	}
 	return path
 }
+
+func GetLastData(sn string) (*iotStruct.LogData, error) {
+	path := getDataLogPath()
+	path += strings.ReplaceAll(sn, "_", "/")
+	dirPath := path
+	latestFilePath, err := getLatestFilePath(dirPath)
+	if err != nil {
+		return nil, err
+	}
+	bytes, err := os.ReadFile(latestFilePath)
+	if err != nil {
+		return nil, err
+	}
+	str := string(bytes)
+	arr := strings.Split(str, "\r\n")
+	if len(arr) <= 1 {
+		return nil, fmt.Errorf("数据为空")
+	}
+	dataStr := arr[len(arr)-1]
+	data := &iotStruct.LogData{}
+	if err = json.Unmarshal([]byte(dataStr), data); err != nil {
+		return nil, err
+	}
+	return data, nil
+}
+
+// getLatestFilePath 返回给定目录中最新被修改的文件的路径。
+// 如果目录不存在或者不是一个目录,或者发生任何错误,函数将返回相应的错误信息。
+func getLatestFilePath(dirPath string) (string, error) {
+	// 判断目录是否存在
+	info, err := os.Stat(dirPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return "", fmt.Errorf("目录 %s 不存在", dirPath)
+		}
+		return "", fmt.Errorf("获取目录信息失败: %w", err)
+	}
+	if !info.IsDir() {
+		return "", fmt.Errorf("%s 不是目录", dirPath)
+	}
+	// 查找目录下最新修改的文件
+	var latestModTime time.Time
+	var latestFilePath string
+
+	files, err := os.ReadDir(dirPath) // 更新为使用os.ReadDir代替ioutil.ReadDir
+	if err != nil {
+		return "", fmt.Errorf("读取目录失败: %w", err)
+	}
+
+	for _, f := range files {
+
+		file, err := f.Info()
+		if err != nil {
+			return "", fmt.Errorf("获取文件信息失败: %w", err)
+		}
+		if !file.Mode().IsRegular() {
+			continue // 跳过非普通文件(如目录、符号链接等)
+		}
+
+		fileModTime := file.ModTime()
+		if fileModTime.After(latestModTime) {
+			latestModTime = fileModTime
+			latestFilePath = filepath.Join(dirPath, file.Name())
+		}
+	}
+	// 确保找到的文件是有效的普通文件
+	fileInfo, err := os.Stat(latestFilePath)
+	if err != nil {
+		return "", fmt.Errorf("获取文件信息失败: %s: %w", latestFilePath, err)
+	}
+	if !fileInfo.Mode().IsRegular() {
+		return "", fmt.Errorf("%s 不是普通文件", latestFilePath)
+	}
+
+	return latestFilePath, nil
+}

+ 5 - 0
UI/IOTADMIN.VUE/src/api/iot/_device.ts

@@ -65,6 +65,11 @@ class deviceApi {
 			data: data
 		})
 	}
+	getDeviceData = (id: any) => {
+		return Rs.get({
+			url: "/iot-device/data/" + id
+		})
+	}
 }
 
 export default deviceApi

+ 3 - 3
UI/IOTADMIN.VUE/src/views/account/login.vue

@@ -8,8 +8,8 @@ const userStore = appStore.authStore
 const router = useRouter()
 const loginRef = ref<HTMLFormElement>()
 const loginForm = ref({
-	username: "admin",
-	password: "123456",
+	username: "",
+	password: "",
 	rememberMe: false,
 	code: "",
 	uuid: ""
@@ -82,7 +82,7 @@ function getCookie() {
 			username: username === undefined ? loginForm.value.username : username,
 			password: password === undefined ? loginForm.value.password : decrypt(password),
 			rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
-			code: "1",
+			code: "",
 			uuid: ""
 		}
 	} catch (e) {

+ 60 - 6
UI/IOTADMIN.VUE/src/views/iot/device/_meter.vue

@@ -11,6 +11,7 @@ const isAdd = ref(false)
 const tableRef = ref()
 const modalRef = ref()
 const modalConfigRef = ref()
+const modalDataRef = ref()
 const opts = reactive<any>({
 	columns: [
 		{ field: "id", name: "ID", width: 100, visible: false, isSort: false, tooltip: true },
@@ -308,13 +309,10 @@ function handelRemoveReportConfig(index: number) {
 		form.value.reportConfig.splice(index, 1)
 	})
 }
-
+const md = ref<any>({})
 function handelUpdateConfig(row: any) {
 	apis.iot.deviceApi.getDeviceConfig(row.id).then((res: any) => {
-		form2.value = {
-			id: row.id,
-			config: JSON.stringify(res.data)
-		}
+		md.value = res.data
 		modalConfigRef.value.show()
 	})
 }
@@ -329,6 +327,13 @@ function submitConfig() {
 	})
 }
 
+function handelMeterData(row: any) {
+	apis.iot.deviceApi.getDeviceData(row.id).then((res: any) => {
+		md.value = res.data
+		modalDataRef.value.show()
+	})
+}
+
 function init() {
 	apis.iot.deviceApi.getDeviceProtocols().then((res: any) => {
 		protocolOptions.value = res.data.map((v: any) => {
@@ -423,7 +428,7 @@ defineExpose({
 				<vb-tooltip content="修改配置" placement="top">
 					<el-button
 						link
-						type="danger"
+						type="warning"
 						@click="handelUpdateConfig(row)"
 						v-hasPermission="'iot:device:config'">
 						<template #icon>
@@ -431,6 +436,17 @@ defineExpose({
 						</template>
 					</el-button>
 				</vb-tooltip>
+				<vb-tooltip content="最新数据" placement="top">
+					<el-button
+						link
+						type="success"
+						@click="handelMeterData(row)"
+						v-hasPermission="'iot:device:data'">
+						<template #icon>
+							<VbIcon icon-name="eye" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
 				<vb-tooltip content="删除" placement="top">
 					<el-button
 						link
@@ -549,5 +565,43 @@ defineExpose({
 			:form-data="form2"
 			@confirm="submitConfig"
 			append-to-body></VbModal>
+		<VbModal v-model:modal="modalDataRef" title="最新数据" append-to-body>
+			<template #body="">
+				<el-row>
+					<dl class="w-50">
+						<dt>设备编码:</dt>
+						<dd>{{ md.n }}</dd>
+					</dl>
+					<dl class="w-50">
+						<dt>采集时间:</dt>
+						<dd>
+							{{
+								md.t < 20240000000000
+									? "-"
+									: dayjs(md.t + "", "YYYYMMDDHHmmss").format("YYYY-MM-DD HH:mm:ss")
+							}}
+						</dd>
+					</dl>
+					<dl class="w-100">
+						<dt>采集数据:</dt>
+						<dd v-if="md.d">
+							<el-row>
+								<dl
+									:class="{
+										'w-100': v.indexOf('Hi') == 0 || v.indexOf('Hu') == 0,
+										'w-25': true
+									}"
+									v-for="(v, i) in Object.keys(md.d)"
+									:key="i">
+									<dt>{{ v }}:</dt>
+									<dd>{{ md.d[v] }}</dd>
+								</dl>
+							</el-row>
+						</dd>
+						<dd v-else>-</dd>
+					</dl>
+				</el-row>
+			</template>
+		</VbModal>
 	</div>
 </template>