李春阳 1 năm trước cách đây
mục cha
commit
a5ebab84aa
13 tập tin đã thay đổi với 207 bổ sung54 xóa
  1. 2 1
      api-common/src/main/java/api/common/pojo/po/scene/SceneEvaluationRulePO.java
  2. 21 0
      api-common/src/main/java/api/common/pojo/po/system/SceneEvaluationRuleScriptPO.java
  3. 46 0
      simulation-resource-scene/src/main/java/com/css/simulation/resource/scene/controller/SceneImportController.java
  4. 13 0
      simulation-resource-scene/src/main/java/com/css/simulation/resource/scene/mapper/SceneEvaluationRuleMapper.java
  5. 22 2
      simulation-resource-scene/src/main/java/com/css/simulation/resource/scene/service/SceneImportService.java
  6. 37 0
      simulation-resource-scene/src/main/resources/mapper/SceneEvaluationRuleMapper.xml
  7. 36 39
      simulation-resource-server/src/main/java/com/css/simulation/resource/server/app/service/SceneEvaluationRuleService.java
  8. 1 1
      simulation-resource-server/src/main/java/com/css/simulation/resource/server/infra/db/mysql/mapper/SceneEvaluationRuleMapper.java
  9. 6 0
      simulation-resource-server/src/main/java/com/css/simulation/resource/server/infra/feign/fallback/SceneServiceFallback.java
  10. 5 0
      simulation-resource-server/src/main/java/com/css/simulation/resource/server/infra/feign/service/SceneService.java
  11. 0 8
      simulation-resource-server/src/main/java/com/css/simulation/resource/server/infra/runnable/SceneEvaluationUploadRunnable.java
  12. 6 3
      simulation-resource-server/src/main/resources/mysql/mapper/SceneEvaluationRuleMapper.xml
  13. 12 0
      simulation-resource-video/src/main/resources/bootstrap-aliyun.yaml

+ 2 - 1
api-common/src/main/java/api/common/pojo/po/scene/SceneEvaluationRulePO.java

@@ -44,7 +44,8 @@ public class SceneEvaluationRulePO implements Serializable {
     public Timestamp modifyTime;
     // 修改人
     public String modifyUserId;
-
     public String localPath;
+    public String status;
+    public String errorMsg;
 
 }

+ 21 - 0
api-common/src/main/java/api/common/pojo/po/system/SceneEvaluationRuleScriptPO.java

@@ -0,0 +1,21 @@
+package api.common.pojo.po.system;
+
+import api.common.pojo.common.CommonPO;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.io.Serializable;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SceneEvaluationRuleScriptPO extends CommonPO implements Serializable {
+
+    private String ruleId;
+    private String localPath;
+}

+ 46 - 0
simulation-resource-scene/src/main/java/com/css/simulation/resource/scene/controller/SceneImportController.java

@@ -3,7 +3,11 @@ package com.css.simulation.resource.scene.controller;
 import api.common.pojo.common.ResponseBodyVO;
 import api.common.pojo.constants.DictConstants;
 import api.common.pojo.param.scene.SceneImportParam;
+import api.common.pojo.po.scene.SceneEvaluationRulePO;
+import api.common.pojo.po.system.SceneEvaluationRuleScriptPO;
 import api.common.pojo.po.system.SceneImportPO;
+import api.common.util.StringUtil;
+import api.common.util.TimeUtil;
 import com.css.simulation.resource.scene.common.oauth.AuthorizationHolder;
 import com.css.simulation.resource.scene.common.feign.SceneService;
 import com.css.simulation.resource.scene.service.SceneImportService;
@@ -15,6 +19,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.util.Date;
 
 @Controller
 @Slf4j
@@ -85,4 +91,44 @@ public class SceneImportController {
         }
         return new ResponseBodyVO<>(ResponseBodyVO.Response.SUCCESS);
     }
