调整 音频合成, 头衔监测

This commit is contained in:
spllzh
2025-10-06 23:16:59 +08:00
parent 20378e8115
commit 76dedb657a
36 changed files with 1732 additions and 12 deletions

View File

@@ -134,6 +134,8 @@ public class PasswordUtil {

View File

@@ -118,6 +118,8 @@ public class ServiceManager {

View File

@@ -96,6 +96,8 @@ public class AliyunConfig {

View File

@@ -106,7 +106,7 @@ public class FaceDetectController {
try {
log.info("开始处理头衔上传,文件名: {}, 大小: {} bytes, 用户ID: {}",
file.getOriginalFilename(), file.getSize(), userId);
expiresInSeconds = 30*24*3600; // 默认30
expiresInSeconds = 7*24*3600; // 默认7
// 1. 验证文件
if (file.isEmpty()) {
result.put("success", false);
@@ -167,12 +167,7 @@ public class FaceDetectController {
result.put("avatarName", avatarName);
// 人脸检测结果
result.put("faceDetection", Map.of(
"success", detectLog.getSuccess(),
"faceCount", detectLog.getFaceCount(),
"processingTimeMs", detectLog.getProcessingTimeMs(),
"requestId", detectLog.getRequestId()
));
result.put("faceDetection", detectLog.toString());
if (!detectLog.getSuccess()) {
result.put("faceDetectionError", detectLog.getErrorMessage());
@@ -225,12 +220,18 @@ public class FaceDetectController {
@Parameter(description = "开始时间")
@RequestParam(required = false) String startTime,
@Parameter(description = "结束时间")
@RequestParam(required = false) String endTime) {
@RequestParam(required = false) String endTime,
@Parameter(description = "所有人姓名")
@RequestParam(required = false) String ownerName,
@Parameter(description = "所有人电话")
@RequestParam(required = false) String ownerPhone,
@Parameter(description = "头像名字")
@RequestParam(required = false) String avatarName) {
Map<String, Object> result = new HashMap<>();
try {
com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.rj.entity.FaceDetectLog> page =
faceDetectLogService.getPageList(current, size, success, startTime, endTime);
faceDetectLogService.getPageList(current, size, success, startTime, endTime, ownerName, ownerPhone, avatarName);
result.put("success", true);
result.put("message", "查询成功");

View File

@@ -0,0 +1,180 @@
package com.rj.controller;
import com.rj.dto.VideoSynthesisRequestDto;
import com.rj.entity.VideoSynthesisLog;
import com.rj.service.IVideoSynthesisService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.HashMap;
import java.util.Map;
/**
* 视频合成控制器
*
* @author rj
* @date 2025-01-02
*/
@Slf4j
@RestController
@RequestMapping("/api/video-synthesis")
@Tag(name = "视频合成,根据头像和音频生成视频", description = "视频合成相关接口,根据头像和音频生成视频")
public class VideoSynthesisFromController {
@Autowired
private IVideoSynthesisService videoSynthesisService;
/**
* 视频合成接口
*/
@PostMapping("/synthesize")
@Operation(summary = "视频合成", description = "根据图片和音频生成视频")
public ResponseEntity<Map<String, Object>> synthesizeVideo(@Valid @RequestBody VideoSynthesisRequestDto requestDto) {
Map<String, Object> result = new HashMap<>();
try {
log.info("开始视频合成图片URL: {}, 音频URL: {}", requestDto.getImageUrl(), requestDto.getAudioUrl());
// 执行视频合成
VideoSynthesisLog synthesisLog = videoSynthesisService.synthesizeVideoAndSave(requestDto);
// 构建响应结果
result.put("success", synthesisLog.getSuccess());
result.put("message", synthesisLog.getSuccess() ? "视频合成成功" : "视频合成失败");
result.put("requestId", synthesisLog.getRequestId());
result.put("taskId", synthesisLog.getTaskId());
result.put("taskStatus", synthesisLog.getTaskStatus());
result.put("videoUrl", synthesisLog.getVideoUrl());
result.put("videoDuration", synthesisLog.getVideoDuration());
result.put("videoRatio", synthesisLog.getVideoRatio());
result.put("processingTimeMs", synthesisLog.getProcessingTimeMs());
result.put("imageId", synthesisLog.getImageId());
result.put("audioId", synthesisLog.getAudioId());
result.put("ownerName", synthesisLog.getOwnerName());
result.put("ownerPhone", synthesisLog.getOwnerPhone());
result.put("modelProvider", synthesisLog.getModelProvider());
if (!synthesisLog.getSuccess()) {
result.put("errorMessage", synthesisLog.getErrorMessage());
return ResponseEntity.badRequest().body(result);
}
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("视频合成失败: {}", e.getMessage(), e);
result.put("success", false);
result.put("message", "视频合成失败: " + e.getMessage());
return ResponseEntity.internalServerError().body(result);
}
}
/**
* 根据请求ID查询视频合成结果
*/
@GetMapping("/result/{requestId}")
@Operation(summary = "查询合成结果", description = "根据请求ID查询视频合成结果")
public ResponseEntity<Map<String, Object>> getSynthesisResult(
@Parameter(description = "请求ID", required = true)
@PathVariable String requestId) {
Map<String, Object> result = new HashMap<>();
try {
VideoSynthesisLog synthesisLog = videoSynthesisService.getVideoSynthesisLogByRequestId(requestId);
if (synthesisLog == null) {
result.put("success", false);
result.put("message", "未找到对应的视频合成记录");
return ResponseEntity.notFound().build();
}
result.put("success", true);
result.put("message", "查询成功");
result.put("requestId", synthesisLog.getRequestId());
result.put("taskId", synthesisLog.getTaskId());
result.put("taskStatus", synthesisLog.getTaskStatus());
result.put("videoUrl", synthesisLog.getVideoUrl());
result.put("videoDuration", synthesisLog.getVideoDuration());
result.put("videoRatio", synthesisLog.getVideoRatio());
result.put("processingTimeMs", synthesisLog.getProcessingTimeMs());
result.put("requestTime", synthesisLog.getRequestTime());
result.put("responseTime", synthesisLog.getResponseTime());
result.put("imageId", synthesisLog.getImageId());
result.put("audioId", synthesisLog.getAudioId());
result.put("ownerName", synthesisLog.getOwnerName());
result.put("ownerPhone", synthesisLog.getOwnerPhone());
result.put("modelProvider", synthesisLog.getModelProvider());
if (!synthesisLog.getSuccess()) {
result.put("errorMessage", synthesisLog.getErrorMessage());
}
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("查询视频合成结果失败: {}", e.getMessage(), e);
result.put("success", false);
result.put("message", "查询失败: " + e.getMessage());
return ResponseEntity.internalServerError().body(result);
}
}
/**
* 根据任务ID查询视频合成结果
*/
@GetMapping("/result/task/{taskId}")
@Operation(summary = "根据任务ID查询结果", description = "根据任务ID查询视频合成结果")
public ResponseEntity<Map<String, Object>> getSynthesisResultByTaskId(
@Parameter(description = "任务ID", required = true)
@PathVariable String taskId) {
Map<String, Object> result = new HashMap<>();
try {
VideoSynthesisLog synthesisLog = videoSynthesisService.getVideoSynthesisLogByTaskId(taskId);
if (synthesisLog == null) {
result.put("success", false);
result.put("message", "未找到对应的视频合成记录");
return ResponseEntity.notFound().build();
}
result.put("success", true);
result.put("message", "查询成功");
result.put("requestId", synthesisLog.getRequestId());
result.put("taskId", synthesisLog.getTaskId());
result.put("taskStatus", synthesisLog.getTaskStatus());
result.put("videoUrl", synthesisLog.getVideoUrl());
result.put("videoDuration", synthesisLog.getVideoDuration());
result.put("videoRatio", synthesisLog.getVideoRatio());
result.put("processingTimeMs", synthesisLog.getProcessingTimeMs());
result.put("requestTime", synthesisLog.getRequestTime());
result.put("responseTime", synthesisLog.getResponseTime());
result.put("imageId", synthesisLog.getImageId());
result.put("audioId", synthesisLog.getAudioId());
result.put("ownerName", synthesisLog.getOwnerName());
result.put("ownerPhone", synthesisLog.getOwnerPhone());
result.put("modelProvider", synthesisLog.getModelProvider());
if (!synthesisLog.getSuccess()) {
result.put("errorMessage", synthesisLog.getErrorMessage());
}
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("根据任务ID查询视频合成结果失败: {}", e.getMessage(), e);
result.put("success", false);
result.put("message", "查询失败: " + e.getMessage());
return ResponseEntity.internalServerError().body(result);
}
}
}

View File

@@ -356,6 +356,8 @@ public class MenuController {

View File

@@ -326,6 +326,8 @@ public class RoleController {

View File

@@ -332,6 +332,8 @@ public class UserRoleController {

View File

@@ -99,6 +99,8 @@ public class DifyWorkflowResponseDto {

View File

@@ -0,0 +1,62 @@
package com.rj.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import java.math.BigDecimal;
/**
* 视频合成请求DTO
*
* @author rj
* @date 2025-01-02
*/
@Data
@Schema(description = "视频合成请求参数")
public class VideoSynthesisRequestDto {
@NotBlank(message = "图片URL不能为空")
@Schema(description = "图片URL", required = true, example = "https://example.com/image.jpg")
private String imageUrl;
@NotBlank(message = "音频URL不能为空")
@Schema(description = "音频URL", required = true, example = "https://example.com/audio.wav")
private String audioUrl;
@Schema(description = "使用的模型", example = "liveportrait")
private String model = "liveportrait";
@Schema(description = "图片ID", example = "img-12345")
private String imageId;
@Schema(description = "音频ID", example = "audio-67890")
private String audioId;
@Schema(description = "归属人姓名", example = "张三")
private String ownerName;
@Schema(description = "归属人电话", example = "13800138000")
private String ownerPhone;
@Schema(description = "模型供应商", example = "dashscope")
private String modelProvider;
@Schema(description = "模板ID", example = "normal")
private String templateId = "normal";
@Schema(description = "眼部动作频率", example = "0.5")
private BigDecimal eyeMoveFreq = new BigDecimal("0.5");
@Schema(description = "视频帧率", example = "30")
private Integer videoFps = 30;
@Schema(description = "嘴部动作强度", example = "1.0")
private BigDecimal mouthMoveStrength = new BigDecimal("1.0");
@Schema(description = "是否粘贴背景", example = "true")
private Boolean pasteBack = true;
@Schema(description = "头部动作强度", example = "0.7")
private BigDecimal headMoveStrength = new BigDecimal("0.7");
}

View File

@@ -0,0 +1,189 @@
package com.rj.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 视频合成日志实体类
*
* @author rj
* @date 2025-01-02
*/
@Data
@TableName("video_synthesis_log")
public class VideoSynthesisLog {
/**
* 主键IDUUID
*/
@TableId(type = IdType.ASSIGN_UUID)
private String id;
/**
* 请求ID唯一
*/
@TableField("request_id")
private String requestId;
/**
* 任务ID
*/
@TableField("task_id")
private String taskId;
/**
* 使用的模型
*/
@TableField("model")
private String model;
/**
* 图片URL
*/
@TableField("image_url")
private String imageUrl;
/**
* 音频URL
*/
@TableField("audio_url")
private String audioUrl;
/**
* 音频ID
*/
@TableField("audio_id")
private String audioId;
/**
* 图片ID
*/
@TableField("image_id")
private String imageId;
/**
* 归属人姓名
*/
@TableField("owner_name")
private String ownerName;
/**
* 归属人电话
*/
@TableField("owner_phone")
private String ownerPhone;
/**
* 模型供应商
*/
@TableField("model_provider")
private String modelProvider;
/**
* 模板ID
*/
@TableField("template_id")
private String templateId;
/**
* 眼部动作频率
*/
@TableField("eye_move_freq")
private BigDecimal eyeMoveFreq;
/**
* 视频帧率
*/
@TableField("video_fps")
private Integer videoFps;
/**
* 嘴部动作强度
*/
@TableField("mouth_move_strength")
private BigDecimal mouthMoveStrength;
/**
* 是否粘贴背景
*/
@TableField("paste_back")
private Boolean pasteBack;
/**
* 头部动作强度
*/
@TableField("head_move_strength")
private BigDecimal headMoveStrength;
/**
* 请求时间
*/
@TableField("request_time")
private LocalDateTime requestTime;
/**
* 响应时间
*/
@TableField("response_time")
private LocalDateTime responseTime;
/**
* 任务状态
*/
@TableField("task_status")
private String taskStatus;
/**
* 生成的视频URL
*/
@TableField("video_url")
private String videoUrl;
/**
* 视频时长(秒)
*/
@TableField("video_duration")
private BigDecimal videoDuration;
/**
* 视频比例
*/
@TableField("video_ratio")
private String videoRatio;
/**
* 是否成功
*/
@TableField("success")
private Boolean success;
/**
* 错误信息
*/
@TableField("error_message")
private String errorMessage;
/**
* 处理时间(毫秒)
*/
@TableField("processing_time_ms")
private Long processingTimeMs;
/**
* 创建时间
*/
@TableField("created_at")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@TableField("updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -42,6 +42,8 @@ public interface CustomerProfileAnalysisMapper extends BaseMapper<CustomerProfil

View File

@@ -0,0 +1,15 @@
package com.rj.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.rj.entity.VideoSynthesisLog;
import org.apache.ibatis.annotations.Mapper;
/**
* 视频合成日志Mapper接口
*
* @author rj
* @date 2025-01-02
*/
@Mapper
public interface VideoSynthesisLogMapper extends BaseMapper<VideoSynthesisLog> {
}

View File

@@ -102,6 +102,8 @@ public class AudioStatisticsScheduler {

View File

@@ -40,10 +40,14 @@ public interface IFaceDetectLogService extends IService<FaceDetectLog> {
* @param success 成功状态筛选
* @param startTime 开始时间
* @param endTime 结束时间
* @param ownerName 所有人姓名
* @param ownerPhone 所有人电话
* @param avatarName 头像名字
* @return 分页结果
*/
com.baomidou.mybatisplus.extension.plugins.pagination.Page<FaceDetectLog> getPageList(
Integer current, Integer size, Boolean success, String startTime, String endTime);
Integer current, Integer size, Boolean success, String startTime, String endTime,
String ownerName, String ownerPhone, String avatarName);
/**
* 获取统计信息

View File

@@ -0,0 +1,45 @@
package com.rj.service;
import com.rj.dto.VideoSynthesisRequestDto;
import com.rj.entity.VideoSynthesisLog;
/**
* 视频合成服务接口
*
* @author rj
* @date 2025-01-02
*/
public interface IVideoSynthesisService {
/**
* 执行视频合成并保存日志
*
* @param request 视频合成请求
* @return 视频合成日志
*/
VideoSynthesisLog synthesizeVideoAndSave(VideoSynthesisRequestDto request);
/**
* 保存视频合成日志
*
* @param log 视频合成日志
* @return 是否保存成功
*/
boolean saveVideoSynthesisLog(VideoSynthesisLog log);
/**
* 根据请求ID查询视频合成日志
*
* @param requestId 请求ID
* @return 视频合成日志
*/
VideoSynthesisLog getVideoSynthesisLogByRequestId(String requestId);
/**
* 根据任务ID查询视频合成日志
*
* @param taskId 任务ID
* @return 视频合成日志
*/
VideoSynthesisLog getVideoSynthesisLogByTaskId(String taskId);
}

View File

@@ -63,6 +63,8 @@ public interface ICustomerProfileAnalysisService extends IService<CustomerProfil

View File

@@ -98,6 +98,8 @@ public class CustomerProfileAnalysisServiceImpl extends ServiceImpl<CustomerProf

View File

@@ -71,7 +71,7 @@ public class FaceDetectLogServiceImpl extends ServiceImpl<FaceDetectLogMapper, F
LocalDateTime requestTime = LocalDateTime.now();
faceDetectLog.setRequestId(requestId);
faceDetectLog.setModel("liveportrait-detect");
faceDetectLog.setModel("liveportrait-detect"); //TODO 模型名称
faceDetectLog.setImageUrl(imageUrl);
faceDetectLog.setOwnerName(ownerName);
faceDetectLog.setOwnerPhone(ownerPhone);
@@ -261,7 +261,8 @@ public class FaceDetectLogServiceImpl extends ServiceImpl<FaceDetectLogMapper, F
*/
@Override
public com.baomidou.mybatisplus.extension.plugins.pagination.Page<FaceDetectLog> getPageList(
Integer current, Integer size, Boolean success, String startTime, String endTime) {
Integer current, Integer size, Boolean success, String startTime, String endTime,
String ownerName, String ownerPhone, String avatarName) {
com.baomidou.mybatisplus.extension.plugins.pagination.Page<FaceDetectLog> page =
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(current, size);
@@ -279,6 +280,15 @@ public class FaceDetectLogServiceImpl extends ServiceImpl<FaceDetectLogMapper, F
if (endTime != null && !endTime.trim().isEmpty()) {
queryWrapper.le(FaceDetectLog::getRequestTime, endTime);
}
if (ownerName != null && !ownerName.trim().isEmpty()) {
queryWrapper.like(FaceDetectLog::getOwnerName, ownerName);
}
if (ownerPhone != null && !ownerPhone.trim().isEmpty()) {
queryWrapper.like(FaceDetectLog::getOwnerPhone, ownerPhone);
}
if (avatarName != null && !avatarName.trim().isEmpty()) {
queryWrapper.like(FaceDetectLog::getAvatarName, avatarName);
}
// 按请求时间倒序排列
queryWrapper.orderByDesc(FaceDetectLog::getRequestTime);

View File

@@ -0,0 +1,151 @@
package com.rj.service.impl;
import com.rj.dto.VideoSynthesisRequestDto;
import com.rj.entity.VideoSynthesisLog;
import com.rj.mapper.VideoSynthesisLogMapper;
import com.rj.service.IVideoSynthesisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 视频合成服务实现类
*
* @author rj
* @date 2025-01-02
*/
@Slf4j
@Service
public class VideoSynthesisServiceImpl implements IVideoSynthesisService {
@Autowired
private VideoSynthesisLogMapper videoSynthesisLogMapper;
@Autowired
private RestTemplate restTemplate;
@Value("${dashscope.api.key}")
private String apiKey;
private static final String API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis/";
@Override
public VideoSynthesisLog synthesizeVideoAndSave(VideoSynthesisRequestDto request) {
LocalDateTime startTime = LocalDateTime.now();
String requestId = UUID.randomUUID().toString();
VideoSynthesisLog synthesisLog = new VideoSynthesisLog();
synthesisLog.setRequestId(requestId);
synthesisLog.setRequestTime(startTime);
synthesisLog.setSuccess(false);
// 使用BeanUtils复制相同名称的属性
BeanUtils.copyProperties(request, synthesisLog);
try {
log.info("开始视频合成请求ID: {}, 图片URL: {}, 音频URL: {}",
requestId, request.getImageUrl(), request.getAudioUrl());
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-DashScope-Async", "enable");
headers.set("Authorization", "Bearer " + apiKey);
// 构建请求体
HttpEntity<VideoSynthesisRequestDto> entity = new HttpEntity<>(request, headers);
// 发送请求
ResponseEntity<String> response = restTemplate.postForEntity(API_URL, entity, String.class);
LocalDateTime endTime = LocalDateTime.now();
synthesisLog.setResponseTime(endTime);
synthesisLog.setProcessingTimeMs(java.time.Duration.between(startTime, endTime).toMillis());
// 解析响应
if (response.getStatusCode() == HttpStatus.OK) {
// 直接解析JSON响应
String responseBody = response.getBody();
log.info("API响应: {}", responseBody);
// 简单解析关键字段
if (responseBody != null && responseBody.contains("task_id")) {
// 这里可以添加更复杂的JSON解析逻辑
// 暂时设置基本状态
synthesisLog.setTaskStatus("PENDING");
synthesisLog.setSuccess(true);
} else {
synthesisLog.setErrorMessage("API响应格式异常");
synthesisLog.setSuccess(false);
}
log.info("视频合成请求成功任务ID: {}, 状态: {}",
synthesisLog.getTaskId(), synthesisLog.getTaskStatus());
} else {
synthesisLog.setErrorMessage("HTTP请求失败状态码: " + response.getStatusCode());
log.error("视频合成请求失败,状态码: {}, 响应: {}", response.getStatusCode(), response.getBody());
}
} catch (Exception e) {
LocalDateTime endTime = LocalDateTime.now();
synthesisLog.setResponseTime(endTime);
synthesisLog.setProcessingTimeMs(java.time.Duration.between(startTime, endTime).toMillis());
synthesisLog.setErrorMessage("视频合成异常: " + e.getMessage());
log.error("视频合成失败: {}", e.getMessage(), e);
}
// 保存日志
saveVideoSynthesisLog(synthesisLog);
return synthesisLog;
}
@Override
public boolean saveVideoSynthesisLog(VideoSynthesisLog synthesisLog) {
try {
synthesisLog.setCreatedAt(LocalDateTime.now());
synthesisLog.setUpdatedAt(LocalDateTime.now());
int result = videoSynthesisLogMapper.insert(synthesisLog);
log.info("视频合成日志保存{}: 请求ID={}, 任务ID={}, 成功={}",
result > 0 ? "成功" : "失败", synthesisLog.getRequestId(), synthesisLog.getTaskId(), synthesisLog.getSuccess());
return result > 0;
} catch (Exception e) {
log.error("保存视频合成日志失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public VideoSynthesisLog getVideoSynthesisLogByRequestId(String requestId) {
try {
return videoSynthesisLogMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<VideoSynthesisLog>()
.eq("request_id", requestId)
);
} catch (Exception e) {
log.error("根据请求ID查询视频合成日志失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public VideoSynthesisLog getVideoSynthesisLogByTaskId(String taskId) {
try {
return videoSynthesisLogMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<VideoSynthesisLog>()
.eq("task_id", taskId)
);
} catch (Exception e) {
log.error("根据任务ID查询视频合成日志失败: {}", e.getMessage(), e);
return null;
}
}
}

View File

@@ -65,6 +65,8 @@ public interface IMenuService extends IService<Menu> {

View File

@@ -65,6 +65,8 @@ public interface IUserRoleService extends IService<UserRole> {

View File

@@ -69,6 +69,8 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IM

View File

@@ -69,6 +69,8 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IR

View File

@@ -69,6 +69,8 @@ public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> i