sms
This commit is contained in:
parent
bcf4875336
commit
66658793de
|
@ -45,4 +45,5 @@ public interface ErrorCodeConstants {
|
|||
|
||||
ErrorCode SUPERIOR_API_LOG_NOT_EXISTS = new ErrorCode(1_823_003_005, "订单接口记录不存在");
|
||||
ErrorCode BLACK_LIST_NOT_EXISTS = new ErrorCode(1_824_003_005, "黑名单不存在");
|
||||
ErrorCode SMS_TASK_NOT_EXISTS = new ErrorCode(1_824_123_005, "短信任务不存在");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package cn.iocoder.yudao.module.haoka.controller.admin.smstask;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import cn.iocoder.yudao.module.haoka.controller.admin.smstask.vo.*;
|
||||
import cn.iocoder.yudao.module.haoka.dal.dataobject.smstask.SmsTaskDO;
|
||||
import cn.iocoder.yudao.module.haoka.service.smstask.SmsTaskService;
|
||||
|
||||
@Tag(name = "管理后台 - 短信任务")
|
||||
@RestController
|
||||
@RequestMapping("/haoka/sms-task")
|
||||
@Validated
|
||||
public class SmsTaskController {
|
||||
|
||||
@Resource
|
||||
private SmsTaskService smsTaskService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建短信任务")
|
||||
@PreAuthorize("@ss.hasPermission('haoka:sms-task:create')")
|
||||
public CommonResult<Long> createSmsTask(@Valid @RequestBody SmsTaskSaveReqVO createReqVO) {
|
||||
return success(smsTaskService.createSmsTask(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新短信任务")
|
||||
@PreAuthorize("@ss.hasPermission('haoka:sms-task:update')")
|
||||
public CommonResult<Boolean> updateSmsTask(@Valid @RequestBody SmsTaskSaveReqVO updateReqVO) {
|
||||
smsTaskService.updateSmsTask(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除短信任务")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('haoka:sms-task:delete')")
|
||||
public CommonResult<Boolean> deleteSmsTask(@RequestParam("id") Long id) {
|
||||
smsTaskService.deleteSmsTask(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得短信任务")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('haoka:sms-task:query')")
|
||||
public CommonResult<SmsTaskRespVO> getSmsTask(@RequestParam("id") Long id) {
|
||||
SmsTaskDO smsTask = smsTaskService.getSmsTask(id);
|
||||
return success(BeanUtils.toBean(smsTask, SmsTaskRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得短信任务分页")
|
||||
@PreAuthorize("@ss.hasPermission('haoka:sms-task:query')")
|
||||
public CommonResult<PageResult<SmsTaskRespVO>> getSmsTaskPage(@Valid SmsTaskPageReqVO pageReqVO) {
|
||||
PageResult<SmsTaskDO> pageResult = smsTaskService.getSmsTaskPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, SmsTaskRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出短信任务 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('haoka:sms-task:export')")
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportSmsTaskExcel(@Valid SmsTaskPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<SmsTaskDO> list = smsTaskService.getSmsTaskPage(pageReqVO).getList();
|
||||
// 导出 Excel
|
||||
ExcelUtils.write(response, "短信任务.xls", "数据", SmsTaskRespVO.class,
|
||||
BeanUtils.toBean(list, SmsTaskRespVO.class));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package cn.iocoder.yudao.module.haoka.controller.admin.smstask.vo;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 短信任务分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class SmsTaskPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "任务名称", example = "张三")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "订单状态", example = "1")
|
||||
private Long orderStatus;
|
||||
|
||||
@Schema(description = "订单来源")
|
||||
private String orderSourceList;
|
||||
|
||||
@Schema(description = "商品名称")
|
||||
private String orderOnSaleProductList;
|
||||
|
||||
@Schema(description = "是否外呼", example = "1")
|
||||
private Integer callStatus;
|
||||
|
||||
@Schema(description = "发送时间段")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private Integer[] sendTime;
|
||||
|
||||
@Schema(description = "短信渠道", example = "6730")
|
||||
private Long smsChannelId;
|
||||
|
||||
@Schema(description = "短信类型", example = "2")
|
||||
private Integer smsType;
|
||||
|
||||
@Schema(description = "短信内容", example = "4943")
|
||||
private String smsTemplateCode;
|
||||
|
||||
@Schema(description = "测试手机")
|
||||
private String testPhone;
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package cn.iocoder.yudao.module.haoka.controller.admin.smstask.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import com.alibaba.excel.annotation.*;
|
||||
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
|
||||
|
||||
@Schema(description = "管理后台 - 短信任务 Response VO")
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
public class SmsTaskRespVO {
|
||||
|
||||
@Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11858")
|
||||
@ExcelProperty("ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
|
||||
@ExcelProperty("任务名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "订单状态", example = "1")
|
||||
@ExcelProperty(value = "订单状态", converter = DictConvert.class)
|
||||
@DictFormat("haoka_order_status") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private Long orderStatus;
|
||||
|
||||
@Schema(description = "订单来源")
|
||||
@ExcelProperty("订单来源")
|
||||
private String orderSourceList;
|
||||
|
||||
@Schema(description = "商品名称")
|
||||
@ExcelProperty("商品名称")
|
||||
private String orderOnSaleProductList;
|
||||
|
||||
@Schema(description = "是否外呼", example = "1")
|
||||
@ExcelProperty("是否外呼")
|
||||
private Integer callStatus;
|
||||
|
||||
@Schema(description = "发送时间段", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@ExcelProperty("发送时间段")
|
||||
private Integer sendTime;
|
||||
|
||||
@Schema(description = "短信渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "6730")
|
||||
@ExcelProperty("短信渠道")
|
||||
private Long smsChannelId;
|
||||
|
||||
@Schema(description = "短信类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
@ExcelProperty("短信类型")
|
||||
private Integer smsType;
|
||||
|
||||
@Schema(description = "短信内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "4943")
|
||||
@ExcelProperty("短信内容")
|
||||
private String smsTemplateCode;
|
||||
|
||||
@Schema(description = "测试手机")
|
||||
@ExcelProperty("测试手机")
|
||||
private String testPhone;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@ExcelProperty(value = "是否启用", converter = DictConvert.class)
|
||||
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private Boolean active;
|
||||
|
||||
@Schema(description = "退款状态", example = "1")
|
||||
@ExcelProperty(value = "退款状态", converter = DictConvert.class)
|
||||
@DictFormat("haoka_order_refund_status") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
private String refundStatus;
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package cn.iocoder.yudao.module.haoka.controller.admin.smstask.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "管理后台 - 短信任务新增/修改 Request VO")
|
||||
@Data
|
||||
public class SmsTaskSaveReqVO {
|
||||
|
||||
@Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11858")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
|
||||
@NotEmpty(message = "任务名称不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "订单状态", example = "1")
|
||||
private Long orderStatus;
|
||||
|
||||
@Schema(description = "订单来源")
|
||||
private String orderSourceList;
|
||||
|
||||
@Schema(description = "商品名称")
|
||||
private String orderOnSaleProductList;
|
||||
|
||||
@Schema(description = "是否外呼", example = "1")
|
||||
private Integer callStatus;
|
||||
|
||||
@Schema(description = "发送时间段", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "发送时间段不能为空")
|
||||
private Integer sendTime;
|
||||
|
||||
@Schema(description = "短信渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "6730")
|
||||
@NotNull(message = "短信渠道不能为空")
|
||||
private Long smsChannelId;
|
||||
|
||||
@Schema(description = "短信类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
@NotNull(message = "短信类型不能为空")
|
||||
private Integer smsType;
|
||||
|
||||
@Schema(description = "短信内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "4943")
|
||||
@NotNull(message = "短信内容不能为空")
|
||||
private String smsTemplateCode;
|
||||
|
||||
@Schema(description = "测试手机")
|
||||
private String testPhone;
|
||||
|
||||
@Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "是否启用不能为空")
|
||||
private Boolean active;
|
||||
|
||||
@Schema(description = "退款状态", example = "1")
|
||||
private String refundStatus;
|
||||
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package cn.iocoder.yudao.module.haoka.dal.dataobject.smstask;
|
||||
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
|
||||
/**
|
||||
* 短信任务 DO
|
||||
*
|
||||
* @author 超级管理员
|
||||
*/
|
||||
@TableName("haoka_sms_task")
|
||||
@KeySequence("haoka_sms_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SmsTaskDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 任务名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 订单状态
|
||||
*
|
||||
* 枚举 {@link TODO haoka_order_status 对应的类}
|
||||
*/
|
||||
private Long orderStatus;
|
||||
/**
|
||||
* 订单来源
|
||||
*/
|
||||
private String orderSourceList;
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
private String orderOnSaleProductList;
|
||||
/**
|
||||
* 是否外呼
|
||||
*/
|
||||
private Integer callStatus;
|
||||
/**
|
||||
* 发送时间段
|
||||
*/
|
||||
private Integer sendTime;
|
||||
/**
|
||||
* 短信渠道
|
||||
*/
|
||||
private Long smsChannelId;
|
||||
/**
|
||||
* 短信类型
|
||||
*/
|
||||
private Integer smsType;
|
||||
/**
|
||||
* 短信内容
|
||||
*/
|
||||
private String smsTemplateCode;
|
||||
/**
|
||||
* 测试手机
|
||||
*/
|
||||
private String testPhone;
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* 枚举 {@link TODO infra_boolean_string 对应的类}
|
||||
*/
|
||||
private Boolean active;
|
||||
/**
|
||||
* 退款状态
|
||||
*
|
||||
* 枚举 {@link TODO haoka_order_refund_status 对应的类}
|
||||
*/
|
||||
private String refundStatus;
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package cn.iocoder.yudao.module.haoka.dal.mysql.smstask;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.haoka.dal.dataobject.smstask.SmsTaskDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import cn.iocoder.yudao.module.haoka.controller.admin.smstask.vo.*;
|
||||
|
||||
/**
|
||||
* 短信任务 Mapper
|
||||
*
|
||||
* @author 超级管理员
|
||||
*/
|
||||
@Mapper
|
||||
public interface SmsTaskMapper extends BaseMapperX<SmsTaskDO> {
|
||||
|
||||
default PageResult<SmsTaskDO> selectPage(SmsTaskPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<SmsTaskDO>()
|
||||
.likeIfPresent(SmsTaskDO::getName, reqVO.getName())
|
||||
.eqIfPresent(SmsTaskDO::getOrderStatus, reqVO.getOrderStatus())
|
||||
.eqIfPresent(SmsTaskDO::getOrderSourceList, reqVO.getOrderSourceList())
|
||||
.eqIfPresent(SmsTaskDO::getOrderOnSaleProductList, reqVO.getOrderOnSaleProductList())
|
||||
.eqIfPresent(SmsTaskDO::getCallStatus, reqVO.getCallStatus())
|
||||
.betweenIfPresent(SmsTaskDO::getSendTime, reqVO.getSendTime())
|
||||
.eqIfPresent(SmsTaskDO::getSmsChannelId, reqVO.getSmsChannelId())
|
||||
.eqIfPresent(SmsTaskDO::getSmsType, reqVO.getSmsType())
|
||||
.eqIfPresent(SmsTaskDO::getSmsTemplateCode, reqVO.getSmsTemplateCode())
|
||||
.eqIfPresent(SmsTaskDO::getTestPhone, reqVO.getTestPhone())
|
||||
.orderByDesc(SmsTaskDO::getId));
|
||||
}
|
||||
|
||||
}
|
|
@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.haoka.service.api.ApiDealStrategyService;
|
|||
import cn.iocoder.yudao.module.haoka.service.api.models.OrderApiCreateParam;
|
||||
import cn.iocoder.yudao.module.haoka.service.api.models.OrderApiCreateResp;
|
||||
import cn.iocoder.yudao.module.haoka.service.onsaleproduct.OnSaleProductService;
|
||||
import cn.iocoder.yudao.module.haoka.service.smstask.SmsTaskService;
|
||||
import cn.iocoder.yudao.module.haoka.utils.SnowflakeId;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -42,6 +43,9 @@ public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, OrdersDO> imple
|
|||
@Qualifier(ApiFrom.ApiDealStrategyImpl)
|
||||
private ApiDealStrategyService apiDealStrategyService;
|
||||
|
||||
@Resource
|
||||
private SmsTaskService smsTaskService;
|
||||
|
||||
// 19547688 -> 电信 19547688 湖南电信,:号码+号码ID
|
||||
@Override
|
||||
public Long createOrders(OrdersSaveReqVO createReqVO) {
|
||||
|
@ -83,6 +87,17 @@ public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, OrdersDO> imple
|
|||
updateObj.setSuperiorApiId(oldOrderDo.getSuperiorApiId());
|
||||
|
||||
ordersMapper.updateById(updateObj);
|
||||
|
||||
smsTaskService.sendSMS(
|
||||
updateObj.getStatus(),
|
||||
updateObj.getId(),
|
||||
updateObj.getSource(),
|
||||
updateObj.getOnSaleProductId(),
|
||||
updateObj.getRefundStatus(),
|
||||
updateObj.getCallStatus(),
|
||||
updateObj.getAddressMobile(),
|
||||
updateObj
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package cn.iocoder.yudao.module.haoka.service.smstask;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.module.haoka.dal.dataobject.orders.OrdersDO;
|
||||
import jakarta.validation.*;
|
||||
import cn.iocoder.yudao.module.haoka.controller.admin.smstask.vo.*;
|
||||
import cn.iocoder.yudao.module.haoka.dal.dataobject.smstask.SmsTaskDO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
|
||||
/**
|
||||
* 短信任务 Service 接口
|
||||
*
|
||||
* @author 超级管理员
|
||||
*/
|
||||
public interface SmsTaskService {
|
||||
|
||||
/**
|
||||
* 创建短信任务
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createSmsTask(@Valid SmsTaskSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新短信任务
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateSmsTask(@Valid SmsTaskSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除短信任务
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteSmsTask(Long id);
|
||||
|
||||
/**
|
||||
* 获得短信任务
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 短信任务
|
||||
*/
|
||||
SmsTaskDO getSmsTask(Long id);
|
||||
|
||||
/**
|
||||
* 获得短信任务分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 短信任务分页
|
||||
*/
|
||||
PageResult<SmsTaskDO> getSmsTaskPage(SmsTaskPageReqVO pageReqVO);
|
||||
/**
|
||||
* @param orderStatus
|
||||
* @param orderId
|
||||
* @param source
|
||||
* @param onSaleProductId
|
||||
* @param refundStatus
|
||||
* @param callStatus
|
||||
* @param sentToPhone
|
||||
*/
|
||||
public void sendSMS(Long orderStatus, Long orderId, String source, Long onSaleProductId,
|
||||
String refundStatus, Long callStatus, String sentToPhone, OrdersDO ordersDO);
|
||||
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
package cn.iocoder.yudao.module.haoka.service.smstask;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.haoka.api.liantong.util.StringUtils;
|
||||
import cn.iocoder.yudao.module.haoka.dal.dataobject.orders.OrdersDO;
|
||||
import cn.iocoder.yudao.module.haoka.dal.dataobject.superiorapi.SuperiorApiDO;
|
||||
import cn.iocoder.yudao.module.haoka.service.superiorapi.SuperiorApiService;
|
||||
import cn.iocoder.yudao.module.haoka.utils.SnowflakeId;
|
||||
import cn.iocoder.yudao.module.system.api.sms.SmsSendApi;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import cn.iocoder.yudao.module.haoka.controller.admin.smstask.vo.*;
|
||||
import cn.iocoder.yudao.module.haoka.dal.dataobject.smstask.SmsTaskDO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
|
||||
import cn.iocoder.yudao.module.haoka.dal.mysql.smstask.SmsTaskMapper;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.haoka.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 短信任务 Service 实现类
|
||||
*
|
||||
* @author 超级管理员
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Validated
|
||||
public class SmsTaskServiceImpl implements SmsTaskService {
|
||||
|
||||
@Resource
|
||||
private SmsTaskMapper smsTaskMapper;
|
||||
|
||||
@Override
|
||||
public Long createSmsTask(SmsTaskSaveReqVO createReqVO) {
|
||||
// 插入
|
||||
SmsTaskDO smsTask = BeanUtils.toBean(createReqVO, SmsTaskDO.class);
|
||||
smsTask.setId(SnowflakeId.generate());
|
||||
smsTaskMapper.insert(smsTask);
|
||||
// 返回
|
||||
return smsTask.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSmsTask(SmsTaskSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateSmsTaskExists(updateReqVO.getId());
|
||||
// 更新
|
||||
SmsTaskDO updateObj = BeanUtils.toBean(updateReqVO, SmsTaskDO.class);
|
||||
smsTaskMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSmsTask(Long id) {
|
||||
// 校验存在
|
||||
validateSmsTaskExists(id);
|
||||
// 删除
|
||||
smsTaskMapper.deleteById(id);
|
||||
}
|
||||
|
||||
private void validateSmsTaskExists(Long id) {
|
||||
if (smsTaskMapper.selectById(id) == null) {
|
||||
throw exception(SMS_TASK_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsTaskDO getSmsTask(Long id) {
|
||||
return smsTaskMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<SmsTaskDO> getSmsTaskPage(SmsTaskPageReqVO pageReqVO) {
|
||||
return smsTaskMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
|
||||
@Resource
|
||||
private SmsSendApi smsSendApi;
|
||||
|
||||
/**
|
||||
* @param orderStatus
|
||||
* @param orderId
|
||||
* @param source
|
||||
* @param onSaleProductId
|
||||
* @param refundStatus
|
||||
* @param callStatus
|
||||
* @param sentToPhone
|
||||
*/
|
||||
public void sendSMS(Long orderStatus, Long orderId, String source, Long onSaleProductId,
|
||||
String refundStatus, Long callStatus, String sentToPhone, OrdersDO ordersDO) {
|
||||
try {
|
||||
List<SmsTaskDO> smsTaskDOList = smsTaskMapper
|
||||
.selectList(new LambdaQueryWrapperX<SmsTaskDO>()
|
||||
.eq(SmsTaskDO::getActive, true));
|
||||
|
||||
if (CollectionUtil.isEmpty(smsTaskDOList)) {
|
||||
return;
|
||||
}
|
||||
for (SmsTaskDO smsTaskDO : smsTaskDOList) {
|
||||
if (smsTaskDO.getOrderStatus() != null &&
|
||||
!smsTaskDO.getOrderStatus().equals(orderStatus)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (smsTaskDO.getOrderSourceList() != null &&
|
||||
!smsTaskDO.getOrderSourceList().contains(source)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (smsTaskDO.getOrderOnSaleProductList() != null &&
|
||||
!smsTaskDO.getOrderOnSaleProductList().contains(onSaleProductId.toString())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (smsTaskDO.getRefundStatus() != null &&
|
||||
!smsTaskDO.getRefundStatus().contains(refundStatus)) {
|
||||
continue;
|
||||
}
|
||||
if (smsTaskDO.getCallStatus() != null &&
|
||||
!smsTaskDO.getCallStatus().equals(callStatus.intValue())) {
|
||||
continue;
|
||||
}
|
||||
// logisticsInformation
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("phone", ordersDO.getPlanMobile());
|
||||
param.put("logisticsInfo", ordersDO.getTrackingCompany() + ordersDO.getTrackingNumber());
|
||||
param.put("imgUrl", getImageUrl(ordersDO));
|
||||
smsSendApi.sendSingleSmsWithChannel(
|
||||
sentToPhone,
|
||||
smsTaskDO.getName(),
|
||||
orderId,
|
||||
smsTaskDO.getSmsTemplateCode(),
|
||||
param,
|
||||
smsTaskDO.getSmsChannelId()
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getImageUrl(OrdersDO ordersDO) {
|
||||
String orderCreateResponse = ordersDO.getOrderCreateResponse();
|
||||
if (StringUtils.isEmpty(orderCreateResponse)) {
|
||||
return "";
|
||||
}
|
||||
JSONObject jsonObject = JSON.parseObject(orderCreateResponse);
|
||||
if (jsonObject == null) {
|
||||
return "";
|
||||
}
|
||||
if (jsonObject.containsKey("shortUrl")) {
|
||||
return jsonObject.getString("shortUrl");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?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="cn.iocoder.yudao.module.haoka.dal.mysql.smstask.SmsTaskMapper">
|
||||
|
||||
<!--
|
||||
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
|
||||
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
|
||||
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
|
||||
文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
|
||||
-->
|
||||
|
||||
</mapper>
|
|
@ -0,0 +1,166 @@
|
|||
package cn.iocoder.yudao.module.haoka.service.smstask;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
|
||||
import cn.iocoder.yudao.module.haoka.controller.admin.smstask.vo.*;
|
||||
import cn.iocoder.yudao.module.haoka.dal.dataobject.smstask.SmsTaskDO;
|
||||
import cn.iocoder.yudao.module.haoka.dal.mysql.smstask.SmsTaskMapper;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.hutool.core.util.RandomUtil.*;
|
||||
import static cn.iocoder.yudao.module.haoka.enums.ErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link SmsTaskServiceImpl} 的单元测试类
|
||||
*
|
||||
* @author 超级管理员
|
||||
*/
|
||||
@Import(SmsTaskServiceImpl.class)
|
||||
public class SmsTaskServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource
|
||||
private SmsTaskServiceImpl smsTaskService;
|
||||
|
||||
@Resource
|
||||
private SmsTaskMapper smsTaskMapper;
|
||||
|
||||
@Test
|
||||
public void testCreateSmsTask_success() {
|
||||
// 准备参数
|
||||
SmsTaskSaveReqVO createReqVO = randomPojo(SmsTaskSaveReqVO.class).setId(null);
|
||||
|
||||
// 调用
|
||||
Long smsTaskId = smsTaskService.createSmsTask(createReqVO);
|
||||
// 断言
|
||||
assertNotNull(smsTaskId);
|
||||
// 校验记录的属性是否正确
|
||||
SmsTaskDO smsTask = smsTaskMapper.selectById(smsTaskId);
|
||||
assertPojoEquals(createReqVO, smsTask, "id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSmsTask_success() {
|
||||
// mock 数据
|
||||
SmsTaskDO dbSmsTask = randomPojo(SmsTaskDO.class);
|
||||
smsTaskMapper.insert(dbSmsTask);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
SmsTaskSaveReqVO updateReqVO = randomPojo(SmsTaskSaveReqVO.class, o -> {
|
||||
o.setId(dbSmsTask.getId()); // 设置更新的 ID
|
||||
});
|
||||
|
||||
// 调用
|
||||
smsTaskService.updateSmsTask(updateReqVO);
|
||||
// 校验是否更新正确
|
||||
SmsTaskDO smsTask = smsTaskMapper.selectById(updateReqVO.getId()); // 获取最新的
|
||||
assertPojoEquals(updateReqVO, smsTask);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSmsTask_notExists() {
|
||||
// 准备参数
|
||||
SmsTaskSaveReqVO updateReqVO = randomPojo(SmsTaskSaveReqVO.class);
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> smsTaskService.updateSmsTask(updateReqVO), SMS_TASK_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSmsTask_success() {
|
||||
// mock 数据
|
||||
SmsTaskDO dbSmsTask = randomPojo(SmsTaskDO.class);
|
||||
smsTaskMapper.insert(dbSmsTask);// @Sql: 先插入出一条存在的数据
|
||||
// 准备参数
|
||||
Long id = dbSmsTask.getId();
|
||||
|
||||
// 调用
|
||||
smsTaskService.deleteSmsTask(id);
|
||||
// 校验数据不存在了
|
||||
assertNull(smsTaskMapper.selectById(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSmsTask_notExists() {
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> smsTaskService.deleteSmsTask(id), SMS_TASK_NOT_EXISTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
|
||||
public void testGetSmsTaskPage() {
|
||||
// mock 数据
|
||||
SmsTaskDO dbSmsTask = randomPojo(SmsTaskDO.class, o -> { // 等会查询到
|
||||
o.setName(null);
|
||||
o.setOrderStatus(null);
|
||||
o.setOrderSourceList(null);
|
||||
o.setOrderOnSaleProductList(null);
|
||||
o.setCallStatus(null);
|
||||
o.setSendTime(null);
|
||||
o.setSmsChannelId(null);
|
||||
o.setSmsType(null);
|
||||
o.setSmsTemplateId(null);
|
||||
o.setTestPhone(null);
|
||||
});
|
||||
smsTaskMapper.insert(dbSmsTask);
|
||||
// 测试 name 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setName(null)));
|
||||
// 测试 orderStatus 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setOrderStatus(null)));
|
||||
// 测试 orderSourceList 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setOrderSourceList(null)));
|
||||
// 测试 orderOnSaleProductList 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setOrderOnSaleProductList(null)));
|
||||
// 测试 callStatus 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setCallStatus(null)));
|
||||
// 测试 sendTime 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setSendTime(null)));
|
||||
// 测试 smsChannelId 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setSmsChannelId(null)));
|
||||
// 测试 smsType 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setSmsType(null)));
|
||||
// 测试 smsTemplateId 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setSmsTemplateId(null)));
|
||||
// 测试 testPhone 不匹配
|
||||
smsTaskMapper.insert(cloneIgnoreId(dbSmsTask, o -> o.setTestPhone(null)));
|
||||
// 准备参数
|
||||
SmsTaskPageReqVO reqVO = new SmsTaskPageReqVO();
|
||||
reqVO.setName(null);
|
||||
reqVO.setOrderStatus(null);
|
||||
reqVO.setOrderSourceList(null);
|
||||
reqVO.setOrderOnSaleProductList(null);
|
||||
reqVO.setCallStatus(null);
|
||||
reqVO.setSendTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
|
||||
reqVO.setSmsChannelId(null);
|
||||
reqVO.setSmsType(null);
|
||||
reqVO.setSmsTemplateId(null);
|
||||
reqVO.setTestPhone(null);
|
||||
|
||||
// 调用
|
||||
PageResult<SmsTaskDO> pageResult = smsTaskService.getSmsTaskPage(reqVO);
|
||||
// 断言
|
||||
assertEquals(1, pageResult.getTotal());
|
||||
assertEquals(1, pageResult.getList().size());
|
||||
assertPojoEquals(dbSmsTask, pageResult.getList().get(0));
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,8 @@ import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO
|
|||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信发送 API 接口
|
||||
*
|
||||
|
@ -31,4 +33,6 @@ public interface SmsSendApi {
|
|||
*/
|
||||
Long sendSingleSmsToMember(@Valid SmsSendSingleToUserReqDTO reqDTO);
|
||||
|
||||
Long sendSingleSmsWithChannel(String mobile, String taskName, Long orderId,
|
||||
String templateCode, Map<String, Object> templateParams, Long channelId);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import org.springframework.validation.annotation.Validated;
|
|||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信发送 API 接口
|
||||
*
|
||||
|
@ -31,4 +33,9 @@ public class SmsSendApiImpl implements SmsSendApi {
|
|||
reqDTO.getTemplateCode(), reqDTO.getTemplateParams());
|
||||
}
|
||||
|
||||
public Long sendSingleSmsWithChannel(String mobile, String taskName, Long orderId,
|
||||
String templateCode, Map<String, Object> templateParams, Long channelId) {
|
||||
return smsSendService.sendSingleSmsWithChannel(mobile, taskName, orderId,
|
||||
templateCode, templateParams, channelId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ public interface SmsSendService {
|
|||
Long sendSingleSmsToMember(String mobile, Long userId,
|
||||
String templateCode, Map<String, Object> templateParams);
|
||||
|
||||
Long sendSingleSmsWithChannel(String mobile, Long userId, Integer userType, String taskName, Long orderId,
|
||||
Long sendSingleSmsWithChannel(String mobile, String taskName, Long orderId,
|
||||
String templateCode, Map<String, Object> templateParams, Long channelId);
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,7 +78,7 @@ public class SmsSendServiceImpl implements SmsSendService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Long sendSingleSmsWithChannel(String mobile, Long userId, Integer userType,String taskName,Long orderId,
|
||||
public Long sendSingleSmsWithChannel(String mobile, String taskName,Long orderId,
|
||||
String templateCode, Map<String, Object> templateParams, Long channelId) {
|
||||
// 校验短信模板是否合法
|
||||
SmsTemplateDO template = validateSmsTemplate(templateCode);
|
||||
|
@ -94,7 +94,7 @@ public class SmsSendServiceImpl implements SmsSendService {
|
|||
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
|
||||
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
|
||||
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
|
||||
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams,taskName,orderId);
|
||||
Long sendLogId = smsLogService.createSmsLog(mobile, 1L, 1, isSend, template, content, templateParams,taskName,orderId);
|
||||
|
||||
// 发送 MQ 消息,异步执行发送短信
|
||||
if (isSend) {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'短信任务管理', '', 2, 0, 2912,
|
||||
'sms-task', '', 'haoka/smstask/index', 0, 'SmsTask'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'短信任务查询', 'haoka:sms-task:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'短信任务创建', 'haoka:sms-task:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'短信任务更新', 'haoka:sms-task:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'短信任务删除', 'haoka:sms-task:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'短信任务导出', 'haoka:sms-task:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
|
||||
alter table haoka_sms_task
|
||||
ADD COLUMN `active`
|
||||
bit(1) NOT NULL DEFAULT b'1' COMMENT '是否启用';
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
alter table haoka_sms_task
|
||||
ADD COLUMN `refund_status`
|
||||
varchar(2048) COMMENT '退款状态';
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE `haoka_sms_task`
|
||||
CHANGE COLUMN `sms_template_id` `sms_template_code` varchar(2048) NOT NULL COMMENT '短信内容' AFTER `sms_type`;
|
Loading…
Reference in New Issue