35 changed files with 1723 additions and 5 deletions
@ -0,0 +1,24 @@ |
|||
package com.youlai.boot.common.annotation; |
|||
|
|||
import com.youlai.boot.common.validator.EnumValidator; |
|||
import jakarta.validation.Constraint; |
|||
import jakarta.validation.Payload; |
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
@Documented |
|||
@Constraint(validatedBy = EnumValidator.class) |
|||
@Target({ElementType.FIELD, ElementType.PARAMETER}) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
public @interface EnumValid { |
|||
|
|||
String message() default "值不在枚举范围内"; |
|||
|
|||
Class<?>[] groups() default {}; |
|||
|
|||
Class<? extends Payload>[] payload() default {}; |
|||
|
|||
Class<? extends Enum<?>> enumClass(); |
|||
|
|||
String enumMethod() default "getValue"; // 获取值的方法
|
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
package com.youlai.boot.common.constant; |
|||
|
|||
public class CommonConstants { |
|||
|
|||
//最多上传的图片数量
|
|||
public static final int STRAY_ANIMAL_IMAGE_NUM_LIMIT = 6; |
|||
//最多上传的视频数量
|
|||
public static final int STRAY_ANIMAL_VIDEO_NUM_LIMIT = 2; |
|||
//geohash的level
|
|||
public static final int GEOHASH_LEVEL = 12; |
|||
|
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
package com.youlai.boot.common.util; |
|||
|
|||
import ch.hsr.geohash.GeoHash; |
|||
|
|||
public class CoordinateTransformUtils { |
|||
|
|||
// Earth radius in meters
|
|||
private static final double EARTH_RADIUS = 6378137.0; |
|||
|
|||
// 定义偏移量常量
|
|||
private static final double PI = 3.14159265358979323846; |
|||
private static final double X_PI = PI * 3000.0 / 180.0; |
|||
|
|||
/** |
|||
* GCJ-02 转 WGS-84 |
|||
* @param gcjLng 高德经度 |
|||
* @param gcjLat 高德纬度 |
|||
* @return 转换后的 WGS-84 坐标(经度,纬度) |
|||
*/ |
|||
public static double[] gcj02ToWgs84(double gcjLng, double gcjLat) { |
|||
if (outOfChina(gcjLng, gcjLat)) { |
|||
return new double[]{gcjLng, gcjLat}; // 如果不在中国范围内,直接返回
|
|||
} |
|||
|
|||
double dLat = transformLat(gcjLng - 105.0, gcjLat - 35.0); |
|||
double dLng = transformLng(gcjLng - 105.0, gcjLat - 35.0); |
|||
double radLat = gcjLat / 180.0 * PI; |
|||
double magic = Math.sin(radLat); |
|||
magic = 1 - 0.00669342162296594323 * magic * magic; |
|||
double sqrtMagic = Math.sqrt(magic); |
|||
|
|||
dLat = (dLat * 180.0) / ((EARTH_RADIUS * (1 - 0.00669342162296594323)) / (sqrtMagic * sqrtMagic) * PI); |
|||
dLng = (dLng * 180.0) / (EARTH_RADIUS / sqrtMagic * Math.cos(radLat) * PI); |
|||
|
|||
double wgsLat = gcjLat - dLat; |
|||
double wgsLng = gcjLng - dLng; |
|||
|
|||
return new double[]{wgsLng, wgsLat}; |
|||
} |
|||
|
|||
/** |
|||
* 判断坐标是否在中国范围内 |
|||
* @param lng 经度 |
|||
* @param lat 纬度 |
|||
* @return true表示不在中国范围 |
|||
*/ |
|||
private static boolean outOfChina(double lng, double lat) { |
|||
return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271; |
|||
} |
|||
|
|||
/** |
|||
* 变换纬度的函数 |
|||
*/ |
|||
private static double transformLat(double lng, double lat) { |
|||
double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); |
|||
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; |
|||
ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; |
|||
ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320.0 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; |
|||
return ret; |
|||
} |
|||
|
|||
/** |
|||
* 变换经度的函数 |
|||
*/ |
|||
private static double transformLng(double lng, double lat) { |
|||
double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); |
|||
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; |
|||
ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; |
|||
ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; |
|||
return ret; |
|||
} |
|||
|
|||
private static void geoHashWithCharacterPrecision(double lat, double lng, int precision) { |
|||
String geoHash = GeoHash.withCharacterPrecision(lat, lng, precision).toBase32(); |
|||
System.out.println("geoHash度: " + geoHash); |
|||
} |
|||
|
|||
public static void main(String[] args) { |
|||
// 示例:GCJ-02 坐标
|
|||
double gcjLng = 118.08125; // 高德经度
|
|||
double gcjLat = 24.606929; // 高德纬度
|
|||
|
|||
// 转换为 WGS-84 坐标
|
|||
double[] wgs84 = gcj02ToWgs84(gcjLng, gcjLat); |
|||
|
|||
System.out.println("WGS-84 经度: " + wgs84[0]); |
|||
System.out.println("WGS-84 纬度: " + wgs84[1]); |
|||
|
|||
// Java生成示例
|
|||
|
|||
geoHashWithCharacterPrecision(39.915123456, 116.404123456, 12); |
|||
geoHashWithCharacterPrecision(39.915123457, 116.404123457, 12); |
|||
geoHashWithCharacterPrecision(39.915, 116.404, 11); |
|||
geoHashWithCharacterPrecision(39.915, 116.404, 10); |
|||
geoHashWithCharacterPrecision(39.915, 116.404, 9); |
|||
geoHashWithCharacterPrecision(39.915, 116.404, 8); |
|||
geoHashWithCharacterPrecision(39.915, 116.404, 7); |
|||
geoHashWithCharacterPrecision(39.915, 116.404, 6); |
|||
geoHashWithCharacterPrecision(39.915, 116.404, 5); |
|||
geoHashWithCharacterPrecision(39.915, 116.404, 4); |
|||
|
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,438 @@ |
|||
package com.youlai.boot.common.util; |
|||
|
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import javax.imageio.ImageIO; |
|||
import java.awt.image.BufferedImage; |
|||
import java.io.*; |
|||
import java.net.URLEncoder; |
|||
import java.text.DecimalFormat; |
|||
import java.util.zip.ZipEntry; |
|||
import java.util.zip.ZipOutputStream; |
|||
|
|||
public class FileUtils { |
|||
private static final Logger logger = LoggerFactory.getLogger(FileUtils.class); |
|||
|
|||
/** |
|||
* 保存文件 |
|||
* @param multipartFile 文件 |
|||
* @param filePath 存储路径 |
|||
* @param fileName 存储文件名 |
|||
* @return 文件url |
|||
*/ |
|||
public static boolean saveFile(MultipartFile multipartFile, String filePath, String fileName) { |
|||
boolean result = false; |
|||
if (multipartFile.isEmpty()) |
|||
return true; |
|||
|
|||
File file = new File(filePath); |
|||
if (!file.exists()) { |
|||
file.mkdirs(); |
|||
} |
|||
|
|||
try ( |
|||
InputStream inputStream = multipartFile.getInputStream(); |
|||
BufferedOutputStream bos = new BufferedOutputStream( |
|||
new FileOutputStream(filePath + File.separator + fileName)) |
|||
) { |
|||
byte[] bs = new byte[1024]; |
|||
int len; |
|||
while ((len = inputStream.read(bs)) != -1) { |
|||
bos.write(bs, 0, len); |
|||
} |
|||
bos.flush(); |
|||
result = true; |
|||
} catch (IOException e) { |
|||
logger.error("SaveFile ERROR", e); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 下载文件 |
|||
* @param response |
|||
* @param file |
|||
* @param fileName |
|||
* @return |
|||
*/ |
|||
public static String downloadFile(HttpServletResponse response, File file, String fileName){ |
|||
if (fileName != null) { |
|||
//当前是从该工程的WEB-INF//File//下获取文件(该目录可以在下面一行代码配置)然后下载到C:\\users\\downloads即本机的默认下载的目录
|
|||
if (file.exists()) { |
|||
response.setContentType("application/force-download");// 设置强制下载不打开
|
|||
response.addHeader("Access-Control-Expose-Headers","Content-Disposition"); |
|||
response.addHeader("Content-Disposition", |
|||
"attachment;fileName=" + fileName);// 设置文件名
|
|||
byte[] buffer = new byte[1024]; |
|||
try (FileInputStream fis = new FileInputStream(file)){ |
|||
BufferedInputStream bis = new BufferedInputStream(fis); |
|||
OutputStream os = response.getOutputStream(); |
|||
int i = bis.read(buffer); |
|||
while (i != -1) { |
|||
os.write(buffer, 0, i); |
|||
i = bis.read(buffer); |
|||
} |
|||
} catch (Exception e) { |
|||
logger.error("-----downloadFile---error:"+e.getMessage(),e); |
|||
} |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public static void downloadExcelFile(HttpServletResponse response,File file,String fileName){ |
|||
try { |
|||
if (fileName != null) { |
|||
logger.debug(file.getAbsolutePath()); |
|||
//当前是从该工程的WEB-INF//File//下获取文件(该目录可以在下面一行代码配置)然后下载到C:\\users\\downloads即本机的默认下载的目录
|
|||
if (file.exists()) { |
|||
response.setContentType("application/octet-stream"); |
|||
// 告诉浏览器用什么软件可以打开此文件
|
|||
response.addHeader("Access-Control-Expose-Headers","Content-Disposition"); |
|||
response.addHeader("Content-Disposition","attachment;fileName=" +URLEncoder.encode(fileName, "UTF-8"));// 设置文件名
|
|||
byte[] buffer = new byte[1024]; |
|||
try (FileInputStream fis = new FileInputStream(file)){ |
|||
BufferedInputStream bis = new BufferedInputStream(fis); |
|||
OutputStream os = response.getOutputStream(); |
|||
int i = bis.read(buffer); |
|||
while (i != -1) { |
|||
os.write(buffer, 0, i); |
|||
i = bis.read(buffer); |
|||
} |
|||
} catch (Exception e) { |
|||
logger.error("-----downloadFile---error:"+e.getMessage(),e); |
|||
} |
|||
}else{ |
|||
logger.error("-----downloadFile---文件不存在------"+file.getName()); |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
logger.error("-----downloadFile---error:"+e.getMessage(),e); |
|||
} |
|||
} |
|||
|
|||
public static void downloadExcelFile(HttpServletResponse response,InputStream fis,String fileName){ |
|||
try { |
|||
if (fileName != null) { |
|||
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition"); |
|||
//通知客服文件的MIME类型
|
|||
response.setContentType("application/vnd.ms-excel;charset=UTF-8"); |
|||
//获取文件的路径
|
|||
response.setCharacterEncoding("UTF-8"); |
|||
// 告诉浏览器用什么软件可以打开此文件
|
|||
response.addHeader("Access-Control-Expose-Headers","Content-Disposition"); |
|||
response.addHeader("Content-Disposition","attachment;fileName=" +URLEncoder.encode(fileName, "UTF-8"));// 设置文件名
|
|||
byte[] buffer = new byte[1024]; |
|||
try (BufferedInputStream bis = new BufferedInputStream(fis)){ |
|||
OutputStream os = response.getOutputStream(); |
|||
int i = bis.read(buffer); |
|||
while (i != -1) { |
|||
os.write(buffer, 0, i); |
|||
i = bis.read(buffer); |
|||
} |
|||
response.setHeader("Content-Length", String.valueOf(fis.available())); |
|||
} catch (Exception e) { |
|||
logger.error("-----downloadFile---error:"+e.getMessage(),e); |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
logger.error("-----downloadFile---error:"+e.getMessage(),e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将二进制转换成文件保存 |
|||
* @param instreams 二进制流 |
|||
* @param imgPath 图片的保存路径 |
|||
* @param imgName 图片的名称 |
|||
* @return |
|||
* 1:保存正常 |
|||
* 0:保存失败 |
|||
*/ |
|||
public static int saveToImgByInputStream(InputStream instreams,String imgPath,String imgName){ |
|||
int stateInt = 1; |
|||
if(instreams != null){ |
|||
File file=new File(imgPath,imgName);//可以是任何图片格式.jpg,.png等
|
|||
try (FileOutputStream fos=new FileOutputStream(file);){ |
|||
byte[] b = new byte[1024]; |
|||
int nRead = 0; |
|||
while ((nRead = instreams.read(b)) != -1) { |
|||
fos.write(b, 0, nRead); |
|||
} |
|||
fos.flush(); |
|||
} catch (Exception e) { |
|||
stateInt = 0; |
|||
logger.error("saveToImgByInputStream ERROR",e); |
|||
} |
|||
} |
|||
return stateInt; |
|||
} |
|||
|
|||
public static String formetFileSize(long fileS) {//转换文件大小
|
|||
DecimalFormat df = new DecimalFormat("#.00"); |
|||
String fileSizeString = ""; |
|||
if (fileS < 1024) { |
|||
fileSizeString = df.format((double) fileS) + "B"; |
|||
} else if (fileS < 1048576) { |
|||
fileSizeString = df.format((double) fileS / 1024) + "K"; |
|||
} else if (fileS < 1073741824) { |
|||
fileSizeString = df.format((double) fileS / 1048576) + "M"; |
|||
} else { |
|||
fileSizeString = df.format((double) fileS / 1073741824) + "G"; |
|||
} |
|||
return fileSizeString; |
|||
} |
|||
|
|||
/** |
|||
* 获取文件大小转M |
|||
* @param fileS |
|||
* @return |
|||
*/ |
|||
public static String changeFileSize(long fileS) {//转换文件大小
|
|||
DecimalFormat df = new DecimalFormat("#0.00"); |
|||
return df.format((double) fileS / 1048576); |
|||
} |
|||
|
|||
/** |
|||
* zip文件压缩 |
|||
* @param inputFile 待压缩文件夹/文件名 |
|||
* @param outputFile 生成的压缩包名字 |
|||
*/ |
|||
public static void zipCompress(String inputFile, String outputFile) throws Exception { |
|||
ZipOutputStream out = null; |
|||
BufferedOutputStream bos = null; |
|||
try { |
|||
File fileParent = new File(outputFile).getParentFile(); |
|||
if (!fileParent.exists()) |
|||
fileParent.mkdirs();// 能创建多级目录
|
|||
//创建zip输出流
|
|||
out = new ZipOutputStream(new FileOutputStream(outputFile)); |
|||
//创建缓冲输出流
|
|||
bos = new BufferedOutputStream(out); |
|||
File input = new File(inputFile); |
|||
compress(out, bos, input,null); |
|||
bos.close(); |
|||
out.close(); |
|||
} finally { |
|||
if(bos != null) bos.close(); |
|||
if(out != null) out.close(); |
|||
} |
|||
|
|||
} |
|||
|
|||
public static void zipMultiFile(String filePath, String zipPath) { |
|||
File fileParent = new File(zipPath).getParentFile(); |
|||
if (!fileParent.exists()) |
|||
fileParent.mkdirs();// 能创建多级目录
|
|||
File file = new File(filePath); //获取其file对象
|
|||
File[] srcFiles = file.listFiles(); //遍历path下的文件和目录,放在File数组中
|
|||
if (null != srcFiles && srcFiles.length > 0) { |
|||
zipFiles(srcFiles, new File(zipPath)); |
|||
} |
|||
} |
|||
|
|||
public static void zipFiles(File[] srcFiles, File zipFile) { |
|||
// 判断压缩后的文件存在不,不存在则创建
|
|||
if (!zipFile.exists()) { |
|||
try { |
|||
zipFile.createNewFile(); |
|||
} catch (IOException e) { |
|||
logger.info("导出zip, createNewFile出错", e); |
|||
} |
|||
} |
|||
// // 创建 FileOutputStream 对象
|
|||
// FileOutputStream fileOutputStream = null;
|
|||
// // 创建 ZipOutputStream
|
|||
// ZipOutputStream zipOutputStream = null;
|
|||
// 创建 FileInputStream 对象
|
|||
FileInputStream fileInputStream = null; |
|||
try (FileOutputStream fileOutputStream = new FileOutputStream(zipFile); |
|||
ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream); |
|||
){ |
|||
// // 实例化 FileOutputStream 对象
|
|||
// fileOutputStream = new FileOutputStream(zipFile);
|
|||
// // 实例化 ZipOutputStream 对象
|
|||
// zipOutputStream = new ZipOutputStream(fileOutputStream);
|
|||
// 创建 ZipEntry 对象
|
|||
ZipEntry zipEntry = null; |
|||
// 遍历源文件数组
|
|||
for (int i = 0; i < srcFiles.length; i++) { |
|||
// 将源文件数组中的当前文件读入 FileInputStream 流中
|
|||
fileInputStream = new FileInputStream(srcFiles[i]); |
|||
// 实例化 ZipEntry 对象,源文件数组中的当前文件
|
|||
zipEntry = new ZipEntry(srcFiles[i].getName()); |
|||
zipOutputStream.putNextEntry(zipEntry); |
|||
// 该变量记录每次真正读的字节个数
|
|||
int len; |
|||
// 定义每次读取的字节数组
|
|||
byte[] buffer = new byte[1024]; |
|||
while ((len = fileInputStream.read(buffer)) > 0) { |
|||
zipOutputStream.write(buffer, 0, len); |
|||
} |
|||
//数组中每个file使用完后,要关闭对应的FileInputStream流
|
|||
fileInputStream.close(); |
|||
} |
|||
} catch (IOException e) { |
|||
logger.info("导出zipFiles出错", e); |
|||
} finally { |
|||
try { |
|||
if(fileInputStream != null) fileInputStream.close(); |
|||
} catch (IOException e) { |
|||
logger.info("导出zipFiles, finally出错", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param name 压缩文件名,可以写为null保持默认 |
|||
*/ |
|||
//递归压缩
|
|||
public static void compress(ZipOutputStream out, BufferedOutputStream bos, File input, String name) throws IOException { |
|||
FileInputStream fos = null; |
|||
BufferedInputStream bis = null; |
|||
try { |
|||
if (name == null) { |
|||
name = input.getName(); |
|||
} |
|||
//如果路径为目录(文件夹)
|
|||
if (input.isDirectory()) { |
|||
//取出文件夹中的文件(或子文件夹)
|
|||
File[] flist = input.listFiles(); |
|||
if (flist.length == 0)//如果文件夹为空,则只需在目的地zip文件中写入一个目录进入
|
|||
{ |
|||
out.putNextEntry(new ZipEntry(name + File.separator)); |
|||
} else//如果文件夹不为空,则递归调用compress,文件夹中的每一个文件(或文件夹)进行压缩
|
|||
{ |
|||
for (int i = 0; i < flist.length; i++) { |
|||
compress(out, bos, flist[i], name + File.separator + flist[i].getName()); |
|||
} |
|||
} |
|||
} else//如果不是目录(文件夹),即为文件,则先写入目录进入点,之后将文件写入zip文件中
|
|||
{ |
|||
out.putNextEntry(new ZipEntry(name)); |
|||
fos = new FileInputStream(input); |
|||
bis = new BufferedInputStream(fos); |
|||
int len; |
|||
//将源文件写入到zip文件中
|
|||
byte[] buf = new byte[1024]; |
|||
while ((len = bis.read(buf)) != -1) { |
|||
bos.write(buf,0,len); |
|||
} |
|||
bis.close(); |
|||
fos.close(); |
|||
} |
|||
} finally { |
|||
if(bis != null) bis.close(); |
|||
if(fos != null) fos.close(); |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 删除文件,可以是文件或文件夹 |
|||
* |
|||
* @param fileName |
|||
* 要删除的文件名 |
|||
* @return 删除成功返回true,否则返回false |
|||
*/ |
|||
public static boolean delete(String fileName) { |
|||
File file = new File(fileName); |
|||
if (!file.exists()) { |
|||
System.out.println("删除文件失败:" + fileName + "不存在!"); |
|||
return false; |
|||
} else { |
|||
if (file.isFile()) |
|||
return deleteFile(fileName); |
|||
else |
|||
return deleteDirectory(fileName); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 删除单个文件 |
|||
* @param fileName |
|||
* 要删除的文件的文件名 |
|||
* @return 单个文件删除成功返回true,否则返回false |
|||
*/ |
|||
public static boolean deleteFile(String fileName) { |
|||
File file = new File(fileName); |
|||
// 如果文件路径所对应的文件存在,并且是一个文件,则直接删除
|
|||
if (file.exists() && file.isFile()) { |
|||
if (file.delete()) { |
|||
logger.info("删除单个文件" + fileName + "成功!"); |
|||
return true; |
|||
} else { |
|||
logger.info("删除单个文件" + fileName + "失败!"); |
|||
return false; |
|||
} |
|||
} else { |
|||
logger.info("删除单个文件失败:" + fileName + "不存在!"); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 删除目录及目录下的文件 |
|||
* |
|||
* @param dir |
|||
* 要删除的目录的文件路径 |
|||
* @return 目录删除成功返回true,否则返回false |
|||
*/ |
|||
public static boolean deleteDirectory(String dir) { |
|||
// 如果dir不以文件分隔符结尾,自动添加文件分隔符
|
|||
if (!dir.endsWith(File.separator)) |
|||
dir = dir + File.separator; |
|||
File dirFile = new File(dir); |
|||
// 如果dir对应的文件不存在,或者不是一个目录,则退出
|
|||
if ((!dirFile.exists()) || (!dirFile.isDirectory())) { |
|||
logger.info("删除目录失败:" + dir + "不存在!"); |
|||
return false; |
|||
} |
|||
boolean flag = true; |
|||
// 删除文件夹中的所有文件包括子目录
|
|||
File[] files = dirFile.listFiles(); |
|||
for (int i = 0; i < files.length; i++) { |
|||
// 删除子文件
|
|||
if (files[i].isFile()) { |
|||
flag = deleteFile(files[i].getAbsolutePath()); |
|||
if (!flag) |
|||
break; |
|||
} |
|||
// 删除子目录
|
|||
else if (files[i].isDirectory()) { |
|||
flag = deleteDirectory(files[i].getAbsolutePath()); |
|||
if (!flag) |
|||
break; |
|||
} |
|||
} |
|||
if (!flag) { |
|||
logger.info("删除目录失败!"); |
|||
return false; |
|||
} |
|||
// 删除当前目录
|
|||
if (dirFile.delete()) { |
|||
logger.info("删除目录" + dir + "成功!"); |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public static InputStream bufferedImageToInputStream(BufferedImage image, String formatName) throws IOException { |
|||
ByteArrayOutputStream os = new ByteArrayOutputStream(); |
|||
ImageIO.write(image, formatName, os); // 写入格式,比如 "png", "jpg"
|
|||
return new ByteArrayInputStream(os.toByteArray()); |
|||
} |
|||
|
|||
public static String getFileExtension(MultipartFile file) { |
|||
String originalFilename = file.getOriginalFilename(); |
|||
if (originalFilename != null && originalFilename.contains(".")) { |
|||
return originalFilename.substring(originalFilename.lastIndexOf(".") + 1); |
|||
} |
|||
return ""; // 没有扩展名
|
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
package com.youlai.boot.common.util; |
|||
|
|||
import org.bytedeco.javacv.FFmpegFrameGrabber; |
|||
import org.bytedeco.javacv.Frame; |
|||
import org.bytedeco.javacv.Java2DFrameConverter; |
|||
|
|||
import java.awt.image.BufferedImage; |
|||
|
|||
public class JavaVCUtils { |
|||
|
|||
/** |
|||
* 获取视频时长(秒) |
|||
*/ |
|||
public static double getVideoDuration(String videoPath) throws Exception { |
|||
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(videoPath)) { |
|||
grabber.start(); |
|||
|
|||
long microSeconds = grabber.getLengthInTime(); |
|||
return microSeconds / 1_000_000.0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取视频封面(指定秒数截图) |
|||
*/ |
|||
public static BufferedImage getVideoThumbnail(String videoPath, int second) throws Exception { |
|||
|
|||
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(videoPath)) { |
|||
grabber.start(); |
|||
|
|||
// 防止越界
|
|||
long maxTimestamp = grabber.getLengthInTime(); |
|||
long targetTimestamp = second * 1_000_000L; |
|||
|
|||
if (targetTimestamp > maxTimestamp) { |
|||
targetTimestamp = 0; |
|||
} |
|||
|
|||
grabber.setTimestamp(targetTimestamp); |
|||
|
|||
Frame frame = grabber.grabImage(); |
|||
|
|||
if (frame == null || frame.image == null) { |
|||
return null; |
|||
} |
|||
|
|||
// 不用 Java2DFrameUtils(避免 OpenCV JNI)
|
|||
Java2DFrameConverter converter = new Java2DFrameConverter(); |
|||
return converter.convert(frame); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
package com.youlai.boot.common.util; |
|||
|
|||
import java.util.Random; |
|||
|
|||
/** |
|||
* @author Mr.Jiang |
|||
* @time 2022年5月5日 下午8:57:20 |
|||
*/ |
|||
public class RandomNumberUtils { |
|||
private RandomNumberUtils() { |
|||
} |
|||
public static String createRandomNumber(int length) { |
|||
StringBuilder strBuffer = new StringBuilder(); |
|||
Random rd = new Random(); |
|||
for (int i = 0; i < length; i++) { |
|||
strBuffer.append(rd.nextInt(10)); |
|||
} |
|||
return strBuffer.toString(); |
|||
} |
|||
|
|||
|
|||
//生成随机数字和字母,
|
|||
public static String createRandomLowerLetterAndNumber(int length) { |
|||
String val = ""; |
|||
Random random = new Random(); |
|||
//参数length,表示生成几位随机数
|
|||
for(int i = 0; i < length; i++) { |
|||
String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; |
|||
//输出字母还是数字
|
|||
if( "char".equalsIgnoreCase(charOrNum) ) { |
|||
//输出是大写字母还是小写字母
|
|||
// int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
|
|||
//输出小写字母
|
|||
int temp = 97; |
|||
val += (char)(random.nextInt(26) + temp); |
|||
} else if( "num".equalsIgnoreCase(charOrNum) ) { |
|||
val += String.valueOf(random.nextInt(10)); |
|||
} |
|||
} |
|||
return val; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,66 @@ |
|||
package com.youlai.boot.common.validator; |
|||
|
|||
import com.youlai.boot.common.annotation.EnumValid; |
|||
import jakarta.validation.ConstraintValidator; |
|||
import jakarta.validation.ConstraintValidatorContext; |
|||
|
|||
import java.lang.reflect.Method; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
public class EnumValidator implements ConstraintValidator<EnumValid, String> { |
|||
|
|||
private Class<? extends Enum<?>> enumClass; |
|||
private String enumMethod; |
|||
|
|||
// 缓存方法
|
|||
private Method method; |
|||
|
|||
// 缓存枚举值
|
|||
private final Map<String, Boolean> cache = new HashMap<>(); |
|||
|
|||
@Override |
|||
public void initialize(EnumValid annotation) { |
|||
this.enumClass = annotation.enumClass(); |
|||
this.enumMethod = annotation.enumMethod(); |
|||
|
|||
try { |
|||
this.method = enumClass.getMethod(enumMethod); |
|||
} catch (NoSuchMethodException e) { |
|||
throw new IllegalArgumentException( |
|||
"EnumValid配置错误:方法不存在 " + enumMethod + " in " + enumClass.getName(), e |
|||
); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean isValid(String value, ConstraintValidatorContext context) { |
|||
|
|||
if (value == null) { |
|||
return true; // 交给 @NotNull
|
|||
} |
|||
|
|||
// 缓存命中直接返回
|
|||
Boolean cached = cache.get(value); |
|||
if (cached != null) { |
|||
return cached; |
|||
} |
|||
|
|||
try { |
|||
for (Enum<?> constant : enumClass.getEnumConstants()) { |
|||
Object enumValue = method.invoke(constant); |
|||
|
|||
if (enumValue != null && value.equalsIgnoreCase(enumValue.toString())) { |
|||
cache.put(value, true); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
throw new IllegalStateException("Enum校验执行异常", e); |
|||
} |
|||
|
|||
cache.put(value, false); |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
package com.youlai.boot.mini.controller; |
|||
|
|||
import com.youlai.boot.common.annotation.Log; |
|||
import com.youlai.boot.common.annotation.RepeatSubmit; |
|||
import com.youlai.boot.common.enums.ActionTypeEnum; |
|||
import com.youlai.boot.common.enums.LogModuleEnum; |
|||
import com.youlai.boot.common.model.Option; |
|||
import com.youlai.boot.common.result.Result; |
|||
import com.youlai.boot.mini.model.form.StrayAnimalForm; |
|||
import com.youlai.boot.mini.service.StrayAnimalService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import jakarta.validation.Valid; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 流浪动物信息 |
|||
*/ |
|||
@Tag(name = "流浪动物信息的相关接口") |
|||
@RestController |
|||
@RequestMapping("/api/v1/mini/strayAnimal") |
|||
@RequiredArgsConstructor |
|||
public class StrayAnimalController { |
|||
|
|||
private final StrayAnimalService strayAnimalService; |
|||
|
|||
@Operation(summary = "添加动物信息") |
|||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) |
|||
@RepeatSubmit |
|||
@Log(module = LogModuleEnum.STRAY_ANIMAL_INFO, value = ActionTypeEnum.INSERT) |
|||
public Result<?> saveStrayAnimal( |
|||
@Valid StrayAnimalForm formData, |
|||
@RequestPart(name = "images") List<MultipartFile> images, |
|||
@RequestPart(name = "videos", required = false) List<MultipartFile> videos |
|||
) { |
|||
String uuid = strayAnimalService.saveStrayAnimal(formData, images, videos); |
|||
return Result.success(uuid); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
package com.youlai.boot.mini.converter; |
|||
|
|||
import com.youlai.boot.mini.model.entity.MiniStrayAnimal; |
|||
import com.youlai.boot.mini.model.form.StrayAnimalForm; |
|||
import com.youlai.boot.system.model.entity.Dept; |
|||
import com.youlai.boot.system.model.form.DeptForm; |
|||
import com.youlai.boot.system.model.vo.DeptVO; |
|||
import org.mapstruct.Mapper; |
|||
|
|||
/** |
|||
* 模型转换器 |
|||
*/ |
|||
@Mapper(componentModel = "spring") |
|||
public interface MiniStrayAnimalConverter { |
|||
|
|||
MiniStrayAnimal toEntity(StrayAnimalForm formData); |
|||
|
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.youlai.boot.mini.mapper; |
|||
|
|||
import com.youlai.boot.mini.model.entity.MiniStrayAnimalNote; |
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
|
|||
/** |
|||
* 流浪信息笔记 Mapper 接口 |
|||
* |
|||
* @author jwy |
|||
* @since |
|||
*/ |
|||
public interface MiniStrayAnimalNoteMapper extends BaseMapper<MiniStrayAnimalNote> { |
|||
|
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.youlai.boot.mini.mapper; |
|||
|
|||
import com.youlai.boot.mini.model.entity.MiniStrayAnimalNoteMedia; |
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
|
|||
/** |
|||
* 流浪信息资源表 Mapper 接口 |
|||
* |
|||
* @author jwy |
|||
* @since |
|||
*/ |
|||
public interface MiniStrayAnimalNoteMediaMapper extends BaseMapper<MiniStrayAnimalNoteMedia> { |
|||
|
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
package com.youlai.boot.mini.model.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import lombok.ToString; |
|||
import lombok.experimental.Accessors; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
|
|||
@Getter |
|||
@Setter |
|||
@ToString |
|||
@Accessors(chain = true) |
|||
@TableName("mini_stray_animal_note") |
|||
@Schema(description = "流浪信息笔记") |
|||
public class MiniStrayAnimalNote implements Serializable { |
|||
|
|||
@TableId(value = "id", type = IdType.AUTO) |
|||
@Schema(description = "笔记ID") |
|||
private Long id; |
|||
|
|||
|
|||
@TableField("uuid") |
|||
@Schema(description = "uuid唯一标识,前后端用这个进行数据交互") |
|||
private String uuid; |
|||
|
|||
@TableField("stray_animal_id") |
|||
@Schema(description = "关联的动物ID") |
|||
private Long strayAnimalId; |
|||
|
|||
@TableField("mini_user_id") |
|||
@Schema(description = "作者用户ID") |
|||
private Long miniUserId; |
|||
|
|||
@TableField("title") |
|||
@Schema(description = "笔记标题") |
|||
private String title; |
|||
|
|||
@TableField("content") |
|||
@Schema(description = "笔记正文内容") |
|||
private String content; |
|||
|
|||
@TableField("visibility") |
|||
@Schema(description = "可见范围:public-公开,private-仅自己可见,friends-仅好友") |
|||
private String visibility; |
|||
|
|||
@TableField("view_count") |
|||
@Schema(description = "浏览数") |
|||
private Integer viewCount; |
|||
|
|||
@TableField("like_count") |
|||
@Schema(description = "点赞数") |
|||
private Integer likeCount; |
|||
|
|||
@TableField("comment_count") |
|||
@Schema(description = "评论数") |
|||
private Integer commentCount; |
|||
|
|||
@TableField("collect_count") |
|||
@Schema(description = "收藏数") |
|||
private Integer collectCount; |
|||
|
|||
@TableField("create_time") |
|||
@Schema(description = "创建时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date createTime; |
|||
|
|||
@TableField("create_timestamp") |
|||
@Schema(description = "创建时间毫秒级时间戳") |
|||
private Long createTimestamp; |
|||
|
|||
@TableField("create_by") |
|||
@Schema(description = "创建人ID") |
|||
private Long createBy; |
|||
|
|||
@TableField("update_time") |
|||
@Schema(description = "更新时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date updateTime; |
|||
|
|||
@TableField("update_timestamp") |
|||
@Schema(description = "更新时间毫秒级时间戳") |
|||
private Long updateTimestamp; |
|||
|
|||
@TableField("update_by") |
|||
@Schema(description = "修改人ID") |
|||
private Long updateBy; |
|||
|
|||
@TableField("is_deleted") |
|||
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") |
|||
private Boolean deleted; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
package com.youlai.boot.mini.model.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import lombok.ToString; |
|||
import lombok.experimental.Accessors; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
|
|||
@Getter |
|||
@Setter |
|||
@ToString |
|||
@Accessors(chain = true) |
|||
@TableName("mini_stray_animal_note_media") |
|||
@Schema(description = "流浪信息资源表") |
|||
public class MiniStrayAnimalNoteMedia implements Serializable { |
|||
|
|||
@TableId(value = "id", type = IdType.AUTO) |
|||
@Schema(description = "") |
|||
private Long id; |
|||
|
|||
|
|||
@TableField("uuid") |
|||
@Schema(description = "uuid唯一标识,前后端用这个进行数据交互") |
|||
private String uuid; |
|||
|
|||
@TableField("note_id") |
|||
@Schema(description = "笔记ID") |
|||
private Long noteId; |
|||
|
|||
@TableField("media_type") |
|||
@Schema(description = "媒体类型,image-图片,video-视频") |
|||
private String mediaType; |
|||
|
|||
@TableField("source_url") |
|||
@Schema(description = "资源URL") |
|||
private String sourceUrl; |
|||
|
|||
@TableField("storage_key") |
|||
@Schema(description = "对象存储中的key") |
|||
private String storageKey; |
|||
|
|||
@TableField("thumbnail_url") |
|||
@Schema(description = "缩略图URL(视频需要)") |
|||
private String thumbnailUrl; |
|||
|
|||
@TableField("width") |
|||
@Schema(description = "宽度(像素)") |
|||
private Integer width; |
|||
|
|||
@TableField("height") |
|||
@Schema(description = "高度(像素)") |
|||
private Integer height; |
|||
|
|||
@TableField("duration") |
|||
@Schema(description = "时长(秒,视频用)") |
|||
private Integer duration; |
|||
|
|||
@TableField("sort_order") |
|||
@Schema(description = "排序顺序") |
|||
private Integer sortOrder; |
|||
|
|||
@TableField("create_time") |
|||
@Schema(description = "创建时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date createTime; |
|||
|
|||
@TableField("create_timestamp") |
|||
@Schema(description = "创建时间毫秒级时间戳") |
|||
private Long createTimestamp; |
|||
|
|||
@TableField("create_by") |
|||
@Schema(description = "创建人ID") |
|||
private Long createBy; |
|||
|
|||
@TableField("update_time") |
|||
@Schema(description = "更新时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date updateTime; |
|||
|
|||
@TableField("update_timestamp") |
|||
@Schema(description = "更新时间毫秒级时间戳") |
|||
private Long updateTimestamp; |
|||
|
|||
@TableField("update_by") |
|||
@Schema(description = "修改人ID") |
|||
private Long updateBy; |
|||
|
|||
@TableField("is_deleted") |
|||
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") |
|||
private Boolean deleted; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
package com.youlai.boot.mini.model.enums; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
|
|||
@Schema(description = "媒体类型") |
|||
public enum AnimalNoteMediaTypeEnum { |
|||
|
|||
IMAGE("image", "图片"), |
|||
VIDEO("video", "视频"); |
|||
|
|||
private final String value; |
|||
private final String desc; |
|||
|
|||
AnimalNoteMediaTypeEnum(String value, String desc) { |
|||
this.value = value; |
|||
this.desc = desc; |
|||
} |
|||
|
|||
@JsonValue |
|||
public String getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public String getDesc() { |
|||
return desc; |
|||
} |
|||
|
|||
@JsonCreator |
|||
public static AnimalNoteMediaTypeEnum from(String value) { |
|||
if (value == null) return null; |
|||
|
|||
for (AnimalNoteMediaTypeEnum e : values()) { |
|||
if (e.value.equalsIgnoreCase(value)) { |
|||
return e; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public static boolean contains(String value) { |
|||
return from(value) != null; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
package com.youlai.boot.mini.model.enums; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
|
|||
@Schema(description = "动物体型") |
|||
public enum AnimalSizeEnum { |
|||
|
|||
SMALL("small", "小型动物"), |
|||
MEDIUM("medium", "中等体型动物"), |
|||
LARGE("large", "大型动物"); |
|||
|
|||
private final String value; |
|||
private final String desc; |
|||
|
|||
AnimalSizeEnum(String value, String desc) { |
|||
this.value = value; |
|||
this.desc = desc; |
|||
} |
|||
|
|||
@JsonValue |
|||
public String getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public String getDesc() { |
|||
return desc; |
|||
} |
|||
|
|||
@JsonCreator |
|||
public static AnimalSizeEnum from(String value) { |
|||
if (value == null) return null; |
|||
|
|||
for (AnimalSizeEnum e : values()) { |
|||
if (e.value.equalsIgnoreCase(value)) { |
|||
return e; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public static boolean contains(String value) { |
|||
return from(value) != null; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
package com.youlai.boot.mini.model.enums; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
|
|||
@Schema(description = "动物状态") |
|||
public enum AnimalStatusEnum { |
|||
|
|||
FOUND("found", "发现"), |
|||
ADOPTED("adopted", "已被领养"), |
|||
MISSING("missing", "失踪"), |
|||
REVIEWING("reviewing", "审核中"); |
|||
|
|||
private final String value; |
|||
private final String desc; |
|||
|
|||
AnimalStatusEnum(String value, String desc) { |
|||
this.value = value; |
|||
this.desc = desc; |
|||
} |
|||
|
|||
@JsonValue |
|||
public String getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public String getDesc() { |
|||
return desc; |
|||
} |
|||
|
|||
@JsonCreator |
|||
public static AnimalStatusEnum from(String value) { |
|||
if (value == null) return null; |
|||
for (AnimalStatusEnum e : values()) { |
|||
if (e.value.equalsIgnoreCase(value)) { |
|||
return e; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public static boolean contains(String value) { |
|||
return from(value) != null; |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
package com.youlai.boot.mini.model.enums; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
|
|||
@Schema(description = "动物类型") |
|||
public enum AnimalTypeEnum { |
|||
|
|||
CAT("cat", "猫"), |
|||
DOG("dog", "狗"), |
|||
OTHER("other", "其他"); |
|||
|
|||
private final String value; |
|||
private final String desc; |
|||
|
|||
AnimalTypeEnum(String value, String desc) { |
|||
this.value = value; |
|||
this.desc = desc; |
|||
} |
|||
|
|||
@JsonValue |
|||
public String getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public String getDesc() { |
|||
return desc; |
|||
} |
|||
|
|||
@JsonCreator |
|||
public static AnimalTypeEnum from(String value) { |
|||
if (value == null) return null; |
|||
for (AnimalTypeEnum e : values()) { |
|||
if (e.value.equalsIgnoreCase(value)) { |
|||
return e; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public static boolean contains(String value) { |
|||
return from(value) != null; |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
package com.youlai.boot.mini.model.enums; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
|
|||
@Schema(description = "可见范围") |
|||
public enum VisibilityEnum { |
|||
|
|||
PUBLIC("public", "公开"), |
|||
PRIVATE("private", "仅自己可见"), |
|||
FRIENDS("friends", "仅好友可见"); |
|||
|
|||
private final String value; |
|||
private final String desc; |
|||
|
|||
VisibilityEnum(String value, String desc) { |
|||
this.value = value; |
|||
this.desc = desc; |
|||
} |
|||
|
|||
@JsonValue |
|||
public String getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public String getDesc() { |
|||
return desc; |
|||
} |
|||
|
|||
@JsonCreator |
|||
public static VisibilityEnum from(String value) { |
|||
if (value == null) return null; |
|||
for (VisibilityEnum e : values()) { |
|||
if (e.value.equalsIgnoreCase(value)) { |
|||
return e; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public static boolean contains(String value) { |
|||
return from(value) != null; |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
package com.youlai.boot.mini.model.form; |
|||
|
|||
import com.youlai.boot.common.annotation.EnumValid; |
|||
import com.youlai.boot.mini.model.enums.AnimalSizeEnum; |
|||
import com.youlai.boot.mini.model.enums.AnimalStatusEnum; |
|||
import com.youlai.boot.mini.model.enums.AnimalTypeEnum; |
|||
import com.youlai.boot.mini.model.enums.VisibilityEnum; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import jakarta.validation.constraints.NotNull; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import org.hibernate.validator.constraints.Length; |
|||
|
|||
@Schema(description = "动物信息表单对象") |
|||
@Getter |
|||
@Setter |
|||
public class StrayAnimalForm { |
|||
|
|||
@NotBlank(message = "动物类型不能为空") |
|||
@EnumValid(enumClass = AnimalTypeEnum.class, message = "动物类型不合法") |
|||
@Schema(description = "动物类型,cat-猫,dog-狗,other-其他", example = "cat", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private String animalType; |
|||
|
|||
@Length(max = 50, message = "颜色不能超过50个字符") |
|||
@Schema(description = "颜色", example = "白色") |
|||
private String color; |
|||
|
|||
@EnumValid(enumClass = AnimalSizeEnum.class, message = "体型不合法") |
|||
@Schema(description = "体型,small-小,medium-中等,large-大", example = "medium") |
|||
private String size; |
|||
|
|||
@NotBlank(message = "状态不能为空") |
|||
@EnumValid(enumClass = AnimalStatusEnum.class, message = "状态不合法") |
|||
@Schema(description = "状态,found-发现,adopted-已被领养,missing-失踪", example = "missing", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private String status="found"; |
|||
|
|||
@NotBlank(message = "笔记标题不能为空") |
|||
@Length(max = 200, message = "标题不能超过200个字符") |
|||
@Schema(description = "笔记标题", example = "在公园发现的小猫", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private String title; |
|||
|
|||
@Schema(description = "笔记内容", example = "今天下午在人民公园看到一只走失的小猫") |
|||
private String content; |
|||
|
|||
@NotBlank(message = "可见性范围不能为空") |
|||
@EnumValid(enumClass = VisibilityEnum.class, message = "可见性范围不合法") |
|||
@Schema(description = "可见性范围:public-公开,private-仅自己,friends-仅好友", example = "public") |
|||
private String visibility="public"; |
|||
|
|||
@NotNull(message = "经度不能为空") |
|||
@Schema(description = "经度", example = "118.08125") |
|||
private Double lng; |
|||
|
|||
@NotNull(message = "纬度不能为空") |
|||
@Schema(description = "纬度", example = "24.606929") |
|||
private Double lat; |
|||
|
|||
@Length(max = 50, message = "不能超过50个字符") |
|||
@Schema(description = "省", example = "福建省") |
|||
private String province; |
|||
|
|||
@Length(max = 50, message = "不能超过50个字符") |
|||
@Schema(description = "市", example = "厦门市") |
|||
private String city; |
|||
|
|||
@Length(max = 50, message = "不能超过50个字符") |
|||
@Schema(description = "区(县)", example = "集美区") |
|||
private String district; |
|||
|
|||
@Length(max = 255, message = "地址不能超过255个字符") |
|||
@Schema(description = "完整详细地址,含省市区(县)", example = "福建省厦门市集美区侨英街道莲花尚院2号院") |
|||
private String address; |
|||
|
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
package com.youlai.boot.mini.service; |
|||
|
|||
import com.baomidou.mybatisplus.extension.service.IService; |
|||
import com.youlai.boot.mini.model.entity.MiniStrayAnimal; |
|||
import com.youlai.boot.mini.model.form.StrayAnimalForm; |
|||
import jakarta.validation.Valid; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface StrayAnimalService extends IService<MiniStrayAnimal> { |
|||
|
|||
String saveStrayAnimal(@Valid StrayAnimalForm formData, List<MultipartFile> images, List<MultipartFile> videos); |
|||
|
|||
} |
|||
@ -0,0 +1,227 @@ |
|||
package com.youlai.boot.mini.service.impl; |
|||
|
|||
import ch.hsr.geohash.GeoHash; |
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.lang.Assert; |
|||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
|||
import com.youlai.boot.common.constant.CommonConstants; |
|||
import com.youlai.boot.common.util.CoordinateTransformUtils; |
|||
import com.youlai.boot.common.util.FileUtils; |
|||
import com.youlai.boot.common.util.JavaVCUtils; |
|||
import com.youlai.boot.common.util.RandomNumberUtils; |
|||
import com.youlai.boot.file.service.FileService; |
|||
import com.youlai.boot.framework.security.util.SecurityUtils; |
|||
import com.youlai.boot.mini.converter.MiniStrayAnimalConverter; |
|||
import com.youlai.boot.mini.mapper.MiniStrayAnimalMapper; |
|||
import com.youlai.boot.mini.mapper.MiniStrayAnimalNoteMapper; |
|||
import com.youlai.boot.mini.mapper.MiniStrayAnimalNoteMediaMapper; |
|||
import com.youlai.boot.mini.model.enums.AnimalNoteMediaTypeEnum; |
|||
import com.youlai.boot.mini.model.entity.MiniStrayAnimal; |
|||
import com.youlai.boot.mini.model.entity.MiniStrayAnimalNote; |
|||
import com.youlai.boot.mini.model.entity.MiniStrayAnimalNoteMedia; |
|||
import com.youlai.boot.mini.model.form.StrayAnimalForm; |
|||
import com.youlai.boot.mini.service.StrayAnimalService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.io.FilenameUtils; |
|||
import org.locationtech.jts.geom.Coordinate; |
|||
import org.locationtech.jts.geom.GeometryFactory; |
|||
import org.locationtech.jts.geom.Point; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import javax.imageio.ImageIO; |
|||
import java.awt.image.BufferedImage; |
|||
import java.io.File; |
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
/** |
|||
* 流浪动物业务实现类 |
|||
*/ |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class StrayAnimalServiceImpl extends ServiceImpl<MiniStrayAnimalMapper, MiniStrayAnimal> implements StrayAnimalService { |
|||
|
|||
private final FileService fileService; |
|||
|
|||
private final MiniStrayAnimalNoteMapper miniStrayAnimalNoteMapper; |
|||
private final MiniStrayAnimalNoteMediaMapper miniStrayAnimalNoteMediaMapper; |
|||
private final MiniStrayAnimalMapper miniStrayAnimalMapper; |
|||
|
|||
private final MiniStrayAnimalConverter miniStrayAnimalConverter; |
|||
|
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public String saveStrayAnimal(StrayAnimalForm formData, List<MultipartFile> images, List<MultipartFile> videos) { |
|||
// 1. 参数校验
|
|||
validateInput(formData, images, videos); |
|||
|
|||
// 2. 保存流浪动物基本信息
|
|||
MiniStrayAnimal miniStrayAnimal = saveAnimalInfo(formData); |
|||
|
|||
// 3. 保存笔记信息
|
|||
MiniStrayAnimalNote note = saveNoteInfo(formData, miniStrayAnimal); |
|||
|
|||
// 4. 处理并保存媒体文件
|
|||
saveMediaFiles(note, images, videos); |
|||
|
|||
return miniStrayAnimal.getUuid(); |
|||
} |
|||
|
|||
private void saveMediaFiles(MiniStrayAnimalNote note, List<MultipartFile> images, List<MultipartFile> videos) { |
|||
int sortOrder = 0; |
|||
// 处理图片
|
|||
if (images != null) { |
|||
for (MultipartFile image : images) { |
|||
try { |
|||
String objectName = "animal_note/" |
|||
+ note.getId() + "/" |
|||
+ note.getCreateTimestamp() + RandomNumberUtils.createRandomLowerLetterAndNumber(8) |
|||
+ "." |
|||
+ FilenameUtils.getExtension(image.getOriginalFilename()); |
|||
String url = fileService.uploadFile(objectName, image.getInputStream()); |
|||
MiniStrayAnimalNoteMedia miniStrayAnimalNoteMedia = new MiniStrayAnimalNoteMedia(); |
|||
miniStrayAnimalNoteMedia.setUuid(UUID.randomUUID().toString()); |
|||
miniStrayAnimalNoteMedia.setNoteId(note.getId()); |
|||
miniStrayAnimalNoteMedia.setMediaType(AnimalNoteMediaTypeEnum.IMAGE.name().toLowerCase()); |
|||
miniStrayAnimalNoteMedia.setSourceUrl(url); |
|||
miniStrayAnimalNoteMedia.setStorageKey(objectName); |
|||
BufferedImage imageInfo = ImageIO.read(image.getInputStream()); |
|||
miniStrayAnimalNoteMedia.setWidth(imageInfo.getWidth()); |
|||
miniStrayAnimalNoteMedia.setHeight(imageInfo.getHeight()); |
|||
miniStrayAnimalNoteMedia.setSortOrder(sortOrder++); |
|||
miniStrayAnimalNoteMedia.setCreateTimestamp(note.getCreateTimestamp()); |
|||
miniStrayAnimalNoteMedia.setCreateTime(new Date(miniStrayAnimalNoteMedia.getCreateTimestamp())); |
|||
miniStrayAnimalNoteMedia.setCreateBy(note.getMiniUserId()); |
|||
|
|||
int result = miniStrayAnimalNoteMediaMapper.insert(miniStrayAnimalNoteMedia); |
|||
} catch (Exception e) { |
|||
log.error("image upload failed", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 处理视频 (如果有)
|
|||
if (videos != null) { |
|||
String tmpPath = System.getProperty("user.dir") + "/tmp"; |
|||
for (MultipartFile video : videos) { |
|||
try { |
|||
String fileName = note.getCreateTimestamp() + RandomNumberUtils.createRandomLowerLetterAndNumber(8); |
|||
String objectName = "animal_note/" |
|||
+ note.getId() + "/" |
|||
+ fileName |
|||
+ "." |
|||
+ FilenameUtils.getExtension(video.getOriginalFilename()); |
|||
String url = fileService.uploadFile(objectName, video.getInputStream()); |
|||
MiniStrayAnimalNoteMedia miniStrayAnimalNoteMedia = new MiniStrayAnimalNoteMedia(); |
|||
miniStrayAnimalNoteMedia.setUuid(UUID.randomUUID().toString()); |
|||
miniStrayAnimalNoteMedia.setNoteId(note.getId()); |
|||
miniStrayAnimalNoteMedia.setMediaType(AnimalNoteMediaTypeEnum.VIDEO.name().toLowerCase()); |
|||
miniStrayAnimalNoteMedia.setSourceUrl(url); |
|||
miniStrayAnimalNoteMedia.setStorageKey(objectName); |
|||
miniStrayAnimalNoteMedia.setSortOrder(sortOrder++); |
|||
miniStrayAnimalNoteMedia.setCreateTimestamp(note.getCreateTimestamp()); |
|||
miniStrayAnimalNoteMedia.setCreateTime(new Date(miniStrayAnimalNoteMedia.getCreateTimestamp())); |
|||
miniStrayAnimalNoteMedia.setCreateBy(note.getMiniUserId()); |
|||
//时长
|
|||
FileUtils.saveFile(video, tmpPath, fileName); |
|||
String videoPath = tmpPath + File.separator + fileName; |
|||
double duration = JavaVCUtils.getVideoDuration(videoPath); |
|||
miniStrayAnimalNoteMedia.setDuration((int) Math.ceil(duration)); |
|||
//缩略图
|
|||
BufferedImage thumbnail = JavaVCUtils.getVideoThumbnail(videoPath, 1); |
|||
String thumbnailFileName = note.getCreateTimestamp() + RandomNumberUtils.createRandomLowerLetterAndNumber(8); |
|||
String thumbnailObjectName = "animal_note/" |
|||
+ note.getId() + "/" |
|||
+ thumbnailFileName |
|||
+ ".png"; |
|||
String thumbnailUrl = fileService.uploadFile(thumbnailObjectName, |
|||
FileUtils.bufferedImageToInputStream(thumbnail, "png")); |
|||
miniStrayAnimalNoteMedia.setThumbnailUrl(thumbnailUrl); |
|||
|
|||
miniStrayAnimalNoteMediaMapper.insert(miniStrayAnimalNoteMedia); |
|||
|
|||
FileUtils.delete(videoPath); |
|||
|
|||
} catch (Exception e) { |
|||
log.error("video upload failed", e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private MiniStrayAnimalNote saveNoteInfo(StrayAnimalForm formData, MiniStrayAnimal miniStrayAnimal) { |
|||
MiniStrayAnimalNote note = new MiniStrayAnimalNote(); |
|||
note.setUuid(UUID.randomUUID().toString()); |
|||
note.setStrayAnimalId(miniStrayAnimal.getId()); |
|||
note.setMiniUserId(miniStrayAnimal.getMiniUserId()); |
|||
note.setTitle(formData.getTitle()); |
|||
note.setContent(formData.getContent()); |
|||
note.setCreateTimestamp(miniStrayAnimal.getCreateTimestamp()); |
|||
note.setCreateTime(new Date(note.getCreateTimestamp())); |
|||
note.setCreateBy(miniStrayAnimal.getMiniUserId()); |
|||
|
|||
miniStrayAnimalNoteMapper.insert(note); |
|||
|
|||
return note; |
|||
} |
|||
|
|||
private MiniStrayAnimal saveAnimalInfo(StrayAnimalForm formData) { |
|||
|
|||
long currentTimeUnix = System.currentTimeMillis(); |
|||
MiniStrayAnimal entity = new MiniStrayAnimal(); |
|||
entity.setMiniUserId(SecurityUtils.getUserId()); |
|||
entity.setUuid(UUID.randomUUID().toString()); |
|||
entity.setAnimalType(formData.getAnimalType().toLowerCase()); |
|||
entity.setColor(formData.getColor()); |
|||
if (formData.getSize() != null) { |
|||
entity.setSize(formData.getSize().toLowerCase()); |
|||
} |
|||
entity.setStatus(formData.getStatus().toLowerCase()); |
|||
entity.setCreateTimestamp(currentTimeUnix); |
|||
entity.setCreateTime(new Date(currentTimeUnix)); |
|||
entity.setCreateBy(SecurityUtils.getUserId()); |
|||
entity.setProvince(formData.getProvince()); |
|||
entity.setCity(formData.getCity()); |
|||
entity.setDistrict(formData.getDistrict()); |
|||
entity.setAddress(formData.getAddress()); |
|||
|
|||
handleLngLat(entity, formData.getLng(), formData.getLat()); |
|||
|
|||
boolean result = miniStrayAnimalMapper.insertStrayAnimal(entity); |
|||
Assert.isTrue(result, "动物信息保存失败"); |
|||
|
|||
return entity; |
|||
} |
|||
|
|||
private void handleLngLat(MiniStrayAnimal entity, Double lng, Double lat) { |
|||
GeometryFactory geometryFactory = new GeometryFactory(); |
|||
Point gdPoint = geometryFactory.createPoint(new Coordinate(lat, lng)); |
|||
gdPoint.setSRID(4326); |
|||
double[] wgs84 = CoordinateTransformUtils.gcj02ToWgs84(lng, lat); |
|||
Point wgsPoint = geometryFactory.createPoint(new Coordinate(wgs84[1], wgs84[0])); |
|||
wgsPoint.setSRID(4326); |
|||
entity.setGdLocationPoint(gdPoint); |
|||
entity.setWgs84LocationPoint(wgsPoint); |
|||
entity.setWgs84Geohash( GeoHash.withCharacterPrecision(wgs84[1], wgs84[0], CommonConstants.GEOHASH_LEVEL).toBase32()); |
|||
} |
|||
|
|||
private void validateInput(StrayAnimalForm formData, List<MultipartFile> images, List<MultipartFile> videos) { |
|||
// 验证必填图片
|
|||
Assert.notEmpty(images, "需要上传图片"); |
|||
// 验证图片数量
|
|||
Assert.isTrue( |
|||
images.size() <= CommonConstants.STRAY_ANIMAL_IMAGE_NUM_LIMIT, |
|||
"最多只能上传" + CommonConstants.STRAY_ANIMAL_IMAGE_NUM_LIMIT + "张图片"); |
|||
// 验证视频数量
|
|||
Assert.isTrue( |
|||
CollUtil.isEmpty(videos) || videos.size() <= CommonConstants.STRAY_ANIMAL_VIDEO_NUM_LIMIT, |
|||
"最多只能上传" + CommonConstants.STRAY_ANIMAL_VIDEO_NUM_LIMIT + "个视频"); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
<?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="com.youlai.boot.mini.mapper.MiniStrayAnimalNoteMapper"> |
|||
|
|||
|
|||
</mapper> |
|||
@ -0,0 +1,9 @@ |
|||
<?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="com.youlai.boot.mini.mapper.MiniStrayAnimalNoteMediaMapper"> |
|||
|
|||
|
|||
</mapper> |
|||
Loading…
Reference in new issue