Compare commits

...
5 Commits
16 changed files with 325 additions and 138 deletions
+1
View File
@@ -0,0 +1 @@
99c37edcf8c5f147315f31bda0fe0628fcae6075fbd9193fb93d228d4aea1d2c
@@ -1,5 +1,6 @@
package com.ruoyi; package com.ruoyi;
import java.util.TimeZone;
import com.ruoyi.common.utils.license.LicenseException; import com.ruoyi.common.utils.license.LicenseException;
import com.ruoyi.common.utils.license.LicenseUtils; import com.ruoyi.common.utils.license.LicenseUtils;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@@ -21,6 +22,8 @@ public class RuoYiApplication
public static void main(String[] args) public static void main(String[] args)
{ {
// System.setProperty("spring.devtools.restart.enabled", "false"); // System.setProperty("spring.devtools.restart.enabled", "false");
// 统一将服务进程默认时区固定为北京时间,避免 Java 时间与数据库会话时间不一致。
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
ConfigurableApplicationContext application = SpringApplication.run(RuoYiApplication.class, args); ConfigurableApplicationContext application = SpringApplication.run(RuoYiApplication.class, args);
verifyLicense(application.getEnvironment()); verifyLicense(application.getEnvironment());
@@ -30,9 +33,18 @@ public class RuoYiApplication
/** /**
* 校验 License 授权,校验失败则退出程序。 * 校验 License 授权,校验失败则退出程序。
*
* <p>本地开发环境可通过 af.license.enabled=false 显式关闭校验,
* 避免调试阶段因为本机授权文件不匹配导致服务无法启动。
*/ */
private static void verifyLicense(Environment env) private static void verifyLicense(Environment env)
{ {
boolean licenseEnabled = Boolean.parseBoolean(env.getProperty("af.license.enabled", "true"));
if (!licenseEnabled)
{
return;
}
String licensePath = env.getProperty("af.license.path"); String licensePath = env.getProperty("af.license.path");
if (licensePath == null || licensePath.trim().isEmpty()) if (licensePath == null || licensePath.trim().isEmpty())
{ {
@@ -13,6 +13,7 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.text.Convert; import com.ruoyi.common.core.text.Convert;
import com.ruoyi.system.domain.af.AfDevice; import com.ruoyi.system.domain.af.AfDevice;
import com.ruoyi.system.domain.af.AfStatusConstants;
import com.ruoyi.system.service.af.IAfDeviceService; import com.ruoyi.system.service.af.IAfDeviceService;
/** /**
@@ -37,6 +38,8 @@ public class AfDeviceController extends BaseController
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(AfDevice device) public TableDataInfo list(AfDevice device)
{ {
// PC 设备管理列表只展示当前在线设备,避免离线设备干扰日常调度。
device.setOnlineStatus(AfStatusConstants.DEVICE_ONLINE);
startPage(); startPage();
List<AfDevice> list = deviceService.selectAfDeviceList(device); List<AfDevice> list = deviceService.selectAfDeviceList(device);
return getDataTable(list); return getDataTable(list);
@@ -58,4 +61,20 @@ public class AfDeviceController extends BaseController
String maintainReason = Convert.toStr(body.get("maintainReason")); String maintainReason = Convert.toStr(body.get("maintainReason"));
return toAjax(deviceService.disableDispatch(deviceId, maintainReason, getUsername())); return toAjax(deviceService.disableDispatch(deviceId, maintainReason, getUsername()));
} }
/**
* PC 端调整设备工作状态,直接复用移动端相同的设备工作状态字段。
*
* @param body 请求体
* @return 更新后的设备
*/
@PreAuthorize("@ss.hasPermi('af:device:edit')")
@PostMapping("/work/status")
public AjaxResult updateWorkStatus(@RequestBody java.util.Map<String, Object> body)
{
String userName = Convert.toStr(body.get("userName"));
String deviceCode = Convert.toStr(body.get("deviceCode"));
String workStatus = Convert.toStr(body.get("workStatus"));
return success(deviceService.updateWorkStatus(userName, deviceCode, workStatus, getUsername()));
}
} }
@@ -1,6 +1,5 @@
package com.ruoyi.web.controller.af; package com.ruoyi.web.controller.af;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -10,10 +9,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.system.domain.af.AfDispatchPolicy; import com.ruoyi.system.domain.af.AfDispatchPolicy;
import com.ruoyi.system.service.af.IAfDispatchPolicyService; import com.ruoyi.system.service.af.IAfDispatchPolicyService;
import com.ruoyi.system.service.af.IAfDispatchService;
@RestController @RestController
@RequestMapping("/af/dispatch") @RequestMapping("/af/dispatch")
@@ -22,9 +19,6 @@ public class AfDispatchController extends BaseController
@Autowired @Autowired
private IAfDispatchPolicyService dispatchPolicyService; private IAfDispatchPolicyService dispatchPolicyService;
@Autowired
private IAfDispatchService dispatchService;
@PreAuthorize("@ss.hasPermi('af:order:dispatch')") @PreAuthorize("@ss.hasPermi('af:order:dispatch')")
@GetMapping("/policy") @GetMapping("/policy")
public AjaxResult getPolicy() public AjaxResult getPolicy()
@@ -39,15 +33,4 @@ public class AfDispatchController extends BaseController
return success(dispatchPolicyService.savePolicy(policy, getUsername())); return success(dispatchPolicyService.savePolicy(policy, getUsername()));
} }
@PreAuthorize("@ss.hasPermi('af:order:dispatch')")
@PostMapping("/trigger")
public AjaxResult trigger(@RequestBody(required = false) Map<String, Object> body)
{
Integer limit = body == null ? null : Convert.toInt(body.get("limit"));
String triggerType = body == null ? null : Convert.toStr(body.get("triggerType"));
Map<String, Object> triggerResult = dispatchService.triggerAutoDispatch(limit, triggerType, getUsername());
AjaxResult result = success();
result.putAll(triggerResult);
return result;
}
} }
@@ -77,24 +77,6 @@ public class AfOrderController extends BaseController
return success(dispatchService.manualDispatch(orderId, deviceId, getUsername())); return success(dispatchService.manualDispatch(orderId, deviceId, getUsername()));
} }
/**
* 手动触发一轮自动分发。
*
* @param body 请求体
* @return 触发结果
*/
@PreAuthorize("@ss.hasPermi('af:order:dispatch')")
@PostMapping("/dispatch/auto")
public AjaxResult autoDispatch(@RequestBody(required = false) Map<String, Object> body)
{
Integer limit = body == null ? null : Convert.toInt(body.get("limit"));
String triggerType = body == null ? null : Convert.toStr(body.get("triggerType"));
Map<String, Object> triggerResult = dispatchService.triggerAutoDispatch(limit, triggerType, getUsername());
AjaxResult result = success();
result.putAll(triggerResult);
return result;
}
/** /**
* 取消订单。 * 取消订单。
* *
@@ -111,17 +93,31 @@ public class AfOrderController extends BaseController
} }
/** /**
* 重试订单。 * 人工标记订单完成。
* *
* @param body 请求体 * @param body 请求体
* @return 处理结果 * @return 处理结果
*/ */
@PreAuthorize("@ss.hasPermi('af:order:retry')") @PreAuthorize("@ss.hasPermi('af:order:complete')")
@PostMapping("/retry") @PostMapping("/complete")
public AjaxResult retry(@RequestBody Map<String, Object> body) public AjaxResult complete(@RequestBody Map<String, Object> body)
{ {
Long orderId = Convert.toLong(body.get("orderId")); Long orderId = Convert.toLong(body.get("orderId"));
return toAjax(orderService.retryOrder(orderId, getUsername())); return toAjax(orderService.completeOrder(orderId, getUsername()));
}
/**
* 人工重置订单状态为待分发。
*
* @param body 请求体
* @return 处理结果
*/
@PreAuthorize("@ss.hasPermi('af:order:reset')")
@PostMapping("/reset")
public AjaxResult reset(@RequestBody Map<String, Object> body)
{
Long orderId = Convert.toLong(body.get("orderId"));
return toAjax(orderService.resetOrder(orderId, getUsername()));
} }
/** /**
@@ -17,9 +17,10 @@ spring:
username: username:
password: password:
# 初始连接数 # 初始连接数
initialSize: 5 initialSize: 5
# 最小连接池数量 connectionInitSqls: SET time_zone = '+08:00'
minIdle: 10 # 最小连接池数量
minIdle: 10
# 最大连接池数量 # 最大连接池数量
maxActive: 20 maxActive: 20
# 配置获取连接等待超时的时间 # 配置获取连接等待超时的时间
@@ -58,4 +59,4 @@ spring:
merge-sql: true merge-sql: true
wall: wall:
config: config:
multi-statement-allow: true multi-statement-allow: true
+17 -13
View File
@@ -46,9 +46,9 @@ user:
lockTime: 10 lockTime: 10
# Spring配置 # Spring配置
spring: spring:
# 资源信息 # 资源信息
messages: messages:
# 国际化资源文件路径 # 国际化资源文件路径
basename: i18n/messages basename: i18n/messages
profiles: profiles:
@@ -66,7 +66,7 @@ spring:
# 热部署开关 # 热部署开关
enabled: true enabled: true
# redis 配置 # redis 配置
redis: redis:
# 地址 # 地址
host: 38.76.220.172 host: 38.76.220.172
# 端口,默认为6379 # 端口,默认为6379
@@ -77,16 +77,19 @@ spring:
password: fenghp123456 password: fenghp123456
# 连接超时时间 # 连接超时时间
timeout: 10s timeout: 10s
lettuce: lettuce:
pool: pool:
# 连接池中的最小空闲连接 # 连接池中的最小空闲连接
min-idle: 0 min-idle: 0
# 连接池中的最大空闲连接 # 连接池中的最大空闲连接
max-idle: 8 max-idle: 8
# 连接池的最大数据库连接数 # 连接池的最大数据库连接数
max-active: 8 max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制) # #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms max-wait: -1ms
jackson:
time-zone: Asia/Shanghai
date-format: yyyy-MM-dd HH:mm:ss
# token配置 # token配置
token: token:
@@ -127,10 +130,11 @@ referer:
allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip
# 自动充值业务配置 # 自动充值业务配置
af: af:
license: license:
path: D:\license\license.lic enabled: false
rechargeUrl: https://www.douyin.com/pay path: /Users/fenghp/Desktop/DEV/project/autoFill/RuoYi-Vue/license.lic
rechargeUrl: https://www.douyin.com/pay
weidian: weidian:
wdToken: e96014f3 wdToken: e96014f3
cookie: __spider__visitorid=884e5806506186bd; is_login=true; login_type=LOGIN_USER_TYPE_MASTER; login_source=LOGIN_USER_SOURCE_MASTER; uid=1774188581; duid=1774188581; sid=1619718241; smart_login_type=0; hi_dxh=; hold=; cn_merchant=; wdtoken=e96014f3; __spider__sessionid=d7756539bef26b5b; Hm_lvt_f3b91484e26c0d850ada494bff4b469b=1777446699,1778986671; HMACCOUNT=2383A5C40E664A02; login_token=_EwWqqVIQEKuI-Phw6hSEGFcEi9mlSZQq9CGPxfyUsL_A0Fnptw_8BdSEQvLn8r8oOo73sACMqrfJj3n0ichiWNcQwt0cS2Yxh1UgqGCs8bE5zqWBCXV2KZIxKzMApQbuf98L9hOdI1xlZH0pIJ6MT7WXbckhMUaS0juJhzzDpn1YnXxUAB9jy2G7fb2f15UK6_Thhr-eLwj54zYDhTghIixzfpTsmAVvlE_45xetReIESH7SYsHeO1JGiRIEey3yDnlo_-jJ; Hm_lpvt_f3b91484e26c0d850ada494bff4b469b=1778988001; cashier_token=558d8084586ea5a1ecf74c1d372296d5 cookie: __spider__visitorid=884e5806506186bd; is_login=true; login_type=LOGIN_USER_TYPE_MASTER; login_source=LOGIN_USER_SOURCE_MASTER; uid=1774188581; duid=1774188581; sid=1619718241; smart_login_type=0; hi_dxh=; hold=; cn_merchant=; wdtoken=e96014f3; __spider__sessionid=d7756539bef26b5b; Hm_lvt_f3b91484e26c0d850ada494bff4b469b=1777446699,1778986671; HMACCOUNT=2383A5C40E664A02; login_token=_EwWqqVIQEKuI-Phw6hSEGFcEi9mlSZQq9CGPxfyUsL_A0Fnptw_8BdSEQvLn8r8oOo73sACMqrfJj3n0ichiWNcQwt0cS2Yxh1UgqGCs8bE5zqWBCXV2KZIxKzMApQbuf98L9hOdI1xlZH0pIJ6MT7WXbckhMUaS0juJhzzDpn1YnXxUAB9jy2G7fb2f15UK6_Thhr-eLwj54zYDhTghIixzfpTsmAVvlE_45xetReIESH7SYsHeO1JGiRIEey3yDnlo_-jJ; Hm_lpvt_f3b91484e26c0d850ada494bff4b469b=1778988001; cashier_token=558d8084586ea5a1ecf74c1d372296d5
@@ -3,22 +3,27 @@ package com.ruoyi.common.utils;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Date; import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils; import java.util.TimeZone;
import org.apache.commons.lang3.time.DateFormatUtils;
/** /**
* 时间工具类 * 时间工具类
* *
* @author ruoyi * @author ruoyi
*/ */
public class DateUtils extends org.apache.commons.lang3.time.DateUtils public class DateUtils extends org.apache.commons.lang3.time.DateUtils
{ {
public static String YYYY = "yyyy"; private static final ZoneId BEIJING_ZONE_ID = ZoneId.of("Asia/Shanghai");
private static final TimeZone BEIJING_TIME_ZONE = TimeZone.getTimeZone(BEIJING_ZONE_ID);
public static String YYYY = "yyyy";
public static String YYYY_MM = "yyyy-MM"; public static String YYYY_MM = "yyyy-MM";
@@ -38,10 +43,10 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
* *
* @return Date() 当前日期 * @return Date() 当前日期
*/ */
public static Date getNowDate() public static Date getNowDate()
{ {
return new Date(); return Date.from(ZonedDateTime.now(BEIJING_ZONE_ID).toInstant());
} }
/** /**
* 获取当前日期, 默认格式为yyyy-MM-dd * 获取当前日期, 默认格式为yyyy-MM-dd
@@ -63,27 +68,31 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
return dateTimeNow(YYYYMMDDHHMMSS); return dateTimeNow(YYYYMMDDHHMMSS);
} }
public static final String dateTimeNow(final String format) public static final String dateTimeNow(final String format)
{ {
return parseDateToStr(format, new Date()); return parseDateToStr(format, getNowDate());
} }
public static final String dateTime(final Date date) public static final String dateTime(final Date date)
{ {
return parseDateToStr(YYYY_MM_DD, date); return parseDateToStr(YYYY_MM_DD, date);
} }
public static final String parseDateToStr(final String format, final Date date) public static final String parseDateToStr(final String format, final Date date)
{ {
return new SimpleDateFormat(format).format(date); SimpleDateFormat formatter = new SimpleDateFormat(format);
} formatter.setTimeZone(BEIJING_TIME_ZONE);
return formatter.format(date);
}
public static final Date dateTime(final String format, final String ts) public static final Date dateTime(final String format, final String ts)
{ {
try try
{ {
return new SimpleDateFormat(format).parse(ts); SimpleDateFormat formatter = new SimpleDateFormat(format);
} formatter.setTimeZone(BEIJING_TIME_ZONE);
return formatter.parse(ts);
}
catch (ParseException e) catch (ParseException e)
{ {
throw new RuntimeException(e); throw new RuntimeException(e);
@@ -93,20 +102,20 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
/** /**
* 日期路径 即年/月/日 如2018/08/08 * 日期路径 即年/月/日 如2018/08/08
*/ */
public static final String datePath() public static final String datePath()
{ {
Date now = new Date(); Date now = getNowDate();
return DateFormatUtils.format(now, "yyyy/MM/dd"); return DateFormatUtils.format(now, "yyyy/MM/dd");
} }
/** /**
* 日期路径 即年/月/日 如20180808 * 日期路径 即年/月/日 如20180808
*/ */
public static final String dateTime() public static final String dateTime()
{ {
Date now = new Date(); Date now = getNowDate();
return DateFormatUtils.format(now, "yyyyMMdd"); return DateFormatUtils.format(now, "yyyyMMdd");
} }
/** /**
* 日期型字符串转化为日期 格式 * 日期型字符串转化为日期 格式
@@ -173,19 +182,19 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
/** /**
* 增加 LocalDateTime ==> Date * 增加 LocalDateTime ==> Date
*/ */
public static Date toDate(LocalDateTime temporalAccessor) public static Date toDate(LocalDateTime temporalAccessor)
{ {
ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); ZonedDateTime zdt = temporalAccessor.atZone(BEIJING_ZONE_ID);
return Date.from(zdt.toInstant()); return Date.from(zdt.toInstant());
} }
/** /**
* 增加 LocalDate ==> Date * 增加 LocalDate ==> Date
*/ */
public static Date toDate(LocalDate temporalAccessor) public static Date toDate(LocalDate temporalAccessor)
{ {
LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); ZonedDateTime zdt = localDateTime.atZone(BEIJING_ZONE_ID);
return Date.from(zdt.toInstant()); return Date.from(zdt.toInstant());
} }
} }
@@ -100,8 +100,8 @@ public class AfStatusConstants
/** 自动分发触发类型:定时扫描 */ /** 自动分发触发类型:定时扫描 */
public static final String DISPATCH_TRIGGER_CRON = "CRON"; public static final String DISPATCH_TRIGGER_CRON = "CRON";
/** 心跳超时分钟 */ /** 心跳超时秒数 */
public static final int HEARTBEAT_TIMEOUT_MINUTES = 2; public static final int HEARTBEAT_TIMEOUT_SECONDS = 30;
/** 自动分发单次最大扫描数 */ /** 自动分发单次最大扫描数 */
public static final int AUTO_DISPATCH_LIMIT = 20; public static final int AUTO_DISPATCH_LIMIT = 20;
@@ -191,11 +191,19 @@ public interface AfDeviceMapper
*/ */
public int countActiveTaskByDeviceId(@Param("deviceId") Long deviceId); public int countActiveTaskByDeviceId(@Param("deviceId") Long deviceId);
/**
* 查询心跳超时仍标记为在线的设备。
*
* @param timeoutSeconds 心跳超时秒数
* @return 设备列表
*/
public List<AfDevice> selectHeartbeatTimeoutOnlineList(@Param("timeoutSeconds") int timeoutSeconds);
/** /**
* 批量刷新离线状态 * 批量刷新离线状态
* *
* @param timeoutMinutes 心跳超时分钟数 * @param timeoutSeconds 心跳超时秒数
* @return 结果 * @return 结果
*/ */
public int refreshOfflineStatus(@Param("timeoutMinutes") int timeoutMinutes); public int refreshOfflineStatus(@Param("timeoutSeconds") int timeoutSeconds);
} }
@@ -38,13 +38,22 @@ public interface IAfOrderService
public int cancelOrder(Long orderId, String failReason, String operator); public int cancelOrder(Long orderId, String failReason, String operator);
/** /**
* 重试订单 * 人工标记订单完成
* *
* @param orderId 订单ID * @param orderId 订单ID
* @param operator 操作人 * @param operator 操作人
* @return 结果 * @return 结果
*/ */
public int retryOrder(Long orderId, String operator); public int completeOrder(Long orderId, String operator);
/**
* 人工重置订单为待分发
*
* @param orderId 订单ID
* @param operator 操作人
* @return 结果
*/
public int resetOrder(Long orderId, String operator);
/** /**
* 从微店拉取待充值订单并同步入库。 * 从微店拉取待充值订单并同步入库。
@@ -1,10 +1,14 @@
package com.ruoyi.system.service.af.impl; package com.ruoyi.system.service.af.impl;
import java.util.Collection;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
@@ -30,6 +34,9 @@ public class AfDeviceServiceImpl implements IAfDeviceService
@Autowired @Autowired
private AfDispatchTaskMapper taskMapper; private AfDispatchTaskMapper taskMapper;
@Autowired
private RedisCache redisCache;
@Lazy @Lazy
@Autowired @Autowired
private IAfDispatchService dispatchService; private IAfDispatchService dispatchService;
@@ -347,6 +354,56 @@ public class AfDeviceServiceImpl implements IAfDeviceService
@Override @Override
public void refreshOfflineStatus() public void refreshOfflineStatus()
{ {
deviceMapper.refreshOfflineStatus(AfStatusConstants.HEARTBEAT_TIMEOUT_MINUTES); List<AfDevice> timeoutDeviceList = deviceMapper.selectHeartbeatTimeoutOnlineList(AfStatusConstants.HEARTBEAT_TIMEOUT_SECONDS);
deviceMapper.refreshOfflineStatus(AfStatusConstants.HEARTBEAT_TIMEOUT_SECONDS);
clearLoginTokenByTimeoutDevice(timeoutDeviceList);
}
/**
* 心跳超时后主动清理该账号在线 token,避免离线设备继续持有登录态。
*
* @param timeoutDeviceList 超时设备列表
*/
private void clearLoginTokenByTimeoutDevice(List<AfDevice> timeoutDeviceList)
{
if (timeoutDeviceList == null || timeoutDeviceList.isEmpty())
{
return;
}
Collection<String> tokenKeys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
if (tokenKeys == null || tokenKeys.isEmpty())
{
return;
}
for (AfDevice device : timeoutDeviceList)
{
String userName = device.getUserName();
if (StringUtils.isEmpty(userName))
{
continue;
}
clearLoginTokenByUserName(tokenKeys, userName);
}
}
/**
* 按账号名清理在线 token,保证心跳超时后重新登录才能继续使用移动端。
*
* @param tokenKeys 在线 token key 集合
* @param userName 登录账号
*/
private void clearLoginTokenByUserName(Collection<String> tokenKeys, String userName)
{
for (String tokenKey : tokenKeys)
{
LoginUser loginUser = redisCache.getCacheObject(tokenKey);
if (loginUser == null || !StringUtils.equals(userName, loginUser.getUsername()))
{
continue;
}
redisCache.deleteObject(tokenKey);
}
} }
} }
@@ -7,6 +7,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.ruoyi.system.domain.af.AfDispatchPolicy; import com.ruoyi.system.domain.af.AfDispatchPolicy;
import com.ruoyi.system.service.af.IAfDeviceService;
import com.ruoyi.system.service.af.IAfDispatchPolicyService; import com.ruoyi.system.service.af.IAfDispatchPolicyService;
import com.ruoyi.system.service.af.IAfDispatchService; import com.ruoyi.system.service.af.IAfDispatchService;
import com.ruoyi.system.service.af.IAfOrderService; import com.ruoyi.system.service.af.IAfOrderService;
@@ -28,6 +29,9 @@ public class AfDispatchScheduler
@Autowired @Autowired
private IAfOrderService orderService; private IAfOrderService orderService;
@Autowired
private IAfDeviceService deviceService;
@Scheduled(fixedDelay = 10000L, initialDelay = 15000L) @Scheduled(fixedDelay = 10000L, initialDelay = 15000L)
public void scanPendingOrders() public void scanPendingOrders()
{ {
@@ -53,4 +57,20 @@ public class AfDispatchScheduler
} }
dispatchService.triggerAutoDispatch(null, "CRON", "system"); dispatchService.triggerAutoDispatch(null, "CRON", "system");
} }
/**
* 定时刷新超时设备的在线状态,避免无分发动作时设备一直停留在线。
*/
@Scheduled(fixedDelay = 10000L, initialDelay = 15000L)
public void refreshOfflineDevices()
{
try
{
deviceService.refreshOfflineStatus();
}
catch (Exception e)
{
log.error("定时刷新设备离线状态失败", e);
}
}
} }
@@ -131,7 +131,7 @@ public class AfOrderServiceImpl implements IAfOrderService
} }
/** /**
* 重试订单。 * 人工标记订单完成。
* *
* @param orderId 订单ID * @param orderId 订单ID
* @param operator 操作人 * @param operator 操作人
@@ -139,36 +139,95 @@ public class AfOrderServiceImpl implements IAfOrderService
*/ */
@Override @Override
@Transactional @Transactional
public int retryOrder(Long orderId, String operator) public int completeOrder(Long orderId, String operator)
{ {
AfOrder order = orderMapper.selectAfOrderByIdForUpdate(orderId); AfOrder order = orderMapper.selectAfOrderByIdForUpdate(orderId);
if (StringUtils.isNull(order)) if (StringUtils.isNull(order))
{ {
throw new ServiceException("订单不存在"); throw new ServiceException("订单不存在");
} }
if (StringUtils.equals(order.getStatus(), AfStatusConstants.ORDER_CANCELLED))
if (!StringUtils.equalsAny(order.getStatus(), AfStatusConstants.ORDER_FAILED, AfStatusConstants.ORDER_CANCELLED))
{ {
throw new ServiceException("只有失败或取消订单支持重试"); throw new ServiceException("已取消订单不允许标记完成");
} }
Long oldTaskId = order.getCurrentTaskId(); orderMapper.updateOrderStatusById(orderId, AfStatusConstants.ORDER_SUCCESS, null, operator);
orderMapper.resetOrderToPending(orderId, operator); releaseCurrentTask(order, AfStatusConstants.TASK_SUCCESS, null, operator);
deliverOrderIfNeed(order, operator);
if (oldTaskId != null)
{
AfDispatchTask task = taskMapper.selectAfDispatchTaskByIdForUpdate(oldTaskId);
if (StringUtils.isNotNull(task) && task.getDeviceId() != null)
{
deviceMapper.updateDeviceToIdle(task.getDeviceId(), oldTaskId, operator);
}
}
dispatchService.onOrderPending(orderId);
return 1; return 1;
} }
/**
* 人工重置订单为待分发。
*
* @param orderId 订单ID
* @param operator 操作人
* @return 结果
*/
@Override
@Transactional
public int resetOrder(Long orderId, String operator)
{
AfOrder order = orderMapper.selectAfOrderByIdForUpdate(orderId);
if (StringUtils.isNull(order))
{
throw new ServiceException("订单不存在");
}
if (StringUtils.equals(order.getStatus(), AfStatusConstants.ORDER_SUCCESS))
{
throw new ServiceException("已完成订单不允许重置");
}
orderMapper.resetOrderToPending(orderId, operator);
releaseCurrentTask(order, AfStatusConstants.TASK_CANCELLED, "人工重置订单状态", operator);
return 1;
}
/**
* 人工改订单状态后释放当前任务和设备占用。
*
* @param order 订单
* @param taskStatus 任务目标状态
* @param failReason 任务原因
* @param operator 操作人
*/
private void releaseCurrentTask(AfOrder order, String taskStatus, String failReason, String operator)
{
if (order.getCurrentTaskId() == null)
{
return;
}
AfDispatchTask task = taskMapper.selectAfDispatchTaskByIdForUpdate(order.getCurrentTaskId());
if (StringUtils.isNull(task))
{
return;
}
if (StringUtils.equalsAny(task.getStatus(), AfStatusConstants.TASK_CREATED, AfStatusConstants.TASK_PUSHED,
AfStatusConstants.TASK_RECEIVED, AfStatusConstants.TASK_PROCESSING))
{
taskMapper.updateTaskStatusById(task.getId(), taskStatus, failReason, operator);
}
if (task.getDeviceId() != null)
{
deviceMapper.updateDeviceToIdle(task.getDeviceId(), task.getId(), operator);
}
}
/**
* 订单完成后同步通知外部来源订单。
*
* @param order 订单
* @param operator 操作人
*/
private void deliverOrderIfNeed(AfOrder order, String operator)
{
if (StringUtils.equals(order.getSource(), SOURCE_WEIDIAN))
{
deliverWeidianOrder(order.getSourceOrderNo(), operator);
}
}
/** /**
* 从微店拉取待充值订单并同步入库。 * 从微店拉取待充值订单并同步入库。
* 调用微店 seller.getOrderListForPC 接口,解析订单列表后逐条 INSERT IGNORE 写入。 * 调用微店 seller.getOrderListForPC 接口,解析订单列表后逐条 INSERT IGNORE 写入。
@@ -112,7 +112,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
create_by, create_time, update_by, update_time, remark create_by, create_time, update_by, update_time, remark
) values ( ) values (
#{userName}, #{deviceCode}, #{onlineStatus}, #{taskStatus}, #{currentTaskId}, #{userName}, #{deviceCode}, #{onlineStatus}, #{taskStatus}, #{currentTaskId},
#{dispatchEnabled}, #{workStatus}, #{idleTime}, #{maintainReason}, #{lastHeartbeatTime}, #{dispatchEnabled}, #{workStatus}, #{idleTime}, #{maintainReason}, sysdate(),
#{createBy}, sysdate(), #{updateBy}, sysdate(), #{remark} #{createBy}, sysdate(), #{updateBy}, sysdate(), #{remark}
) )
</insert> </insert>
@@ -129,7 +129,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="idleTime != null">idle_time = #{idleTime},</if> <if test="idleTime != null">idle_time = #{idleTime},</if>
<if test="maintainReason != null">maintain_reason = #{maintainReason},</if> <if test="maintainReason != null">maintain_reason = #{maintainReason},</if>
<if test="cooldownUntil != null">cooldown_until = #{cooldownUntil},</if> <if test="cooldownUntil != null">cooldown_until = #{cooldownUntil},</if>
<if test="lastHeartbeatTime != null">last_heartbeat_time = #{lastHeartbeatTime},</if> <if test="lastHeartbeatTime != null">last_heartbeat_time = sysdate(),</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if> <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if> <if test="remark != null">remark = #{remark},</if>
update_time = sysdate() update_time = sysdate()
@@ -233,13 +233,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and status in ('CREATED', 'PUSHED', 'RECEIVED', 'PROCESSING') and status in ('CREATED', 'PUSHED', 'RECEIVED', 'PROCESSING')
</select> </select>
<select id="selectHeartbeatTimeoutOnlineList" resultMap="AfDeviceResult">
<include refid="selectAfDeviceVo"/>
where online_status = 'ONLINE'
and last_heartbeat_time is not null
and last_heartbeat_time &lt; date_sub(now(), interval #{timeoutSeconds} second)
</select>
<update id="refreshOfflineStatus"> <update id="refreshOfflineStatus">
update af_device update af_device
set online_status = 'OFFLINE', set online_status = 'OFFLINE',
work_status = 'NOT_READY',
update_time = sysdate() update_time = sysdate()
where online_status = 'ONLINE' where online_status = 'ONLINE'
and last_heartbeat_time is not null and last_heartbeat_time is not null
and last_heartbeat_time &lt; date_sub(now(), interval #{timeoutMinutes} minute) and last_heartbeat_time &lt; date_sub(now(), interval #{timeoutSeconds} second)
</update> </update>
</mapper> </mapper>
+3 -2
View File
@@ -158,8 +158,9 @@ INSERT INTO sys_menu
VALUES VALUES
('订单查询', @AF_ORDER_ID, 1, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:query', '#', 'admin', NOW(), ''), ('订单查询', @AF_ORDER_ID, 1, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:query', '#', 'admin', NOW(), ''),
('订单分发', @AF_ORDER_ID, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:dispatch', '#', 'admin', NOW(), ''), ('订单分发', @AF_ORDER_ID, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:dispatch', '#', 'admin', NOW(), ''),
('订单重试', @AF_ORDER_ID, 3, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:retry', '#', 'admin', NOW(), ''), ('标记完成', @AF_ORDER_ID, 3, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:complete','#', 'admin', NOW(), ''),
('订单取消', @AF_ORDER_ID, 4, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:cancel', '#', 'admin', NOW(), ''), ('重置状态', @AF_ORDER_ID, 4, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:reset', '#', 'admin', NOW(), ''),
('订单取消', @AF_ORDER_ID, 5, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:order:cancel', '#', 'admin', NOW(), ''),
('账号设备查询', @AF_DEVICE_ID, 1, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:device:query', '#', 'admin', NOW(), ''), ('账号设备查询', @AF_DEVICE_ID, 1, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:device:query', '#', 'admin', NOW(), ''),
('账号设备修改', @AF_DEVICE_ID, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:device:edit', '#', 'admin', NOW(), ''), ('账号设备修改', @AF_DEVICE_ID, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:device:edit', '#', 'admin', NOW(), ''),
('任务查询', @AF_TASK_ID, 1, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:task:query', '#', 'admin', NOW(), ''), ('任务查询', @AF_TASK_ID, 1, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'af:task:query', '#', 'admin', NOW(), ''),