Browse Source

登记动物信息

master
review512jwy@163.com 1 month ago
parent
commit
dfccd4be09
  1. 10
      pom.xml
  2. 2
      src/main/java/com/youlai/boot/YouLaiBootApplication.java
  3. 10
      src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java
  4. 24
      src/main/java/com/youlai/boot/common/annotation/EnumValid.java
  5. 12
      src/main/java/com/youlai/boot/common/constant/CommonConstants.java
  6. 1
      src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java
  7. 104
      src/main/java/com/youlai/boot/common/util/CoordinateTransformUtils.java
  8. 438
      src/main/java/com/youlai/boot/common/util/FileUtils.java
  9. 52
      src/main/java/com/youlai/boot/common/util/JavaVCUtils.java
  10. 43
      src/main/java/com/youlai/boot/common/util/RandomNumberUtils.java
  11. 66
      src/main/java/com/youlai/boot/common/validator/EnumValidator.java
  12. 5
      src/main/java/com/youlai/boot/file/service/FileService.java
  13. 12
      src/main/java/com/youlai/boot/file/service/impl/AliyunFileService.java
  14. 5
      src/main/java/com/youlai/boot/file/service/impl/LocalFileService.java
  15. 5
      src/main/java/com/youlai/boot/file/service/impl/MinioFileService.java
  16. 47
      src/main/java/com/youlai/boot/mini/controller/StrayAnimalController.java
  17. 18
      src/main/java/com/youlai/boot/mini/converter/MiniStrayAnimalConverter.java
  18. 4
      src/main/java/com/youlai/boot/mini/mapper/MiniStrayAnimalMapper.java
  19. 14
      src/main/java/com/youlai/boot/mini/mapper/MiniStrayAnimalNoteMapper.java
  20. 14
      src/main/java/com/youlai/boot/mini/mapper/MiniStrayAnimalNoteMediaMapper.java
  21. 98
      src/main/java/com/youlai/boot/mini/model/entity/MiniStrayAnimalNote.java
  22. 98
      src/main/java/com/youlai/boot/mini/model/entity/MiniStrayAnimalNoteMedia.java
  23. 45
      src/main/java/com/youlai/boot/mini/model/enums/AnimalNoteMediaTypeEnum.java
  24. 46
      src/main/java/com/youlai/boot/mini/model/enums/AnimalSizeEnum.java
  25. 46
      src/main/java/com/youlai/boot/mini/model/enums/AnimalStatusEnum.java
  26. 45
      src/main/java/com/youlai/boot/mini/model/enums/AnimalTypeEnum.java
  27. 45
      src/main/java/com/youlai/boot/mini/model/enums/VisibilityEnum.java
  28. 75
      src/main/java/com/youlai/boot/mini/model/form/StrayAnimalForm.java
  29. 15
      src/main/java/com/youlai/boot/mini/service/StrayAnimalService.java
  30. 227
      src/main/java/com/youlai/boot/mini/service/impl/StrayAnimalServiceImpl.java
  31. 9
      src/main/resources/application-dev.yml
  32. 1
      src/main/resources/application-prod.yml
  33. 74
      src/main/resources/mapper/mini/MiniStrayAnimalMapper.xml
  34. 9
      src/main/resources/mapper/mini/MiniStrayAnimalNoteMapper.xml
  35. 9
      src/main/resources/mapper/mini/MiniStrayAnimalNoteMediaMapper.xml

10
pom.xml

@ -70,6 +70,8 @@
<geohash.version>1.4.0</geohash.version>
<javacv.platform.version>1.5.13</javacv.platform.version>
</properties>
@ -302,6 +304,14 @@
<version>${geohash.version}</version>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/org.bytedeco/javacv-platform -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>${javacv.platform.version}</version>
<scope>compile</scope>
</dependency>
<!-- 动态多数据源 -->
<!--<dependency>
<groupId>com.baomidou</groupId>

2
src/main/java/com/youlai/boot/YouLaiBootApplication.java