+
+    @RequestMapping("/startImportEvaluationScript")
+    @ResponseBody
+    public ResponseBodyVO<String> startImportEvaluationScript(@RequestBody SceneEvaluationRuleScriptPO po) {
+        File file = new File(po.getLocalPath());
+        SceneEvaluationRulePO sceneEvaluationRulePO = new SceneEvaluationRulePO();
+        sceneEvaluationRulePO.setRuleId(po.getRuleId());
+        if (!file.exists()) {
+            sceneEvaluationRulePO.setStatus(DictConstants.SCENE_IMPORT_STATUS_0);
+            sceneEvaluationRulePO.setErrorMsg("路径错误,文件不存在");
+            sceneImportService.updateSceneEvaluationRule(sceneEvaluationRulePO);
+            return new ResponseBodyVO<>(ResponseBodyVO.Response.CLIENT_FAILURE);
+        }
+        try {
+            //* -------------------------------- 上传到 minio --------------------------------
+            log.info("上传到minio " + po.getLocalPath());
+            Integer nowTime = TimeUtil.getRq(new Date(), 0);
+            String randomCode = StringUtil.getRandomCode();
+            String objectPath = "场景评价可执行文件/" + nowTime + "/" + randomCode + "/";
+            String fileName = file.getName();
+            boolean uploadSuccess = sceneImportService.startUploadEvaluationScript(objectPath, fileName, po.getLocalPath());
+            if (!uploadSuccess) {
+                sceneEvaluationRulePO.setStatus(DictConstants.SCENE_IMPORT_STATUS_0);
+                sceneEvaluationRulePO.setErrorMsg("文件上传失败");
+                sceneImportService.updateSceneEvaluationRule(sceneEvaluationRulePO);
+                return new ResponseBodyVO<>(ResponseBodyVO.Response.CLIENT_FAILURE);
+            }
+            sceneEvaluationRulePO.setScriptName(fileName);
+            sceneEvaluationRulePO.setScriptPath(objectPath + fileName);
+            sceneEvaluationRulePO.setStatus(DictConstants.SCENE_IMPORT_STATUS_2);
+            sceneImportService.updateSceneEvaluationRule(sceneEvaluationRulePO);
+        } catch (Exception e) {
+            log.error("上传可执行文件失败:" + po.getRuleId());
+            sceneEvaluationRulePO.setStatus(DictConstants.SCENE_IMPORT_STATUS_0);
+            sceneEvaluationRulePO.setErrorMsg("上传失败");
+            sceneImportService.updateSceneEvaluationRule(sceneEvaluationRulePO);
+            return new ResponseBodyVO<>(ResponseBodyVO.Response.SERVER_FAILURE);
+        }
+        return new ResponseBodyVO<>(ResponseBodyVO.Response.SUCCESS);
+    }
 }

+ 13 - 0
simulation-resource-scene/src/main/java/com/css/simulation/resource/scene/mapper/SceneEvaluationRuleMapper.java

@@ -0,0 +1,13 @@
+package com.css.simulation.resource.scene.mapper;
+
+import api.common.pojo.po.scene.SceneEvaluationRulePO;
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+
+
+@Mapper
+@Repository
+public interface SceneEvaluationRuleMapper {
+
+    void updateSceneEvaluationRule(SceneEvaluationRulePO params);
+}

+ 22 - 2
simulation-resource-scene/src/main/java/com/css/simulation/resource/scene/service/SceneImportService.java

@@ -1,11 +1,13 @@
 package com.css.simulation.resource.scene.service;
 
 import api.common.pojo.constants.DictConstants;
+import api.common.pojo.po.scene.SceneEvaluationRulePO;
 import api.common.pojo.po.system.SceneImportPO;
 import api.common.util.FileUtil;
 import api.common.util.ObjectUtil;
 import api.common.util.StringUtil;
 import api.common.util.TimeUtil;
+import com.css.simulation.resource.scene.mapper.SceneEvaluationRuleMapper;
 import com.css.simulation.resource.scene.mapper.SceneImportMapper;
 import com.css.simulation.resource.scene.common.utils.PoUtil;
 import io.minio.MinioClient;
