[Java]RuoYi帝可得-3工单管理
工单管理
准备工作
业务场景: 管理员在后台创建工单后,工作人员可在运营管理App中查看并根据情况选择执行或取消分配给自己的任务。
- 工单管理主要涉及到二个功能模块,业务流程如下:
- 工单是一种专业名词,是指用于
记录、处理、跟踪一项工作的完成情况。
- 管理人员登录后台系统选择创建工单,在工单类型里选择合适的工单类型,在设备编号里输入正确的设备编号。
- 工作人员在运营管理App可以看到分配给自己的工单,根据实际情况选择接收工单并完成,或者拒绝/取消工单。
- 立可得工单分为两大类 :
- 运营工单:运营人员来维护售货机
商品,即补货工单。 - 运维工单:运维人员来维护售货机
设备,即投放工单、撤机工单、维修工单。
- 工单有四种状态:1 待处理 2 进行中 3 已取消 4 已完成
- 对于工单和其他管理数据,下面是示意图:
- 关系字段:task_id、 product_type_id、inner_code、user_id、assignor_id、region_id
- 数据字典:task_status(1待办、2进行、3取消、4完成)
- 数据字典:create_type(0自动、1手动)
- PS:运营的工单包含补货信息,运维工单没有,所以运营工单需要单独创建补货工单
- 其他说明
- 创建所有工单,都会在工单表和工单明细表插入记录吗?
- 创建运维类工单只会在工单表插入数据。
- 创建运营类工单(补货工单)会在工单表和工单明细表插入数据。
- task_code和task_id有什么区别?
- task_code是工单编号,具有业务规则 ,格式为年月日+当日序号。
- task_id 为工单表数据唯一标识。
- 工单表的user_id和assignor_id分别是做什么的?
- user_id是工单执行人的id(运维或运营)
- assignor_id是工单指派人的id(创建工单的人)
生成代码
使用若依代码生成器,生成工管理前后端基础代码,并导入到项目中:
- 创建目录菜单
- 添加数据字典
先创建工单状态的字典类型
再创建工单状态的字典数据
先创建工单创建类型的字典类型
再创建工单创建类型的字典数据
- 配置代码生成信息
配置工单表(运维、运营)
配置工单类型表(工单原型)
配置工单详情表(工单原型)
创建自动补货任务表(工单原型)
- 下载代码并导入项目
解压ruoyi.zip得到前后端代码和动态菜单sql
注意:工单管理只需要后端代码,前端使用资料中的文件,菜单手动创建
- 配置工单前端代码
从资料中(工单前端)复制工单api请求js文件到api/manage目录下
从资料中(工单前端)复制货道的视图组件到views/manage目录下
创建运营工单二级菜单
创建运维工单二级菜单
修复bug
在工单表中,有一个desc备注字段,这个desc是一个数据库关键字,所以我们在执行查询时,报了语法错误所以要给所有的desc增加反引号表示一个普通的sql字段
<sql id="selectTaskVo"> select task_id, task_code, task_status, create_type, inner_code, user_id, user_name, region_id, `desc`, product_type_id, assignor_id, addr, create_time, update_time from tb_task </sql>使用资料中的文件覆盖工程文件,逐个处理desc字段麻烦
查看页面
查询工单列表
运营和运营工单共享一套后端接口,通过特定的查询条件区分工单类型,并在返回结果中包含工单类型的详细信息
@Data public class TaskVo extends Task { // 工单类型 private TaskType taskType; }/** * 查询运维工单列表 * * @param task 运维工单 * @return TaskVo集合 */ List<TaskVo> selectTaskVoList(Task task);<resultMap type="taskVo" id="TaskVoResult"> <result property="taskId" column="task_id"/> <result property="taskCode" column="task_code"/> <result property="taskStatus" column="task_status"/> <result property="createType" column="create_type"/> <result property="innerCode" column="inner_code"/> <result property="userId" column="user_id"/> <result property="userName" column="user_name"/> <result property="regionId" column="region_id"/> <result property="desc" column="desc"/> <result property="productTypeId" column="product_type_id"/> <result property="assignorId" column="assignor_id"/> <result property="addr" column="addr"/> <result property="createTime" column="create_time"/> <result property="updateTime" column="update_time"/> <association property="taskType" javaType="TaskType" column="product_type_id" select="com.dkd.manage.mapper.TaskTypeMapper.selectTaskTypeByTypeId"/> </resultMap> <select id="selectTaskVoList" resultMap="TaskVoResult"> <include refid="selectTaskVo"/> <where> <if test="taskCode != null and taskCode != ''">and task_code = #{taskCode}</if> <if test="taskStatus != null ">and task_status = #{taskStatus}</if> <if test="createType != null ">and create_type = #{createType}</if> <if test="innerCode != null and innerCode != ''">and inner_code = #{innerCode}</if> <if test="userId != null ">and user_id = #{userId}</if> <if test="userName != null and userName != ''">and user_name like concat('%', #{userName}, '%')</if> <if test="regionId != null ">and region_id = #{regionId}</if> <if test="desc != null and desc != ''">and `desc` = #{desc}</if> <if test="productTypeId != null ">and product_type_id = #{productTypeId}</if> <if test="assignorId != null ">and assignor_id = #{assignorId}</if> <if test="addr != null and addr != ''">and addr = #{addr}</if> <if test="params.isRepair != null and params.isRepair=='true'"> and product_type_id in (1,3,4) </if> <if test="params.isRepair != null and params.isRepair=='false'"> and product_type_id =2 </if> order by create_time desc </where> </select>/** * 查询运维工单列表 * @param task * @return TaskVo集合 */ List<TaskVo> selectTaskVoList(Task task);/** * 查询运维工单列表 * @param task * @return TaskVo集合 */ @Override public List<TaskVo> selectTaskVoList(Task task) { return taskMapper.selectTaskVoList(task); }/** * 查询工单列表 */ @PreAuthorize("@ss.hasPermi('manage:task:list')") @GetMapping("/list") public TableDataInfo list(Task task) { startPage(); List<TaskVo> voList = taskService.selectTaskVoList(task); return getDataTable(voList); }获取人员列表
根据售货机编号获取负责当前区域下的运营人员列表
/** * 根据设备编号查询设备信息 * * @param innerCode * @return VendingMachine */ @Select("select * from tb_vending_machine where inner_code=#{innerCode}") VendingMachine selectVendingMachineByInnerCode(String innerCode);/** * 根据设备编号查询设备信息 * * @param innerCode * @return VendingMachine */ VendingMachine selectVendingMachineByInnerCode(String innerCode);/** * 根据设备编号查询设备信息 * * @param innerCode * @return VendingMachine */ @Override public VendingMachine selectVendingMachineByInnerCode(String innerCode) { return vendingMachineMapper.selectVendingMachineByInnerCode(innerCode); }/** * 员工启用 */ public static final Long EMP_STATUS_NORMAL = 1L; /** * 员工禁用 */ public static final Long EMP_STATUS_DISABLE = 0L;@Autowired private IVendingMachineService vendingMachineService; /** * 根据售货机获取运营人员列表 */ @PreAuthorize("@ss.hasPermi('manage:emp:list')") @GetMapping("/businessList/{innerCode}") public AjaxResult businessList(@PathVariable("innerCode") String innerCode) { // 1.查询售货机信息 VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(innerCode); if (vm == null) { return error(); } // 2.根据区域id、角色编号、员工状态查询运营人员列表 Emp empParam = new Emp(); empParam.setRegionId(vm.getRegionId());// 设备所属区域 empParam.setStatus(DkdContants.EMP_STATUS_NORMAL);// 员工启用 empParam.setRoleCode(DkdContants.ROLE_CODE_BUSINESS);// 角色编码:运营员 return success(empService.selectEmpList(empParam)); }根据售货机编号获取负责当前区域下的运维人员列表
/** * 根据售货机获取运维人员列表 */ @PreAuthorize("@ss.hasPermi('manage:emp:list')") @GetMapping("/operationList/{innerCode}") public AjaxResult getOperationList(@PathVariable("innerCode") String innerCode) { // 1.查询售货机信息 VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(innerCode); if (vm == null) { return error("售货机不存在"); } // 2.根据区域id、角色编号、状态查询运维人员列表 Emp empParam = new Emp(); empParam.setRegionId(vm.getRegionId());// 设备所属区域 empParam.setStatus(DkdContants.EMP_STATUS_NORMAL);// 员工启用 empParam.setRoleCode(DkdContants.ROLE_CODE_OPERATOR);// 角色编码:维修员 return success(empService.selectEmpList(empParam)); }新增工单
本系统中有两类工单需要创建,分别是:
- 运维工单:运维工单主要是对售货机的操作,又可以细分为投放工单、撤机工单、维修工单
- 运营工单:运营工单主要是对货物的操作,只有一种就是补货工单
- 运维和运营新增工单共享一套后端接口
- 思路:新增工单时序图
- 新增工单业务流程图
- 1.查询售货机是否存在
- 2.校验售货机状态与工单类型是否相符
- 3.检查设备是否有未完成的同类型工单
- 4.查询并校验员工是否存在
- 5.校验员工区域是否匹配
- 6.TaskDto->Task并补充属性,保存工单
- 7.判断是否为补货工单
- 8.TaskDetailsDto->TaskDetails并补充属性,批量保存
- 实现步骤
- DTO
package com.dkd.manage.domain.dto; import lombok.Data; @Data public class TaskDetailsDto { private String channelCode;// 货道编号 private Long expectCapacity;// 期望补货数量 private Long skuId;// 商品Id private String skuName;// 商品名称 private String skuImage;// 商品图片 }package com.dkd.manage.domain.dto; import lombok.Data; import java.util.List; @Data public class TaskDto { private Long createType;// 创建类型 private String innerCode;// 关联设备编号 private Long userId;// 任务执行人Id private Long assignorId;// 用户创建人id private Long productTypeId;// 工单类型 private String desc;// 描述信息 private List<TaskDetailsDto> details;// 工单详情(只有补货工单才涉及) }- TaskDetailsMapper
/** * 批量新增工单详情 * @param taskDetailsList * @return 结果 */ int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);<insert id="batchInsertTaskDetails"> INSERT INTO tb_task_details(task_id, channel_code, expect_capacity, sku_id, sku_name, sku_image) VALUES <foreach item="item" collection="list" separator=","> (#{item.taskId}, #{item.channelCode}, #{item.expectCapacity}, #{item.skuId}, #{item.skuName}, #{item.skuImage}) </foreach> </insert>- ITaskDetailsService
/** * 批量新增工单详情 * @param taskDetailsList * @return 结果 */ int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);/** * 批量新增工单详情 * @param taskDetailsList * @return 结果 */ @Override public int batchInsertTaskDetails(List<TaskDetails> taskDetailsList) { return taskDetailsMapper.batchInsertTaskDetails(taskDetailsList); }- ITaskService
/** * 新增运营、运维工单 * * @param taskDto * @return 结果 */ int insertTaskDto(TaskDto taskDto);@Autowired private IVendingMachineService vendingMachineService; @Autowired private IEmpService empService; @Autowired private ITaskDetailsService taskDetailsService; @Autowired private RedisTemplate redisTemplate; /** * 新增运营、运维工单 * * @param taskDto * @return 结果 */ @Transactional @Override public int insertTaskDto(TaskDto taskDto) { //1. 查询售货机是否存在 VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(taskDto.getInnerCode()); if (vm == null) { throw new ServiceException("设备不存在"); } //2. 校验售货机状态与工单类型是否相符 checkCreateTask(vm.getVmStatus(), taskDto.getProductTypeId()); //3. 检查设备是否有未完成的同类型工单 hasTask(taskDto.getInnerCode(), taskDto.getProductTypeId()); //4. 查询并校验员工是否存在 Emp emp = empService.selectEmpById(taskDto.getUserId()); if (emp == null) { throw new ServiceException("员工不存在"); } //5. 校验员工区域是否匹配 if (!emp.getRegionId().equals(vm.getRegionId())) { throw new ServiceException("员工区域与设备区域不一致,无法处理此工单"); } //6. 将dto转为po并补充属性,保存工单 Task task = BeanUtil.copyProperties(taskDto, Task.class);// 属性复制 task.setTaskStatus(DkdContants.TASK_STATUS_CREATE);// 创建工单 task.setUserName(emp.getUserName());// 执行人名称 task.setRegionId(vm.getRegionId());// 所属区域id task.setAddr(vm.getAddr());// 地址 task.setCreateTime(DateUtils.getNowDate());// 创建时间 task.setTaskCode(generateTaskCode());// 工单编号 int taskResult = taskMapper.insertTask(task); //7. 判断是否为补货工单 if (taskDto.getProductTypeId().equals(DkdContants.TASK_TYPE_SUPPLY)) { // 8.保存工单详情 List<TaskDetailsDto> details = taskDto.getDetails(); if (CollUtil.isEmpty(details)) { throw new ServiceException("补货工单详情不能为空"); } // 将dto转为po补充属性 List<TaskDetails> taskDetailsList = details.stream().map(dto -> { TaskDetails taskDetails = BeanUtil.copyProperties(dto, TaskDetails.class); taskDetails.setTaskId(task.getTaskId()); return taskDetails; }).collect(Collectors.toList()); // 批量新增 taskDetailsService.batchInsertTaskDetails(taskDetailsList); } return taskResult; } /** * 生成并获取当天任务代码的唯一标识。 * 该方法首先尝试从Redis中获取当天的任务代码计数,如果不存在,则初始化为1并返回"日期0001"格式的字符串。 * 如果存在,则对计数加1并返回更新后的任务代码。 * * @return 返回当天任务代码的唯一标识,格式为"日期XXXX",其中XXXX是四位数字的计数。 */ public String generateTaskCode() { // 获取当前日期并格式化为"yyyyMMdd" String dateStr = DateUtils.getDate().replaceAll("-", ""); // 根据日期生成redis的键 String key = "dkd.task.code." + dateStr; // 判断key是否存在 if (!redisTemplate.hasKey(key)) { // 如果key不存在,设置初始值为1,并指定过期时间为1天 redisTemplate.opsForValue().set(key, 1, Duration.ofDays(1)); // 返回工单编号(日期+0001) return dateStr + "0001"; } // 如果key存在,计数器+1(0002),确保字符串长度为4位 return dateStr+StrUtil.padPre(redisTemplate.opsForValue().increment(key).toString(),4,'0'); } /** * 检查设备是否已有未完成的同类型工单。 * 本方法用于在创建新工单前,验证指定设备是否已经有处于进行中的同类型工单。 * 如果存在未完成的同类型工单,则抛出服务异常,阻止新工单的创建。 * * @param innerCode 设备的内部编码,用于唯一标识设备。 * @param productTypeId 任务的类型,决定任务的性质(投放、维修、补货、撤机)。 */ private void hasTask(String innerCode, Long productTypeId) { // 创建Task对象,并设置设备编号和工单类型ID,以及任务状态为进行中 Task taskParam = new Task(); taskParam.setInnerCode(innerCode); taskParam.setProductTypeId(productTypeId); taskParam.setTaskStatus(DkdContants.TASK_STATUS_PROGRESS); // 查询数据库中符合指定条件的工单列表 List<Task> taskList = taskMapper.selectTaskList(taskParam); // 如果存在未完成的同类型工单,则抛出服务异常 if (CollUtil.isNotEmpty(taskList)) { throw new ServiceException("该设备有未完成的同类型工单,不能重复创建"); } } /** * 根据设备的状态和任务类型,验证是否可以创建相应的任务。 * 如果条件不满足,抛出服务异常。 * * @param vmStatus 设备的状态,表示设备是否在运行。 * @param productTypeId 任务的类型,决定任务的性质(投放、维修、补货、撤机)。 */ private void checkCreateTask(Long vmStatus, Long productTypeId) { // 如果是投放工单,且设备状态为运行中,则抛出异常,因为设备已在运营中无法进行投放 if (productTypeId == DkdContants.TASK_TYPE_DEPLOY && vmStatus == DkdContants.VM_STATUS_RUNNING) { throw new ServiceException("该设备状态为运行中,无法进行投放"); } // 如果是维修工单,且设备状态不是运行中,则抛出异常,因为设备不在运营中无法进行维修 if (productTypeId == DkdContants.TASK_TYPE_REPAIR && vmStatus != DkdContants.VM_STATUS_RUNNING) { throw new ServiceException("该设备状态不是运行中,无法进行维修"); } // 如果是补货工单,且设备状态不是运行中,则抛出异常,因为设备不在运营状态无法进行补货 if (productTypeId == DkdContants.TASK_TYPE_SUPPLY && vmStatus != DkdContants.VM_STATUS_RUNNING) { throw new ServiceException("该设备状态不是运行中,无法进行补货"); } // 如果是撤机工单,且设备状态不是运行中,则抛出异常,因为设备不在运营状态无法进行撤机 if (productTypeId == DkdContants.TASK_TYPE_REVOKE && vmStatus != DkdContants.VM_STATUS_RUNNING) { throw new ServiceException("该设备状态不是运行中,无法进行撤机"); } }方法说明
- redisTemplate.opsForValue().increment(key):原子性地将计数器+1
- StrUtil.padPre(..., 4, '0'):左补零至4位长度
- TaskController
/** * 新增工单 */ @PreAuthorize("@ss.hasPermi('manage:task:add')") @Log(title = "工单", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody TaskDto taskDto) { // 设置指派人(登录用户)id taskDto.setAssignorId(getUserId()); return toAjax(taskService.insertTaskDto(taskDto)); }取消工单
对于未完成的工单,管理员可以进行取消操作
/** * 取消工单 */ @PreAuthorize("@ss.hasPermi('manage:task:edit')") @Log(title = "工单", businessType = BusinessType.UPDATE) @PutMapping("/cancel") public AjaxResult cancelTask(@RequestBody Task task) { return toAjax(taskService.cancelTask(task)); }/** * 取消工单 * @param task * @return 结果 */ int cancelTask(Task task);/** * 取消工单 * @param task * @return 结果 */ @Override public int cancelTask(Task task) { //1. 判断工单状态是否可以取消 // 先根据工单id查询数据库 Task taskDb = taskMapper.selectTaskByTaskId(task.getTaskId()); // 判断工单状态是否为已取消,如果是,则抛出异常 if (taskDb.getTaskStatus().equals(DkdContants.TASK_STATUS_CANCEL)) { throw new ServiceException("该工单已取消了,不能再次取消"); } // 判断工单状态是否为已完成,如果是,则抛出异常 if (taskDb.getTaskStatus().equals(DkdContants.TASK_STATUS_FINISH)) { throw new ServiceException("该工单已完成了,不能取消"); } //2. 设置更新字段 task.setTaskStatus(DkdContants.TASK_STATUS_CANCEL);// 工单状态:取消 task.setUpdateTime(DateUtils.getNowDate());// 更新时间 //3. 更新工单 return taskMapper.updateTask(task);// 注意别传错了,这里是前端task参数 }查询补货详情
运营工单页面可以查看补货详情
/** * 查看工单补货详情 */ @PreAuthorize("@ss.hasPermi('manage:taskDetails:list')") @GetMapping(value = "/byTaskId/{taskId}") public AjaxResult byTaskId(@PathVariable("taskId") Long taskId) { TaskDetails taskDetailsParam = new TaskDetails(); taskDetailsParam.setTaskId(taskId); return success(taskDetailsService.selectTaskDetailsList(taskDetailsParam)); }Knife4j
如果不习惯使用swagger可以使用前端UI的增强解决方案knife4j,对比swagger相比有以下优势,友好界面,离线文档,接口排序,安全控制,在线调试,文档清晰,注解增强,容易上手。
1、ruoyi-common\pom.xml模块添加整合依赖
<!-- knife4j --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>2、views/tool/swagger/index.vue修改跳转访问地址(已完成)
const url = ref(import.meta.env.VITE_APP_BASE_API + "/doc.html")3、登录系统,访问菜单系统工具/系统接口,出现如下图表示成功。(首次加载较慢)
- TaskDetailsController添加swagger注解
@Api: 用于类级别,描述API的标签和描述。@ApiOperation: 用于方法级别,描述一个HTTP操作。@ApiParam: 用于参数级别,描述请求参数。
/** * 工单详情Controller * * @author itheima * @date 2024-07-22 */ @RestController @RequestMapping("/manage/taskDetails") @Api(tags = "工单详情") public class TaskDetailsController extends BaseController { /** * 查看工单补货详情 */ @ApiOperation("根据工单id查看工单补货详情") @PreAuthorize("@ss.hasPermi('manage:taskDetails:query')") @GetMapping(value = "/byTaskId/{taskId}") public R<List<TaskDetails>> byTaskId( @ApiParam(value = "工单ID", required = true) @PathVariable Long taskId) { TaskDetails taskDetailsParam = new TaskDetails(); taskDetailsParam.setTaskId(taskId); return R.ok(taskDetailsService.selectTaskDetailsList(taskDetailsParam)); } }注意:若依框架的AjaxResult由于继承自HashMap导致与Swagger和knife4j不兼容的问题,选择替换返回值类型为R以解决Swagger解析问题,减少整体改动量。
5、TaskDetails实体类添加swagger注解
@ApiModelProperty注解来描述每个字段的意义
/** * 工单详情对象 tb_task_details * * @author itheima * @date 2024-07-22 */ @ApiModel(description = "任务详情对象") public class TaskDetails extends BaseEntity { private static final long serialVersionUID = 1L; /** $column.columnComment */ @ApiModelProperty(value = "任务详情ID", hidden = true) private Long detailsId; /** 工单Id */ @ApiModelProperty(value = "工单Id") @Excel(name = "工单Id") private Long taskId; /** 货道编号 */ @ApiModelProperty(value = "货道编号") @Excel(name = "货道编号") private String channelCode; /** 补货期望容量 */ @ApiModelProperty(value = "补货期望容量") @Excel(name = "补货期望容量") private Long expectCapacity; /** 商品Id */ @ApiModelProperty(value = "商品Id") @Excel(name = "商品Id") private Long skuId; /** $column.columnComment */ @ApiModelProperty(value = "商品名称") @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") private String skuName; /** $column.columnComment */ @ApiModelProperty(value = "商品图片") @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") private String skuImage; }6、接口测试
- 设置文档信息
运营APP
Android模拟器
业务场景: 管理员在后台创建工单后,工作人员可在运营管理App中查看并根据情况选择接受或取消分配给自己的任务
本项目的App客户端部分已经由前端团队进行开发完成,并且以apk的方式提供出来,供我们测试使用,如果要运行apk,需要先安装安卓的模拟器。
- 可以选择国内的安卓模拟器产品,比如:网易mumu、雷电、夜神等。
- 课程中使用网易mumu模拟器,官网地址:http://mumu.172.com/。
- 资料中提供了mumu安装包,大家安装到非中文路径即可。
- 资料中提供了运营管理App端安装包
- 需要让模拟器中的App能够连接我们自己本地代码,需要修改下URL地址:
- http://10.0.2.2是windows本机地址
- 9007是后端服务端口
Java后端
本项目运营管理App的java后端已开发完成,在资料中已提供源码,导入idea中即可
本项目连接的也是dkd数据库,如果密码不是root可以进行修改
功能测试
投放工单
- 帝可得管理端,创建新设备
- 帝可得管理端,创建投放工单
- 运营管理App端登录负责此工单员工,即可查看待办工单,可以选择拒绝、接受
- 如果点击接受,帝可得管理端工单状态改为进行
- 在进行工单界面,可以点击查看详情,选择取消、完成
- 如果点击完成工单,帝可得管理端工单状态改为完成
- 帝可得管理端设备状态改为运营,表示设备投放成功
补货工单
- 帝可得管理端,货道关联商品(大家练习时,可以全部关联)
- 帝可得管理端,创建补货工单
- 运营管理App端登录负责此工单员工,即可查看待办工单,可以选择拒绝、接受
- 如果点击接受,帝可得管理端工单状态改为进行
- 在进行工单界面,可以点击查看详情,选择取消、完成
- 如果点击完成工单,帝可得管理端工单状态改为完成
- 数据库货道表的库存已同步更新
源码介绍
运营管理App的java后端技术栈:SpringBoot+MybatisPlus+阿里云短信
- 员工管理: 发送短信、App登录、查询员工信息
- 工单管理: 查询工单、接受工单、拒绝/取消工单、完成工单
- 工单详情: 根据工单id查询补货详情列表
设备屏幕
业务场景
消费者可以在设备屏幕端查看商品列表-选择支付方式--显示支付二维码--用户扫码完成支付--商品出货
设备屏幕
本项目的设备屏幕客户端部分已经由前端团队进行开发完成,
在资料中已提供源码,双击打开index.html即可
Java后端
本项目设备屏幕端的java后端已开发完成,在资料中已提供源码,导入idea中即可
功能测试
在设备屏幕端加上innerCode=设备编号,即可显示当前设备货道信息
- 帝可得管理端,设备策略分配,设置折扣信息
- 再次访问设备屏幕端,价格就是折扣的了
支付出货流程
我们能够从屏幕上看到支付二维码,其实是经历了“长途跋涉
- 屏幕端实际上是一个H5页面,向后端发起支付请求,订单服务首先会创建订单,然后调用第三方支付来获得用于生成支付二维码的链接。 然后订单微服务将二维码链接返回给屏幕端,屏幕端生成二维码图片展示。
- 用户看到二维码后,拿出手机扫码支付,此时第三方支付平台确认用户支付成功后会回调订单服务。订单服务收到回调信息后修改订单状态,并通知设备发货。
- 设备通过mqtt协议实现网络通信,有兴趣可以看一下emqx框架
- 优雅支付框架: https://gitee.com/myelegent/elegent-pay
源码介绍
设备屏幕端的java后端技术栈:SpringBoot+MybatisPlus
