[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

更多推荐


所有评论(0)