From 5732208c7dde555255ed1b1a1935ddef49198b13 Mon Sep 17 00:00:00 2001 From: longbinbin <2284496322@qq.com> Date: Thu, 5 Dec 2024 18:06:27 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=B7=BB=E5=8A=A0oss=E4=BA=BA=E8=84=B8?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/mobile/AppFaceController.java | 27 +- .../exception/CustomExceptionHandle.java | 2 +- .../service/impl/pc/FaceServiceImpl.java | 72 +++ .../ycwl/basic/service/pc/FaceService.java | 2 + .../java/com/ycwl/basic/utils/DateUtils.java | 450 ++++++++++++++++++ .../com/ycwl/basic/utils/oss/OssUtil.java | 87 ++++ src/main/resources/application.yml | 13 + 7 files changed, 639 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/utils/DateUtils.java create mode 100644 src/main/java/com/ycwl/basic/utils/oss/OssUtil.java diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppFaceController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppFaceController.java index d0936e0..4a87fcb 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppFaceController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppFaceController.java @@ -1,11 +1,14 @@ package com.ycwl.basic.controller.mobile; +import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.utils.ApiResponse; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; /** * @Author:longbinbin @@ -16,19 +19,17 @@ import org.springframework.web.bind.annotation.RestController; @Api(tags = "用户人脸相关接口") public class AppFaceController { + @Autowired + private FaceService faceService; - @ApiOperation("人脸有效性校验") - @PostMapping("/checkFaceValidity") - public ApiResponse checkFaceValidity() { - //TODO 人脸有效性校验逻辑 - return ApiResponse.success(""); + @ApiOperation("人脸照片上传") + @PostMapping("/faceUPload") + public ApiResponse faceUPload(MultipartFile file) { + //1、上传人脸照片 + //2、人脸照片有效性校验 + //3、校验失败,删除,提示重新上传 + //4、校验成功,保存用户人脸信息,将访问人脸照片访问地址响应给前端 + + return faceService.faceUPload(file); } - - @ApiOperation("人脸上传") - @PostMapping("/saveFace") - public ApiResponse saveFace() { - //TODO 保存人脸逻辑 - return ApiResponse.success(""); - } - } diff --git a/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java b/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java index 47aac5a..7c5f76b 100644 --- a/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java +++ b/src/main/java/com/ycwl/basic/exception/CustomExceptionHandle.java @@ -16,7 +16,7 @@ import javax.servlet.http.HttpServletResponse; * @date 2022年09月23日 10:19 * 全局异常处理器 */ -@RestControllerAdvice(basePackages = {"com.ycwl.smartPark"}) +@RestControllerAdvice(basePackages = {"com.ycwl.basic"}) public class CustomExceptionHandle { private static final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionHandle.class); diff --git a/src/main/java/com/ycwl/basic/service/impl/pc/FaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/pc/FaceServiceImpl.java index 5998a5a..b40c8e4 100644 --- a/src/main/java/com/ycwl/basic/service/impl/pc/FaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/impl/pc/FaceServiceImpl.java @@ -2,26 +2,37 @@ package com.ycwl.basic.service.impl.pc; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; +import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.mapper.pc.FaceMapper; import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.req.FaceReqQuery; import com.ycwl.basic.model.pc.face.resp.FaceRespVO; import com.ycwl.basic.service.pc.FaceService; import com.ycwl.basic.utils.ApiResponse; +import com.ycwl.basic.utils.DateUtils; import com.ycwl.basic.utils.SnowFlakeUtil; +import com.ycwl.basic.utils.oss.OssUtil; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; import java.util.List; /** * @Author:longbinbin * @Date:2024/12/2 16:39 */ +@Slf4j @Service public class FaceServiceImpl implements FaceService { @Autowired private FaceMapper faceMapper; + @Autowired + private OssUtil ossUtil; @Override public ApiResponse> pageQuery(FaceReqQuery faceReqQuery) { @@ -77,4 +88,65 @@ public class FaceServiceImpl implements FaceService { } return ApiResponse.success(i); } + + @Override + public ApiResponse faceUPload(MultipartFile file) { + //TODO 获取用户信息 + String userId="1"; + //1、上传人脸照片 + String facaeUrl = uploadFileALiOss(file, userId); + //TODO 2、人脸照片有效性校验 + Boolean isValid=true; + + + + if(isValid){ + //校验成功,保存用户人脸信息,将访问人脸照片访问地址响应给前端 + + FaceEntity faceEntity = new FaceEntity(); + //TODO 人脸数据存库 + faceMapper.add(faceEntity); + + return ApiResponse.success(facaeUrl); + }else { + //校验失败,删除,提示重新上传 + + + throw new BaseException("人脸照片校验失败,请重新上传"); + } + } + + /** + * 阿里oss图片上传 + * + * @param file file + * @param userId 用户id + * @return 地址 + */ + private String uploadFileALiOss(MultipartFile file,String userId) { + if (file.isEmpty()) { + throw new RuntimeException("文件不存在!"); + } + String originalFilename = file.getOriginalFilename(); + //获取文件名后缀 + String suffix = originalFilename.split("\\.")[1]; + if ("Jpeg".equals(suffix)) { + suffix = "jpg"; + } + //文件储存路径 + String filePath=""; + String dateStr = DateUtils.format(new Date(),"yyyy-MM-dd"); + + filePath=filePath+dateStr+"/"; + // 生成文件名 + String fileName= userId+"." + suffix; + InputStream inputStream ; + try { + inputStream = file.getInputStream(); + } catch (IOException e) { + log.error("文件上传失败!", e); + return null; + } + return ossUtil.uploadFile(inputStream,filePath,fileName) ; + } } diff --git a/src/main/java/com/ycwl/basic/service/pc/FaceService.java b/src/main/java/com/ycwl/basic/service/pc/FaceService.java index 9d32b13..b5aff94 100644 --- a/src/main/java/com/ycwl/basic/service/pc/FaceService.java +++ b/src/main/java/com/ycwl/basic/service/pc/FaceService.java @@ -5,6 +5,7 @@ import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.req.FaceReqQuery; import com.ycwl.basic.model.pc.face.resp.FaceRespVO; import com.ycwl.basic.utils.ApiResponse; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -21,4 +22,5 @@ public interface FaceService { ApiResponse deleteByIds(List ids); ApiResponse update(FaceEntity face); + ApiResponse faceUPload(MultipartFile file); } diff --git a/src/main/java/com/ycwl/basic/utils/DateUtils.java b/src/main/java/com/ycwl/basic/utils/DateUtils.java new file mode 100644 index 0000000..fd285f3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/utils/DateUtils.java @@ -0,0 +1,450 @@ + +package com.ycwl.basic.utils; + +import lombok.SneakyThrows; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.*; + +/** + * 日期处理 + */ +public class DateUtils { + /** + * 时间格式(yyyy-MM-dd) + */ + public final static String DATE_PATTERN = "yyyy-MM-dd"; + public final static String DATE_PATTERN_NYR = "yyyy年MM月dd日"; + public final static String DATE_NO_PATTERN = "yyyyMMdd"; + public final static String DATE_NO_TIME = "HHmmss"; + public final static String DATE_PATTERN_YYDD = "MM月dd日 HH:mm"; + public final static String YYYY_MM = "yyyyMM"; + public final static String YYYY_MM_DD_HH_MM_SS_SSS = "yyyyMMddHHmmssSSS"; + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + /** + * 时间格式(yyyy-MM-dd HH:mm:ss) + */ + public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + /** + * 日期转化为cron表达式 + */ + public final static String CRON_DATE_TIME_PATTERN = "ss mm HH dd MM ? yyyy"; + + /** + * 日期格式化 日期格式为:yyyy-MM-dd + * + * @param date 日期 + * @return 返回yyyy-MM-dd格式日期 + */ + public static String format(Date date) { + return format(date, DATE_PATTERN); + } + + /** + * 日期格式化 日期格式为:yyyy-MM-dd + * + * @param date 日期 + * @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN + * @return 返回yyyy-MM-dd格式日期 + */ + public static String format(Date date, String pattern) { + if (date != null) { + SimpleDateFormat df = new SimpleDateFormat(pattern); + return df.format(date); + } + return null; + } + + + /** + * 日期格式化 日期格式为:yyyy-MM-dd HH:mm:ss + * + * @param dataStr + * @return 返回yyyy-MM-dd格式日期 + */ + public static Date format(String dataStr) throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat(DATE_TIME_PATTERN); + return sdf.parse(dataStr); + } + + + /** + * 日期解析 + * + * @param date 日期 + * @param pattern 格式,如:DateUtils.DATE_TIME_PATTERN + * @return 返回Date + */ + public static Date parse(String date, String pattern) { + try { + return new SimpleDateFormat(pattern).parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 返回0时0分0秒的date + * + * @param date 日期 + * @return + */ + @SneakyThrows + public static Date startOfDay(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String s = sdf.format(date); + return sdf.parse(s); + } + + /** + * 这个月的第一天 + * + * @param date 日期 + * @return + */ + public static Date startOfMonth(Date date) { + Date date1 = startOfDay(date); + date1.setDate(1); + return date1; + } + + /** + * 获取某个月有多少天 + * + * @param yearMonth + * @return + */ + public static String getLastDayOfMonth(String yearMonth) { + int year = Integer.parseInt(yearMonth.split("-")[0]); //年 + int month = Integer.parseInt(yearMonth.split("-")[1]); //月 + Calendar cal = Calendar.getInstance(); + // 设置年份 + cal.set(Calendar.YEAR, year); + // 设置月份 + // cal.set(Calendar.MONTH, month - 1); + cal.set(Calendar.MONTH, month); //设置当前月的上一个月 + // 获取某月最大天数 + //int lastDay = cal.getActualMaximum(Calendar.DATE); + int lastDay = cal.getMinimum(Calendar.DATE); //获取月份中的最小值,即第一天 + // 设置日历中月份的最大天数 + //cal.set(Calendar.DAY_OF_MONTH, lastDay); + cal.set(Calendar.DAY_OF_MONTH, lastDay - 1); //上月的第一天减去1就是当月的最后一天 + // 格式化日期 + SimpleDateFormat sdf = new SimpleDateFormat("dd"); + return sdf.format(cal.getTime()); + } + + /** + * 获得某月最大时间 2019-10-08 23:59:59 + * + * @param date + * @return + */ + public static Date getEndMonthOfDay(Date date) { + if (date == null) { + return null; + } + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault()); + LocalDateTime endOfDay = localDateTime.with(TemporalAdjusters.lastDayOfMonth()); + return getEndOfDay(localDateTimeToDate(endOfDay)); + } + + public static Date getStartMonthOfDay(Date date) { + if (date == null) { + return null; + } + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault()); + LocalDateTime endOfDay = localDateTime.with(TemporalAdjusters.firstDayOfMonth()); + return getStartOfDay(localDateTimeToDate(endOfDay)); + } + + public static Date getMonthLastDay(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.DAY_OF_MONTH, 0); + calendar.add(Calendar.MONTH, 1); + return calendar.getTime(); + } + + /** + * 判断当天和传入的时间是否是同一天 + * + * @param thatDay 另一个日期 + * @return + */ + public static boolean isSameDay(Date thatDay) { + return isSameDay(thatDay, new Date()); + } + + /** + * 判断两个日期是否为同一天 + * + * @param date1 一个日期 + * @param date2 另一个日期 + * @return + */ + public static boolean isSameDay(Date date1, Date date2) { + if (date1 == null || date2 == null) { + return false; + } + Calendar thisDat = Calendar.getInstance(); + thisDat.setTime(date1); + Calendar thatDay = Calendar.getInstance(); + thatDay.setTime(date2); + return ( + thatDay.get(Calendar.YEAR) == thisDat.get(Calendar.YEAR) && + thatDay.get(Calendar.MONTH) == thisDat.get(Calendar.MONTH) && + thatDay.get(Calendar.DAY_OF_MONTH) == thisDat.get(Calendar.DAY_OF_MONTH) + ); + } + + /** + * 判断两个日期相差多少天 + * + * @param endTime + * @param startTime + * @return + */ + public static int dateMinus(Date endTime, Date startTime) { + return (int) ((endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60 * 24)); + } + + /** + * 时间戳转字符串 + * + * @param timeStamp + * @return + */ + //传入时间戳即可 + public static String conversionTime(String timeStamp) { + //yyyy-MM-dd HH:mm:ss 转换的时间格式 可以自定义 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + //转换 + String time = sdf.format(new Date(Long.parseLong(timeStamp))); + return time; + } + + /** + * 判断两个日期相差多少秒 + * + * @param endTime + * @param startTime + * @return + */ + public static int dateSeconds(Date endTime, Date startTime) { + return (int) ((endTime.getTime() - startTime.getTime()) / (1000)); + } + + /** + * 获得某天最小时间 2019-10-08 00:00:00 + * + * @param date + * @return + */ + public static Date getStartOfDay(Date date) { + if (date == null) { + return null; + } + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault()); + LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN); + return localDateTimeToDate(startOfDay); + } + + /** + * 获得某天最大时间 2019-10-08 23:59:59 + * + * @param date + * @return + */ + public static Date getEndOfDay(Date date) { + if (date == null) { + return null; + } + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault()); + LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX); + return localDateTimeToDate(endOfDay); + } + + /** + * localDateTime 转date + * + * @param localDateTime + * @return + */ + public static Date localDateTimeToDate(LocalDateTime localDateTime) { + return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * 将秒转为时分秒格式【01:01:01】 + * + * @param second 需要转化的秒数 + * @return + */ + public static String secondConvertHourMinSecond(Long second) { + String str = "00:00:00"; + if (second == null || second < 0) { + return str; + } + + // 得到小时 + long h = second / 3600; + str = h > 0 ? ((h < 10 ? ("0" + h) : h) + ":") : "00:"; + + // 得到分钟 + long m = (second % 3600) / 60; + str += (m < 10 ? ("0" + m) : m) + ":"; + + //得到剩余秒 + long s = second % 60; + str += (s < 10 ? ("0" + s) : s); + return str; + } + + + public static String getWeekOfDate(Date date) { + String[] weekDays = {"1", "2", "3", "4", "5", "6", "7"}; //"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六" + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + int w = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (w < 0) + w = 0; + if (Integer.valueOf(weekDays[w]) >= 2) { + return String.valueOf(Integer.valueOf(weekDays[w]) - 1); + } else { + return String.valueOf(7); + } + } + + /** + * 获取两个日期之间的所有年 + * @param startDate + * @param endDate + * @return + */ + public static List getAllYearsBetweenDates(Date startDate, Date endDate) { + List years = new ArrayList<>(); + Calendar startCal = Calendar.getInstance(); + startCal.setTime(startDate); + int startYear = startCal.get(Calendar.YEAR); + + Calendar endCal = Calendar.getInstance(); + endCal.setTime(endDate); + int endYear = endCal.get(Calendar.YEAR); + + for (int year = startYear; year <= endYear; year++) { + years.add(String.valueOf(year)); + } + return years; + } + + + /** + * 获取两个日期之间的所有月份 + * @param startDate + * @param endDate + * @return + */ + public static List getAllMonthsBetweenDates(Date startDate, Date endDate) { + List months = new ArrayList<>(); + //具体逻辑 + Calendar startCal = Calendar.getInstance(); + startCal.setTime(startDate); + startCal.set(Calendar.DAY_OF_MONTH, 1); // 设置为每个月的第一天 + + Calendar endCal = Calendar.getInstance(); + endCal.setTime(endDate); + endCal.set(Calendar.DAY_OF_MONTH, 1); // 设置为每个月的第一天 + + while (startCal.before(endCal) || startCal.equals(endCal)) { + int year = startCal.get(Calendar.YEAR); + int month = startCal.get(Calendar.MONTH) + 1; // Calendar中月份从0开始,所以要加1 + months.add(String.format("%d-%02d", year, month)); // 格式化年份和月份为 yyyy-MM + + startCal.add(Calendar.MONTH, 1); // 增加一个月 + } + return months; + } + + /** + * 获取两个日期间所有的日期 + * @param startDate + * @param endDate + * @return + */ + public static List getAllDaysBetweenDates(Date startDate, Date endDate){ + List days = new ArrayList<>(); + //具体实现 + Calendar startCal = Calendar.getInstance(); + startCal.setTime(startDate); + + Calendar endCal = Calendar.getInstance(); + endCal.setTime(endDate); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + while (startCal.before(endCal) || startCal.equals(endCal)) { + days.add(sdf.format(startCal.getTime())); + startCal.add(Calendar.DATE, 1); + } + return days; + } + /** + * 获取两个时间点之间的所有的整点时间 + * @param startDate + * @param endDate + * @return + */ + public static List getAllTimesBetweenDates(Date startDate, Date endDate,String format){ + // 创建一个字符串列表用来存放整点时间 + List timesList = new ArrayList<>(); + // 使用日历类来处理时间 + Calendar calendar = Calendar.getInstance(); + calendar.setTime(startDate); + + // 格式化时间为 "yyyy-MM-dd HH:mm" + SimpleDateFormat sdf = new SimpleDateFormat(format); + + // 逐小时遍历 +// while (calendar.getTime().before(endDate) || calendar.getTime().equals(endDate)) { + while (calendar.getTime().before(endDate)) { + // 将当前时间格式化为字符串并加入列表 + timesList.add(sdf.format(calendar.getTime())); + // 增加一个小时 + calendar.add(Calendar.HOUR_OF_DAY, 1); + } + return timesList; + } + public static List getAllTimesBetweenDates(){ + // 创建一个字符串数组,容量为24 + String[] times24 = new String[24]; + // 设置格式化器为24小时制 + java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("HH"); + // 获取0点到23点的每一个小时 + for (int i = 0; i < 24; i++) { + LocalTime time = LocalTime.of(i, 0); + times24[i] = time.format(formatter); + } + return new ArrayList<>(Arrays.asList(times24)); + } + + public static void main(String[] args) { +// System.out.println(getWeekOfDate(stringToDate("2023-07-31 00:00:00", DATE_TIME_PATTERN))); +// String ss = "2,3,4,5,6,7"; +// System.out.println(ss.contains(getWeekOfDate(stringToDate("2023-07-31 00:00:00", DATE_TIME_PATTERN)))); +// System.out.println(stringToDate("2020-03-23",DATE_PATTERN)); +// System.out.println("2020-03-23".length()); +// Date monthLastDay = endOfNextMonth(new Date()); +// System.out.println(format(monthLastDay,DATE_TIME_PATTERN)); +// String s = secondConvertHourMinSecond(360L); +// System.out.println(s); + + } +} diff --git a/src/main/java/com/ycwl/basic/utils/oss/OssUtil.java b/src/main/java/com/ycwl/basic/utils/oss/OssUtil.java new file mode 100644 index 0000000..c58bf51 --- /dev/null +++ b/src/main/java/com/ycwl/basic/utils/oss/OssUtil.java @@ -0,0 +1,87 @@ +package com.ycwl.basic.utils.oss; + +import cn.hutool.core.util.StrUtil; +import com.aliyun.oss.ClientException; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.OSSException; +import com.aliyun.oss.model.PutObjectRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.InputStream; + +/** + * @Author:longbinbin + * @Date:2024/11/6 10:12 + * + * 参考文档:https://help.aliyun.com/zh/oss/getting-started/sdk-quick-start?spm=a2c4g.11186623.help-menu-31815.d_1_5.7065a784pbkMck + */ +@Slf4j +@Component +public class OssUtil { + + @Value("${aliyun.oss.endpoint}") + private String endPoint; + @Value("${aliyun.oss.accessKeyId}") + private String accessKeyId; + @Value("${aliyun.oss.accessKeySecret}") + private String accessKeySecret; + @Value("${aliyun.oss.bucketName}") + private String bucketName; + @Value("${aliyun.oss.url}") + private String url; + + /** + * 上传文件到oss + * @param inputStream 文件数据输入流 + * @param filepath 文件路径 + * @param filename 文件名 + * @return + */ + public String uploadFile(InputStream inputStream, String filepath, String filename) { + String uploadFileName ; + if (StrUtil.isNotBlank(filepath)) { + uploadFileName = filepath+filename; + }else { + uploadFileName=filename; + } + OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret); + try { + PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName,uploadFileName, inputStream); + ossClient.putObject(putObjectRequest); + log.info("人脸照片上传成功"); + String fileUrl = url+uploadFileName; + return fileUrl; + } catch (OSSException oe) { + System.out.println("Caught an OSSException, which means your request made it to OSS, " + + "but was rejected with an error response for some reason."); + System.out.println("Error Message:" + oe.getErrorMessage()); + System.out.println("Error Code:" + oe.getErrorCode()); + System.out.println("Request ID:" + oe.getRequestId()); + System.out.println("Host ID:" + oe.getHostId()); + } catch (ClientException ce) { + System.out.println("Caught an ClientException, which means the client encountered " + + "a serious internal problem while trying to communicate with OSS, " + + "such as not being able to access the network."); + System.out.println("Error Message:" + ce.getMessage()); + } finally { + if (ossClient != null) { + ossClient.shutdown(); + } + } + + return "上传失败"; + } + + /** + * 删除文件 + * @param filePath 文件路径 + * @return + */ + public Boolean deleteFile(String filePath) { + + return null; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 29a10da..4a7c333 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -60,3 +60,16 @@ mybatis-plus: # 指定使用的日志配置文件 logging: config: classpath:logback-spring.xml +# 阿里oss配置: +aliyun: + oss: + # 地域节点 + endpoint: "" + # 密钥ID + accessKeyId: "" + # 密钥 + accessKeySecret: "" + # 存储空间名称 + bucketName: "" + # 文件访问路径 + url: "" \ No newline at end of file From 2b919a3c9f843432204946d5f7dab49affa0aefe Mon Sep 17 00:00:00 2001 From: longbinbin <2284496322@qq.com> Date: Fri, 6 Dec 2024 10:55:49 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=99=AF=E5=8C=BA?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=92=8C=E6=9C=BA=E4=BD=8D=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/AppDeviceController.java | 15 ++++ .../mobile/AppScenicController.java | 33 ++++++- .../ycwl/basic/mapper/pc/DeviceMapper.java | 3 + .../ycwl/basic/mapper/pc/ScenicMapper.java | 6 ++ .../model/mobile/scenic/ScenicAppVO.java | 70 +++++++++++++++ .../mobile/scenic/ScenicDeviceCountVO.java | 18 ++++ .../com/ycwl/basic/service/FileService.java | 9 +- .../impl/mobile/AppScenicServiceImpl.java | 52 +++++++++++ .../service/impl/pc/FaceServiceImpl.java | 20 +++-- .../service/mobile/AppScenicService.java | 20 +++++ .../java/com/ycwl/basic/utils/OssUtil.java | 8 +- .../com/ycwl/basic/utils/oss/OssUtil.java | 87 ------------------- src/main/resources/mapper/pc/DeviceMapper.xml | 14 +++ src/main/resources/mapper/pc/ScenicMapper.xml | 31 ++++++- 14 files changed, 287 insertions(+), 99 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/controller/mobile/AppDeviceController.java create mode 100644 src/main/java/com/ycwl/basic/model/mobile/scenic/ScenicAppVO.java create mode 100644 src/main/java/com/ycwl/basic/model/mobile/scenic/ScenicDeviceCountVO.java create mode 100644 src/main/java/com/ycwl/basic/service/impl/mobile/AppScenicServiceImpl.java create mode 100644 src/main/java/com/ycwl/basic/service/mobile/AppScenicService.java delete mode 100644 src/main/java/com/ycwl/basic/utils/oss/OssUtil.java diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppDeviceController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppDeviceController.java new file mode 100644 index 0000000..5077ccd --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppDeviceController.java @@ -0,0 +1,15 @@ +package com.ycwl.basic.controller.mobile; + +import io.swagger.annotations.Api; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @Author:longbinbin + * @Date:2024/12/6 10:18 + */ +@RestController +@RequestMapping("/api/mobile/scenic/v1") +@Api(tags = "设备相关接口") +public class AppDeviceController { +} diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppScenicController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppScenicController.java index 6e6b955..439095c 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppScenicController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppScenicController.java @@ -1,8 +1,17 @@ package com.ycwl.basic.controller.mobile; +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.model.mobile.scenic.ScenicAppVO; +import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; +import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; +import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; +import com.ycwl.basic.service.mobile.AppScenicService; +import com.ycwl.basic.service.pc.ScenicService; +import com.ycwl.basic.utils.ApiResponse; import io.swagger.annotations.Api; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; /** * @Author:longbinbin @@ -13,5 +22,25 @@ import org.springframework.web.bind.annotation.RestController; @Api(tags = "景区相关接口") public class AppScenicController { + @Autowired + private AppScenicService appScenicService; + + @ApiOperation("分页查询景区列表") + @PostMapping("/page") + public ApiResponse> pageQuery(@RequestBody ScenicReqQuery scenicReqQuery){ + return appScenicService.pageQuery(scenicReqQuery); + } + @ApiOperation("根据id查询景区详情") + @GetMapping("getDetails/{id}") + public ApiResponse getDetails(@PathVariable Long id){ + return appScenicService.getDetails(id); + } + + @ApiOperation("查询景区设备总数和拍到用户的机位数量") + @GetMapping("/deviceCountByScenicId/{scenicId}") + public ApiResponse deviceCountByScenicId(@PathVariable Long scenicId){ + return appScenicService.deviceCountByScenicId(scenicId); + } + } diff --git a/src/main/java/com/ycwl/basic/mapper/pc/DeviceMapper.java b/src/main/java/com/ycwl/basic/mapper/pc/DeviceMapper.java index 00e2aaa..d496826 100644 --- a/src/main/java/com/ycwl/basic/mapper/pc/DeviceMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/pc/DeviceMapper.java @@ -1,5 +1,6 @@ package com.ycwl.basic.mapper.pc; +import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.device.req.DeviceAddOrUpdateReq; import com.ycwl.basic.model.pc.device.req.DeviceReqQuery; @@ -23,4 +24,6 @@ public interface DeviceMapper { int updateStatus(Long id); List listByScenicId(Long scenicId); + + ScenicDeviceCountVO deviceCountByScenicId(Long scenicId, String userId); } diff --git a/src/main/java/com/ycwl/basic/mapper/pc/ScenicMapper.java b/src/main/java/com/ycwl/basic/mapper/pc/ScenicMapper.java index 82042f1..8a48c68 100644 --- a/src/main/java/com/ycwl/basic/mapper/pc/ScenicMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/pc/ScenicMapper.java @@ -1,10 +1,12 @@ package com.ycwl.basic.mapper.pc; +import com.ycwl.basic.model.mobile.scenic.ScenicAppVO; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicAddOrUpdateReq; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; +import com.ycwl.basic.utils.ApiResponse; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -41,4 +43,8 @@ public interface ScenicMapper { * @param scenicId */ void deleteConfigByscenicId(Long scenicId); + + List appList(ScenicReqQuery scenicReqQuery); + + ApiResponse getAppById(Long id); } diff --git a/src/main/java/com/ycwl/basic/model/mobile/scenic/ScenicAppVO.java b/src/main/java/com/ycwl/basic/model/mobile/scenic/ScenicAppVO.java new file mode 100644 index 0000000..642ca7f --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/mobile/scenic/ScenicAppVO.java @@ -0,0 +1,70 @@ +package com.ycwl.basic.model.mobile.scenic; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @Author:longbinbin + * @Date:2024/12/6 10:25 + */ +@Data +@ApiModel("移动端景区响应参数") +public class ScenicAppVO { + private Long id; + /** + * 景区名称 + */ + @ApiModelProperty("景区名称") + private String name; + /** + * 联系电话 + */ + @ApiModelProperty("联系电话") + private String phone; + /** + * 景区介绍 + */ + @ApiModelProperty("景区介绍") + private String introduction; + /** + * 经度 + */ + @ApiModelProperty("经度") + private BigDecimal longitude; + /*** + * 纬度 + */ + @ApiModelProperty("纬度") + private BigDecimal latitude; + /** + * 半径(km) + */ + @ApiModelProperty("半径(km)") + private BigDecimal radius; + /** + * 省份 + */ + @ApiModelProperty("省份") + private String province; + /** + * 城市 + */ + @ApiModelProperty("城市") + private String city; + /** + * 区 + */ + @ApiModelProperty("区") + private String area; + /** + * 详细地址 + */ + @ApiModelProperty("详细地址") + private String address; +} diff --git a/src/main/java/com/ycwl/basic/model/mobile/scenic/ScenicDeviceCountVO.java b/src/main/java/com/ycwl/basic/model/mobile/scenic/ScenicDeviceCountVO.java new file mode 100644 index 0000000..74c5278 --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/mobile/scenic/ScenicDeviceCountVO.java @@ -0,0 +1,18 @@ +package com.ycwl.basic.model.mobile.scenic; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * @Author:longbinbin + * @Date:2024/12/6 10:37 + */ +@Data +@ApiModel("移动端景区设备数量响应参数") +public class ScenicDeviceCountVO { + @ApiModelProperty("景区设备总数") + private Integer totalDeviceCount; + @ApiModelProperty("拍摄到用户的设备数量") + private Integer shotDeviceCount; +} diff --git a/src/main/java/com/ycwl/basic/service/FileService.java b/src/main/java/com/ycwl/basic/service/FileService.java index 6bfcb89..361db86 100644 --- a/src/main/java/com/ycwl/basic/service/FileService.java +++ b/src/main/java/com/ycwl/basic/service/FileService.java @@ -1,5 +1,7 @@ package com.ycwl.basic.service; +import cn.hutool.core.util.StrUtil; +import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.utils.OssUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -20,7 +22,12 @@ public class FileService { private OssUtil ossUtil; public String uploadFile(MultipartFile file) throws IOException { - return ossUtil.uploadFile(file.getInputStream(), Objects.requireNonNull(file.getOriginalFilename())); + String originalFilename = file.getOriginalFilename(); + if (StrUtil.isBlank(originalFilename)) { + throw new BaseException("文件上传失败,文件名不能为空"); + } + String fileName=System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf(".")); + return ossUtil.uploadFile(file.getInputStream(), fileName); } public Boolean delete(String fileName) { diff --git a/src/main/java/com/ycwl/basic/service/impl/mobile/AppScenicServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/mobile/AppScenicServiceImpl.java new file mode 100644 index 0000000..5367efb --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/impl/mobile/AppScenicServiceImpl.java @@ -0,0 +1,52 @@ +package com.ycwl.basic.service.impl.mobile; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.constant.BaseContextHandler; +import com.ycwl.basic.mapper.pc.DeviceMapper; +import com.ycwl.basic.mapper.pc.ScenicMapper; +import com.ycwl.basic.model.mobile.scenic.ScenicAppVO; +import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; +import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; +import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; +import com.ycwl.basic.service.mobile.AppScenicService; +import com.ycwl.basic.utils.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @Author:longbinbin + * @Date:2024/12/6 10:23 + */ +@Slf4j +@Service +public class AppScenicServiceImpl implements AppScenicService { + + @Autowired + private ScenicMapper scenicMapper; + @Autowired + private DeviceMapper deviceMapper; + + @Override + public ApiResponse> pageQuery(ScenicReqQuery scenicReqQuery) { + PageHelper.startPage(scenicReqQuery.getPageNum(), scenicReqQuery.getPageSize()); + List list = scenicMapper.appList(scenicReqQuery); + PageInfo pageInfo = new PageInfo<>(list); + return ApiResponse.success(pageInfo); + } + + @Override + public ApiResponse deviceCountByScenicId(Long scenicId) { + String userId = BaseContextHandler.getUserId(); + ScenicDeviceCountVO scenicDeviceCountVO=deviceMapper.deviceCountByScenicId(scenicId,userId); + return ApiResponse.success(scenicDeviceCountVO); + } + + @Override + public ApiResponse getDetails(Long id) { + return scenicMapper.getAppById(id); + } +} diff --git a/src/main/java/com/ycwl/basic/service/impl/pc/FaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/pc/FaceServiceImpl.java index b40c8e4..c3bfcf9 100644 --- a/src/main/java/com/ycwl/basic/service/impl/pc/FaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/impl/pc/FaceServiceImpl.java @@ -2,16 +2,15 @@ package com.ycwl.basic.service.impl.pc; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; +import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.exception.BaseException; import com.ycwl.basic.mapper.pc.FaceMapper; +import com.ycwl.basic.model.jwt.JwtInfo; import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.req.FaceReqQuery; import com.ycwl.basic.model.pc.face.resp.FaceRespVO; import com.ycwl.basic.service.pc.FaceService; -import com.ycwl.basic.utils.ApiResponse; -import com.ycwl.basic.utils.DateUtils; -import com.ycwl.basic.utils.SnowFlakeUtil; -import com.ycwl.basic.utils.oss.OssUtil; +import com.ycwl.basic.utils.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -92,7 +91,7 @@ public class FaceServiceImpl implements FaceService { @Override public ApiResponse faceUPload(MultipartFile file) { //TODO 获取用户信息 - String userId="1"; + String userId = BaseContextHandler.getUserId(); //1、上传人脸照片 String facaeUrl = uploadFileALiOss(file, userId); //TODO 2、人脸照片有效性校验 @@ -104,13 +103,20 @@ public class FaceServiceImpl implements FaceService { //校验成功,保存用户人脸信息,将访问人脸照片访问地址响应给前端 FaceEntity faceEntity = new FaceEntity(); + faceEntity.setId(SnowFlakeUtil.getLongId()); + faceEntity.setMemberId(Long.parseLong(userId)); + faceEntity.setFaceUrl(facaeUrl); +// faceEntity.setScore(); +// faceEntity.setMatchSampleIds(); +// faceEntity.setFirstMatchRate(); +// faceEntity.setMatchResult(); //TODO 人脸数据存库 faceMapper.add(faceEntity); return ApiResponse.success(facaeUrl); }else { //校验失败,删除,提示重新上传 - + ossUtil.deleteFile(facaeUrl); throw new BaseException("人脸照片校验失败,请重新上传"); } @@ -147,6 +153,6 @@ public class FaceServiceImpl implements FaceService { log.error("文件上传失败!", e); return null; } - return ossUtil.uploadFile(inputStream,filePath,fileName) ; + return ossUtil.uploadFile(inputStream,filePath+fileName) ; } } diff --git a/src/main/java/com/ycwl/basic/service/mobile/AppScenicService.java b/src/main/java/com/ycwl/basic/service/mobile/AppScenicService.java new file mode 100644 index 0000000..22f5264 --- /dev/null +++ b/src/main/java/com/ycwl/basic/service/mobile/AppScenicService.java @@ -0,0 +1,20 @@ +package com.ycwl.basic.service.mobile; + +import com.github.pagehelper.PageInfo; +import com.ycwl.basic.model.mobile.scenic.ScenicAppVO; +import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO; +import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; +import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; +import com.ycwl.basic.utils.ApiResponse; + +/** + * @Author:longbinbin + * @Date:2024/12/6 10:23 + */ +public interface AppScenicService { + ApiResponse> pageQuery(ScenicReqQuery scenicReqQuery); + + ApiResponse deviceCountByScenicId(Long scenicId); + + ApiResponse getDetails(Long id); +} diff --git a/src/main/java/com/ycwl/basic/utils/OssUtil.java b/src/main/java/com/ycwl/basic/utils/OssUtil.java index 054d20d..df6e763 100644 --- a/src/main/java/com/ycwl/basic/utils/OssUtil.java +++ b/src/main/java/com/ycwl/basic/utils/OssUtil.java @@ -20,8 +20,14 @@ public class OssUtil { @Autowired private OssConfig ossConfig; + /** + * 上传文件到oss + * @param inputStream 文件数据流 + * @param filename 文件全路径名称 + * @return + */ public String uploadFile(InputStream inputStream, String filename) { - String uploadFileName = ossConfig.getObjectName() + System.currentTimeMillis() + filename.substring(filename.lastIndexOf(".")); + String uploadFileName = ossConfig.getObjectName() + filename; OSS ossClient = new OSSClientBuilder().build(ossConfig.getEndPoint(), ossConfig.getAccessKeyId(), ossConfig.getAccessKeySecret()); try { PutObjectRequest putObjectRequest = new PutObjectRequest(ossConfig.getBucketName(), uploadFileName, inputStream); diff --git a/src/main/java/com/ycwl/basic/utils/oss/OssUtil.java b/src/main/java/com/ycwl/basic/utils/oss/OssUtil.java deleted file mode 100644 index c58bf51..0000000 --- a/src/main/java/com/ycwl/basic/utils/oss/OssUtil.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.ycwl.basic.utils.oss; - -import cn.hutool.core.util.StrUtil; -import com.aliyun.oss.ClientException; -import com.aliyun.oss.OSS; -import com.aliyun.oss.OSSClientBuilder; -import com.aliyun.oss.OSSException; -import com.aliyun.oss.model.PutObjectRequest; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.io.InputStream; - -/** - * @Author:longbinbin - * @Date:2024/11/6 10:12 - * - * 参考文档:https://help.aliyun.com/zh/oss/getting-started/sdk-quick-start?spm=a2c4g.11186623.help-menu-31815.d_1_5.7065a784pbkMck - */ -@Slf4j -@Component -public class OssUtil { - - @Value("${aliyun.oss.endpoint}") - private String endPoint; - @Value("${aliyun.oss.accessKeyId}") - private String accessKeyId; - @Value("${aliyun.oss.accessKeySecret}") - private String accessKeySecret; - @Value("${aliyun.oss.bucketName}") - private String bucketName; - @Value("${aliyun.oss.url}") - private String url; - - /** - * 上传文件到oss - * @param inputStream 文件数据输入流 - * @param filepath 文件路径 - * @param filename 文件名 - * @return - */ - public String uploadFile(InputStream inputStream, String filepath, String filename) { - String uploadFileName ; - if (StrUtil.isNotBlank(filepath)) { - uploadFileName = filepath+filename; - }else { - uploadFileName=filename; - } - OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret); - try { - PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName,uploadFileName, inputStream); - ossClient.putObject(putObjectRequest); - log.info("人脸照片上传成功"); - String fileUrl = url+uploadFileName; - return fileUrl; - } catch (OSSException oe) { - System.out.println("Caught an OSSException, which means your request made it to OSS, " - + "but was rejected with an error response for some reason."); - System.out.println("Error Message:" + oe.getErrorMessage()); - System.out.println("Error Code:" + oe.getErrorCode()); - System.out.println("Request ID:" + oe.getRequestId()); - System.out.println("Host ID:" + oe.getHostId()); - } catch (ClientException ce) { - System.out.println("Caught an ClientException, which means the client encountered " - + "a serious internal problem while trying to communicate with OSS, " - + "such as not being able to access the network."); - System.out.println("Error Message:" + ce.getMessage()); - } finally { - if (ossClient != null) { - ossClient.shutdown(); - } - } - - return "上传失败"; - } - - /** - * 删除文件 - * @param filePath 文件路径 - * @return - */ - public Boolean deleteFile(String filePath) { - - return null; - } -} diff --git a/src/main/resources/mapper/pc/DeviceMapper.xml b/src/main/resources/mapper/pc/DeviceMapper.xml index 9be8f4e..7422a03 100644 --- a/src/main/resources/mapper/pc/DeviceMapper.xml +++ b/src/main/resources/mapper/pc/DeviceMapper.xml @@ -58,4 +58,18 @@ from device d where d.scenic_id = #{scenicId} + \ No newline at end of file diff --git a/src/main/resources/mapper/pc/ScenicMapper.xml b/src/main/resources/mapper/pc/ScenicMapper.xml index cb3e504..cbc158e 100644 --- a/src/main/resources/mapper/pc/ScenicMapper.xml +++ b/src/main/resources/mapper/pc/ScenicMapper.xml @@ -117,12 +117,41 @@ left join scenic_config c on s.id = c.id where s.id = #{id} + + - +