@ -1,5 +1,6 @@
package com.youlai.boot;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -10,6 +11,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @since 0.0.1
*/
@SpringBootApplication
@MapperScan("com.youlai.boot.**.mapper")
public class YouLaiBootApplication {
public static void main(String[] args) {

10
src/main/java/com/youlai/boot/codegen/freemarker/MyBatisPlusGenerator.java

@ -92,6 +92,9 @@ public class MyBatisPlusGenerator {
List<TableConfig> tableConfigs = Arrays.asList(
new TableConfig("mini_stray_animal", IdType.AUTO, "mini")
,new TableConfig("mini_stray_animal_note", IdType.AUTO, "mini")
,new TableConfig("mini_stray_animal_note_media", IdType.AUTO, "mini")
// ,new TableConfig("mini_stray_animal", IdType.AUTO, "mini")
// ,new TableConfig("mini_stray_animal", IdType.INPUT, "minitest")
@ -199,6 +202,13 @@ public class MyBatisPlusGenerator {
})
// 模板(只保留 entity)
.templateConfig(builder -> builder
.disable(
com.baomidou.mybatisplus.generator.config.TemplateType.SERVICE,
com.baomidou.mybatisplus.generator.config.TemplateType.SERVICE_IMPL,
com.baomidou.mybatisplus.generator.config.TemplateType.CONTROLLER
)
)
// 使用你的自定义引擎
.templateEngine(new CustomFreemarkerTemplateEngine("codegen/ftl"))

24
src/main/java/com/youlai/boot/common/annotation/EnumValid.java

@ -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"; // 获取值的方法
}

12
src/main/java/com/youlai/boot/common/constant/CommonConstants.java

@ -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;
}

1
src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java

@ -27,6 +27,7 @@ public enum LogModuleEnum implements IBaseEnum<Integer> {
NOTICE(9, "通知公告"),
LOG(10, "日志管理"),
CODEGEN(11, "代码生成"),
STRAY_ANIMAL_INFO(12, "流浪动物信息"),
OTHER(99, "其他");
@EnumValue

104
src/main/java/com/youlai/boot/common/util/CoordinateTransformUtils.java

@ -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);
}
}

438
src/main/java/com/youlai/boot/common/util/FileUtils.java

@ -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 ""; // 没有扩展名
}
}

52
src/main/java/com/youlai/boot/common/util/JavaVCUtils.java

@ -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);
}
}
}

43
src/main/java/com/youlai/boot/common/util/RandomNumberUtils.java

@ -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;
}
}

66
src/main/java/com/youlai/boot/common/validator/EnumValidator.java

@ -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;
}
}

5
src/main/java/com/youlai/boot/file/service/FileService.java

@ -3,6 +3,8 @@ package com.youlai.boot.file.service;
import com.youlai.boot.file.model.FileInfo;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
/**
* 对象存储服务接口层
*
@ -27,4 +29,7 @@ public interface FileService {
boolean deleteFile(String filePath);
String uploadFile(String objectName, InputStream inputStream);
}

12
src/main/java/com/youlai/boot/file/service/impl/AliyunFileService.java

@ -89,6 +89,18 @@ public class AliyunFileService implements FileService {
return fileInfo;
}
/**
* 上传流文件返回文件访问 URL
*/
public String uploadFile(String objectName, InputStream inputStream) {
aliyunOssClient.putObject(bucketName, objectName, inputStream);
return getFileUrl(objectName);
}
private String getFileUrl(String objectName) {
return "https://" + bucketName + "." + endpoint.replace("https://", "") + "/" + objectName;
}
@Override
public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空");

5
src/main/java/com/youlai/boot/file/service/impl/LocalFileService.java

@ -89,4 +89,9 @@ public class LocalFileService implements FileService {
// 删除文件
return FileUtil.del(storagePath + filePath);
}
@Override
public String uploadFile(String objectName, InputStream inputStream) {
return "";
}
}

5
src/main/java/com/youlai/boot/file/service/impl/MinioFileService.java

@ -165,6 +165,11 @@ public class MinioFileService implements FileService {
}
}
@Override
public String uploadFile(String objectName, InputStream inputStream) {
return "";
}
/**
* PUBLIC桶策略

47
src/main/java/com/youlai/boot/mini/controller/StrayAnimalController.java

@ -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);
}
}

18
src/main/java/com/youlai/boot/mini/converter/MiniStrayAnimalConverter.java

@ -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);
}

4
src/main/java/com/youlai/boot/mini/mapper/MiniStrayAnimalMapper.java

@ -7,8 +7,10 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
* 流浪动物基础信息表 Mapper 接口
*
* @author jwy
* @since
* @since
*/
public interface MiniStrayAnimalMapper extends BaseMapper<MiniStrayAnimal> {
boolean insertStrayAnimal(MiniStrayAnimal entity);
}

14
src/main/java/com/youlai/boot/mini/mapper/MiniStrayAnimalNoteMapper.java

@ -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> {
}

14
src/main/java/com/youlai/boot/mini/mapper/MiniStrayAnimalNoteMediaMapper.java

@ -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> {
}

98
src/main/java/com/youlai/boot/mini/model/entity/MiniStrayAnimalNote.java

@ -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;
}

98
src/main/java/com/youlai/boot/mini/model/entity/MiniStrayAnimalNoteMedia.java

@ -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;
}

45
src/main/java/com/youlai/boot/mini/model/enums/AnimalNoteMediaTypeEnum.java

@ -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;
}
}

46
src/main/java/com/youlai/boot/mini/model/enums/AnimalSizeEnum.java

@ -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;
}
}

46
src/main/java/com/youlai/boot/mini/model/enums/AnimalStatusEnum.java

@ -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;
}
}

45
src/main/java/com/youlai/boot/mini/model/enums/AnimalTypeEnum.java

@ -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;
}
}

45
src/main/java/com/youlai/boot/mini/model/enums/VisibilityEnum.java

@ -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;
}
}

75
src/main/java/com/youlai/boot/mini/model/form/StrayAnimalForm.java

@ -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;
}

15
src/main/java/com/youlai/boot/mini/service/StrayAnimalService.java