@@ -36,6 +38,9 @@ public class SceneImportService {
     @Resource(name = "minioClientPrivate")
     MinioClient minioClient;
 
+    @Resource
+    SceneEvaluationRuleMapper sceneEvaluationRuleMapper;
+
     @SneakyThrows
     public Boolean startImport(SceneImportPO po) {
         //minio路径生成
@@ -51,8 +56,7 @@ public class SceneImportService {
             sceneName = "交通事故场景";
         } else if (DictConstants.SCENE_REFERENCE_LIB.equals(sceneType)) {
             sceneName = "基准场景库";
-        }
-        else {
+        } else {
             return false;
         }
         String objectPath = sceneName + "/" + nowTime + "/" + randomCode + "/";
@@ -88,4 +92,20 @@ public class SceneImportService {
         sceneImportMapper.updateStatus(po);
     }
 
+    public void updateSceneEvaluationRule(SceneEvaluationRulePO sceneEvaluationRulePO) {
+        sceneEvaluationRuleMapper.updateSceneEvaluationRule(sceneEvaluationRulePO);
+    }
+
+    @SneakyThrows
+    public boolean startUploadEvaluationScript(String objectPath, String fileName, String localPath) {
+        //minio路径完善
+        String objectName = objectPath + fileName;
+        minioClient.uploadObject(UploadObjectArgs.builder()
+                .filename(localPath)
+                .bucket(bucketName)
+                .object(objectName)
+                .build());
+        return true;
+    }
+
 }

+ 37 - 0
simulation-resource-scene/src/main/resources/mapper/SceneEvaluationRuleMapper.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="com.css.simulation.resource.server.infra.db.mysql.mapper.SceneEvaluationRuleMapper">
+    <update id="updateSceneEvaluationRule" parameterType="api.common.pojo.po.scene.SceneEvaluationRulePO">
+        update simulation.scene_evaluation_rule
+        <set>
+            <if test="isDeleted != null and isDeleted!=''">
+                is_deleted = #{isDeleted,jdbcType=VARCHAR},
+            </if>
+            <if test="ruleName != null and ruleName!=''">
+                rule_name = #{ruleName,jdbcType=VARCHAR},
+            </if>
+            <if test="ruleDescription != null and ruleDescription!=''">
+                rule_description = #{ruleDescription,jdbcType=VARCHAR},
+            </if>
+            <if test="scriptName != null and scriptName!=''">
+                script_name = #{scriptName,jdbcType=VARCHAR},
+            </if>
+            <if test="scriptPath != null and scriptPath!=''">
+                script_path = #{scriptPath,jdbcType=VARCHAR},
+            </if>
+            <if test="modifyUserId != null and modifyUserId!=''">
+                modify_user_id = #{modifyUserId,jdbcType=VARCHAR},
+            </if>
+            <if test="modifyTime != null">
+                modify_time = #{modifyTime},
+            </if>
+            <if test="status != null">
+                status = #{status},
+            </if>
+            <if test="error_msg != null">
+                error_msg = #{errorMsg},
+            </if>
+        </set>
+        where rule_id = #{ruleId,jdbcType=VARCHAR}
+    </update>
+</mapper>

+ 36 - 39
simulation-resource-server/src/main/java/com/css/simulation/resource/server/app/service/SceneEvaluationRuleService.java

@@ -8,15 +8,15 @@ import api.common.pojo.po.scene.SceneComplexityPO;
 import api.common.pojo.po.scene.SceneEvaluationOperatePO;
 import api.common.pojo.po.scene.SceneEvaluationRulePO;
 import api.common.pojo.po.scene.SceneRiskPO;
-import api.common.util.LogUtil;
-import api.common.util.StringUtil;
-import api.common.util.TimeUtil;
+import api.common.pojo.po.system.SceneEvaluationRuleScriptPO;
+import api.common.util.*;
 import com.alibaba.druid.util.StringUtils;
 import com.css.simulation.resource.server.infra.db.mysql.mapper.SceneComplexityMapper;
 import com.css.simulation.resource.server.infra.db.mysql.mapper.SceneEvaluationRuleMapper;
 import com.css.simulation.resource.server.infra.db.mysql.mapper.ScenePackageMapper;
 import com.css.simulation.resource.server.infra.db.mysql.mapper.SceneRiskMapper;
 import com.css.simulation.resource.server.infra.feign.service.FileDownService;
+import com.css.simulation.resource.server.infra.feign.service.SceneService;
 import com.css.simulation.resource.server.infra.runnable.SceneEvaluationComputeRunnable;
 import com.css.simulation.resource.server.infra.threadpool.ThreadPool;
 import com.css.simulation.resource.server.infra.util.AuthUtil;
@@ -33,6 +33,9 @@ import java.util.Map;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import static api.common.pojo.constants.DictConstants.SCENE_IMPORT_STATUS_1;
+import static api.common.pojo.constants.DictConstants.SCENE_IMPORT_STATUS_2;
+
 @Slf4j
 @Service
 public class SceneEvaluationRuleService {
@@ -56,6 +59,9 @@ public class SceneEvaluationRuleService {
     @Resource
     private FileDownService fileDownService;
 
+    @Resource
+    private SceneService sceneService;
+
     @SneakyThrows
     public int querySceneEvaluationRuleOnly(String ruleName, String ruleId) {
         return sceneEvaluationRuleMapper.querySceneEvaluationRuleOnly(ruleName, ruleId);
@@ -102,6 +108,7 @@ public class SceneEvaluationRuleService {
             }
         }
 
+        sceneEvaluationRulePO.setStatus(SCENE_IMPORT_STATUS_2);
         // 判断 ruleId 是否为空
         if (StringUtil.isEmpty(sceneEvaluationRulePO.getRuleId())) {
             // 创建
@@ -116,54 +123,44 @@ public class SceneEvaluationRuleService {
 
     @SneakyThrows
     public ResponseBodyVO<String> uploadPyScriptAndSaveRuleByPath(SceneEvaluationRulePO sceneEvaluationRulePO) {
-
-        boolean hasRuleId = StringUtil.isEmpty(sceneEvaluationRulePO.getRuleId());
+        if (ObjectUtil.isNull(sceneEvaluationRulePO)) {
+            return new ResponseBodyVO<>(false, 500, "参数错误!", null);
+        }
+        String sceneType = sceneEvaluationRulePO.getRuleType();
+        if (ObjectUtil.isNull(sceneType) || DictConstants.SCENE_GENERAL.equals(sceneType)) {
+            return new ResponseBodyVO<>(false, 500, "参数错误!", null);
+        }
+        boolean createRule = StringUtil.isEmpty(sceneEvaluationRulePO.getRuleId());
 
         // 检查是否存在同名场景评价规则
         int repeatPyScriptAndSaveRuleCount = querySceneEvaluationRuleOnly(sceneEvaluationRulePO.getRuleName(),
-                hasRuleId ? null : sceneEvaluationRulePO.getRuleId());
+                createRule ? null : sceneEvaluationRulePO.getRuleId());
         if (repeatPyScriptAndSaveRuleCount > 0) {
             return new ResponseBodyVO<>(ResponseBodyVO.Response.CLIENT_FAILURE, "场景规则 " + sceneEvaluationRulePO.getRuleName() + " 已存在,请重新命名。");
         }
         // 新建或者是编辑时重新上传文件
-        if ((hasRuleId) || sceneEvaluationRulePO.getUploadNewFile()) {
+        if ((createRule) || sceneEvaluationRulePO.getUploadNewFile()) {
             String roleCode = AuthUtil.getCurrentUserRoleCode();
             if (!DictConstants.ROLE_CODE_ADMIN.equals(roleCode) && !DictConstants.ROLE_CODE_SYSADMIN.equals(roleCode)) {
                 return new ResponseBodyVO<>(ResponseBodyVO.Response.CLIENT_FAILURE, "非管理员禁止上传文件");
             }
-            // 检查文件内容是否合规
-//            String pyStr = new String(file.getBytes(), StandardCharsets.UTF_8);
-//            //1 校验 python 语法错误
-//            String pylint = PythonUtil.pylint(pyStr, PythonUtil.C, PythonUtil.R, PythonUtil.W, PythonUtil.E0401, PythonUtil.E0601);
-//            if (!pylint.contains(PythonUtil.PASS)) {
-//                return new ResponseBodyVO<>(ResponseBodyVO.Response.CLIENT_FAILURE, "代码格式错误,请检查。");
-//            }
-//            // 获取文件原本的名字
-//            String originName = file.getOriginalFilename();
-//            if (null == originName) {
-//                return new ResponseBodyVO<>(ResponseBodyVO.Response.CLIENT_FAILURE, "文件名为空,请检查!");
-//            }
-//            Integer nowTime = TimeUtil.getRq(new Date(), 0);
-//            String randomCode = StringUtil.getRandomCode();
-//            String fileName = "/" + DictConstants.SCENE_EVALUATION_RULE_PY_FILE + "/" + nowTime + "/" + randomCode + "/" + originName;
-//            ResponseBodyVO<String> response = fileDownService.upload(file, fileName);
-//            if (response.isStatus()) {
-//                sceneEvaluationRulePO.setScriptName(originName);
-//                sceneEvaluationRulePO.setScriptPath(fileName);
-//            } else {
-//                return new ResponseBodyVO<>(ResponseBodyVO.Response.CLIENT_FAILURE, "上传失败!");
-//            }
-//        }
-//
-//        // 判断 ruleId 是否为空
-//        if (StringUtil.isEmpty(sceneEvaluationRulePO.getRuleId())) {
-//            // 创建
-//            saveSceneEvaluationRule(sceneEvaluationRulePO);
-//        } else {
-//            // 修改
-//            updateSceneEvaluationRule(sceneEvaluationRulePO);
-//        }
+            sceneEvaluationRulePO.setStatus(SCENE_IMPORT_STATUS_1);
+            // 判断 ruleId 是否为空
+            if (StringUtil.isEmpty(sceneEvaluationRulePO.getRuleId())) {
+                // 创建
+                saveSceneEvaluationRule(sceneEvaluationRulePO);
+            } else {
+                // 修改
+                updateSceneEvaluationRule(sceneEvaluationRulePO);
+            }
+            // 异步上传
+            SceneEvaluationRuleScriptPO sceneEvaluationRuleScriptPO = new SceneEvaluationRuleScriptPO();
+            sceneEvaluationRuleScriptPO.setRuleId(sceneEvaluationRulePO.getRuleId());
+            sceneEvaluationRuleScriptPO.setLocalPath(sceneEvaluationRulePO.getLocalPath());
+            sceneService.startImportEvaluationScript(sceneEvaluationRuleScriptPO);
+            return new ResponseBodyVO<>(ResponseBodyVO.Response.SUCCESS);
         }
+        updateSceneEvaluationRule(sceneEvaluationRulePO);
         return new ResponseBodyVO<>(ResponseBodyVO.Response.SUCCESS);
     }
 

+ 1 - 1
simulation-resource-server/src/main/java/com/css/simulation/resource/server/infra/db/mysql/mapper/SceneEvaluationRuleMapper.java

@@ -24,7 +24,7 @@ public interface SceneEvaluationRuleMapper {
     @Select("<script>" +
             "select count(rule_name)\n" +
             "from scene_evaluation_rule\n" +
-            "where rule_name = #{ruleName}  \n" +
+            "where rule_name = #{ruleName}  AND is_deleted = '0' \n" +
             "<if test='ruleId != null'>" +
             " AND rule_id != #{ruleId}" +
             "</if>" +

+ 6 - 0
simulation-resource-server/src/main/java/com/css/simulation/resource/server/infra/feign/fallback/SceneServiceFallback.java

@@ -1,6 +1,7 @@
 package com.css.simulation.resource.server.infra.feign.fallback;
 
 import api.common.pojo.common.ResponseBodyVO;
+import api.common.pojo.po.system.SceneEvaluationRuleScriptPO;
 import api.common.pojo.po.system.SceneImportPO;
 import com.css.simulation.resource.server.infra.feign.service.SceneService;
 import org.springframework.stereotype.Component;
@@ -12,4 +13,9 @@ public class SceneServiceFallback implements SceneService {
     public ResponseBodyVO startImport(SceneImportPO po) {
         return new ResponseBodyVO<>(ResponseBodyVO.Response.SERVER_FAILURE);
     }
+
+    @Override
+    public ResponseBodyVO startImportEvaluationScript(SceneEvaluationRuleScriptPO po) {
+        return new ResponseBodyVO<>(ResponseBodyVO.Response.SERVER_FAILURE);
+    }
 }

+ 5 - 0
simulation-resource-server/src/main/java/com/css/simulation/resource/server/infra/feign/service/SceneService.java

@@ -1,6 +1,7 @@
 package com.css.simulation.resource.server.infra.feign.service;
 
 import api.common.pojo.common.ResponseBodyVO;
+import api.common.pojo.po.system.SceneEvaluationRuleScriptPO;
 import api.common.pojo.po.system.SceneImportPO;
 import com.css.simulation.resource.server.infra.feign.config.FeignConfiguration;
 import com.css.simulation.resource.server.infra.feign.fallback.SceneServiceFallback;
@@ -24,4 +25,8 @@ public interface SceneService {
     @PostMapping("/sceneImport/startImport")
     ResponseBodyVO startImport(SceneImportPO po);
 
+    @Async
+    @PostMapping("/sceneImport/startImportEvaluationScript")
+    ResponseBodyVO startImportEvaluationScript(SceneEvaluationRuleScriptPO po);
+
 }

+ 0 - 8
simulation-resource-server/src/main/java/com/css/simulation/resource/server/infra/runnable/SceneEvaluationUploadRunnable.java

@@ -1,8 +0,0 @@
-package com.css.simulation.resource.server.infra.runnable;
-
-/**
- * @author 15765
- * @date 2024/4/10 22:22
- */
-public class SceneEvaluationUploadRunnable {
-}

+ 6 - 3
simulation-resource-server/src/main/resources/mysql/mapper/SceneEvaluationRuleMapper.xml

@@ -6,12 +6,12 @@
         insert into simulation.scene_evaluation_rule
         (rule_id, rule_code, rule_name, rule_description, rule_type, script_name, script_path,
          create_user_id, create_time, modify_user_id, modify_time,
-         is_deleted)
+         is_deleted, status)
         values (#{ruleId,jdbcType=VARCHAR}, #{ruleCode,jdbcType=VARCHAR}, #{ruleName,jdbcType=VARCHAR},
                 #{ruleDescription,jdbcType=VARCHAR}, #{ruleType,jdbcType=VARCHAR}, #{scriptName,jdbcType=VARCHAR},
                 #{scriptPath,jdbcType=VARCHAR},
                 #{createUserId,jdbcType=VARCHAR}, #{createTime}, #{modifyUserId,jdbcType=VARCHAR}, #{modifyTime},
-                #{isDeleted,jdbcType=VARCHAR})
+                #{isDeleted,jdbcType=VARCHAR}, #{status,jdbcType=VARCHAR})
     </insert>
 
     <update id="updateSceneEvaluationRule" parameterType="api.common.pojo.po.scene.SceneEvaluationRulePO">
@@ -38,6 +38,9 @@
             <if test="modifyTime != null">
                 modify_time = #{modifyTime},
             </if>
+            <if test="status != null">
+                status = #{status},
+            </if>
         </set>
         where rule_id = #{ruleId,jdbcType=VARCHAR}
     </update>
@@ -79,7 +82,7 @@
         rule_id,rule_name,rule_type,script_name, is_deleted
         from simulation.scene_evaluation_rule
         <where>
-            is_deleted = '0'
+            is_deleted = '0' AND status = '2'
             <if test="sceneEvaluationType != null and sceneEvaluationType.size()>0">
                 AND rule_type IN
                 <foreach collection="sceneEvaluationType" item="item" index="index"

+ 12 - 0
simulation-resource-video/src/main/resources/bootstrap-aliyun.yaml

@@ -0,0 +1,12 @@
+spring:
+  cloud:
+    nacos:
+      discovery:
+        server-addr: 39.107.58.39:8848
+        namespace: 41b663fb-0996-4b6e-b10b-c369929762d7
+        group: dev
+      config:
+        server-addr: 39.107.58.39:8848
+        namespace: 41b663fb-0996-4b6e-b10b-c369929762d7
+        file-extension: yaml
+        group: dev