@ -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);
}

227
src/main/java/com/youlai/boot/mini/service/impl/StrayAnimalServiceImpl.java

@ -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 + "个视频");
}
}

9
src/main/resources/application-dev.yml

@ -117,13 +117,13 @@ oss:
# 阿里云OSS对象存储服务
aliyun:
# 服务Endpoint
endpoint: oss-cn-hangzhou.aliyuncs.com
endpoint: oss-cn-beijing.aliyuncs.com
# 访问凭据
access-key-id: your-access-key-id
access-key-id: LTAI5tBKi4uysfdwHmBLchm1
# 凭据密钥
access-key-secret: your-access-key-secret
access-key-secret: uGTIv6i7OCQ6TtnmJTqiWi7RYHY1Pd
# 存储桶名称
bucket-name: default
bucket-name: pet-map
# 本地存储
local:
# 文件存储路径 请注意下,mac用户请使用 /Users/your-username/your-path/,否则会有权限问题,windows用户请使用 D:/your-path/
@ -160,6 +160,7 @@ springdoc:
packages-to-scan: # 扫描的 Controller 包,限制只生成指定包的接口文档
- com.youlai.boot.auth.controller
- com.youlai.boot.system.controller
- com.youlai.boot.mini.controller
- com.youlai.boot.module.file.controller
- com.youlai.boot.module.codegen.controller
default-flat-param-object: true # 将对象参数扁平化显示在文档中

1
src/main/resources/application-prod.yml

@ -160,6 +160,7 @@ springdoc:
packages-to-scan: # 扫描的 Controller 包,限制只生成指定包的接口文档
- com.youlai.boot.auth.controller
- com.youlai.boot.system.controller
- com.youlai.boot.mini.controller
- com.youlai.boot.module.file.controller
- com.youlai.boot.module.codegen.controller
default-flat-param-object: true # 将对象参数扁平化显示在文档中

74
src/main/resources/mapper/mini/MiniStrayAnimalMapper.xml

@ -5,5 +5,79 @@
<mapper namespace="com.youlai.boot.mini.mapper.MiniStrayAnimalMapper">
<insert id="insertStrayAnimal" parameterType="com.youlai.boot.mini.model.entity.MiniStrayAnimal" useGeneratedKeys="true" keyProperty="id">
INSERT INTO mini_stray_animal
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="uuid != null">uuid,</if>
<if test="miniUserId != null">mini_user_id,</if>
<if test="animalType != null">animal_type,</if>
<if test="color != null">color,</if>
<if test="size != null">size,</if>
<if test="status != null">status,</if>
<if test="adoptedBy != null">adopted_by,</if>
<if test="adoptedAt != null">adopted_at,</if>
<if test="auditStatus != null">audit_status,</if>
<if test="province != null">province,</if>
<if test="city != null">city,</if>
<if test="district != null">district,</if>
<if test="address != null">address,</if>
<if test="gdLocationPoint != null">gd_location_point,</if>
<if test="wgs84LocationPoint != null">wgs84_location_point,</if>
<if test="wgs84Geohash != null">wgs84_geohash,</if>
<if test="createTime != null">create_time,</if>
<if test="createTimestamp != null">create_timestamp,</if>
<if test="createBy != null">create_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="updateTimestamp != null">update_timestamp,</if>
<if test="updateBy != null">update_by,</if>
<if test="deleted != null">is_deleted,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="uuid != null">#{uuid},</if>
<if test="miniUserId != null">#{miniUserId},</if>
<if test="animalType != null">#{animalType},</if>
<if test="color != null">#{color},</if>
<if test="size != null">#{size},</if>
<if test="status != null">#{status},</if>
<if test="adoptedBy != null">#{adoptedBy},</if>
<if test="adoptedAt != null">#{adoptedAt},</if>
<if test="auditStatus != null">#{auditStatus},</if>
<if test="province != null">#{province},</if>
<if test="city != null">#{city},</if>
<if test="district != null">#{district},</if>
<if test="address != null">#{address},</if>
<if test="gdLocationPoint != null">
ST_GeomFromWKB(#{gdLocationPoint, typeHandler=com.youlai.boot.common.handler.PointTypeHandler}, 4326),
</if>
<if test="wgs84LocationPoint != null">
ST_GeomFromWKB(#{wgs84LocationPoint, typeHandler=com.youlai.boot.common.handler.PointTypeHandler}, 4326),
</if>
<if test="wgs84Geohash != null">#{wgs84Geohash},</if>
<if test="createTime != null">#{createTime},</if>
<if test="createTimestamp != null">#{createTimestamp},</if>
<if test="createBy != null">#{createBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="updateTimestamp != null">#{updateTimestamp},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="deleted != null">#{deleted},</if>
</trim>
</insert>
</mapper>

9
src/main/resources/mapper/mini/MiniStrayAnimalNoteMapper.xml

@ -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>

9
src/main/resources/mapper/mini/MiniStrayAnimalNoteMediaMapper.xml

@ -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…
Cancel
Save