1 Commits

Author SHA1 Message Date
88abc41a9e 下载图片+水印(作者ID+作者名) 2026-01-13 13:48:26 +08:00
11 changed files with 212 additions and 21 deletions

View File

@@ -0,0 +1,16 @@
package com.bao.dating.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSConfig {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}

View File

@@ -30,7 +30,8 @@ public class WebConfig implements WebMvcConfigurer {
// 忽略的接口
.excludePathPatterns(
"/user/login",
"/user/sendCode"
"/user/sendCode",
"/download/{postId}"
);
}
}

View File

@@ -12,6 +12,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.util.List;
/**
@@ -87,4 +90,20 @@ public class PostController {
PostEditVO result = postService.updatePost(postId, postRequestDTO);
return Result.success(ResultCode.SUCCESS, "动态更新成功", result);
}
@GetMapping("/download/{postId}")
public void downloadPostImage(@PathVariable Long postId, HttpServletResponse response) throws Exception {
try {
//Service 返回已经加好水印的图片
BufferedImage image = postService.downloadWithWatermark(postId);
//设置响应头,触发浏览器下载
response.setContentType("image/jpeg");
response.setHeader("Content-Disposition", "attachment;filename=post_" + postId + ".jpg");
//输出到浏览器
ImageIO.write(image, "jpg", response.getOutputStream());
response.getOutputStream().flush();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -6,6 +6,7 @@ import com.bao.dating.common.ResultCode;
import com.bao.dating.context.UserContext;
import com.bao.dating.pojo.dto.UserInfoUpdateDTO;
import com.bao.dating.pojo.dto.UserLoginDTO;
import com.bao.dating.pojo.entity.User;
import com.bao.dating.pojo.vo.UserInfoVO;
import com.bao.dating.pojo.vo.UserLoginVO;
import com.bao.dating.service.UserService;

View File

@@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 动态Mapper
@@ -95,4 +96,12 @@ public interface PostMapper {
* @return 影响行数
*/
int decreaseFavoriteCount(Long postId);
/**
* 根据动态id查询用户名和媒体信息
*
* @param postId 动态id
* @return 用户名和媒体信息
*/
Map<String, Object> getUsernameByUserId(Long postId);
}

View File

@@ -44,7 +44,7 @@ public class User implements Serializable {
private String userPhone;
private Double latitude;
private Double latitude; // 纬度
private Double longitude;
private Double longitude; // 经度
}

View File

@@ -5,6 +5,7 @@ import com.bao.dating.pojo.entity.Post;
import com.bao.dating.pojo.vo.PostEditVO;
import org.springframework.web.multipart.MultipartFile;
import java.awt.image.BufferedImage;
import java.util.List;
/**
@@ -55,4 +56,11 @@ public interface PostService {
* @return 用户id
*/
Long selectUserIdByPostId(Long postId);
/**
* 下载动态图片并添加水印
* @param postId 动态ID
* @return 带水印的图片
*/
BufferedImage downloadWithWatermark(Long postId) throws Exception;
}

View File

@@ -10,11 +10,13 @@ import com.bao.dating.mapper.PostLikeMapper;
import com.bao.dating.mapper.PostMapper;
import com.bao.dating.pojo.dto.PostRequestDTO;
import com.bao.dating.pojo.entity.Post;
import com.bao.dating.pojo.entity.User;
import com.bao.dating.pojo.vo.PostEditVO;
import com.bao.dating.service.PostService;
import com.bao.dating.common.aliyun.AliOssUtil;
import com.bao.dating.service.UserService;
import com.bao.dating.util.FileUtil;
import com.bao.dating.util.WatermarkUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -22,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -57,6 +60,9 @@ public class PostServiceImpl implements PostService {
@Autowired
private CommentsMapper commentsMapper;
@Autowired
private WatermarkUtil watermarkUtil;
/**
* 上传媒体文件
* @param files 媒体文件数组
@@ -331,4 +337,61 @@ public class PostServiceImpl implements PostService {
return postMapper.selectUserIdByPostId(postId);
}
/**
* 下载动态图片并添加水印
*
* @param postId 动态ID
* @return 带水印的图片
* @throws Exception
*/
@Override
public BufferedImage downloadWithWatermark(Long postId) throws Exception {
// 一次性查出 动态图片 + 作者信息
Map<String, Object> map = postMapper.getUsernameByUserId(postId);
if (map == null || map.isEmpty()) {
Post post = postMapper.selectById(postId);
if (post == null) {
throw new RuntimeException("未找到指定postId的帖子: " + postId);
}
throw new RuntimeException("未找到与postId相关的用户和媒体信息: " + postId);
}
String mediaUrl = (String) map.get("media_oss_keys");
String username = (String) map.get("user_name");
Object userIdObj = map.get("user_id");
if (mediaUrl == null || username == null || userIdObj == null) {
throw new RuntimeException("用户或媒体信息不完整: " + map);
}
mediaUrl = mediaUrl.trim();
if (mediaUrl.isEmpty()) {
throw new RuntimeException("媒体URL为空postId: " + postId);
}
Long userId = userIdObj instanceof Number
? ((Number) userIdObj).longValue()
: Long.valueOf(userIdObj.toString());
// 解析 OSS ObjectKey支持完整URL和直接存key两种
String cleanUrl = mediaUrl.split("\\?")[0]; // 去掉 ? 后面的参数
String objectKey;
if (cleanUrl.startsWith("http")) {
// https://xxx.oss-cn-xxx.aliyuncs.com/post/xxx.jpg → post/xxx.jpg
objectKey = cleanUrl.substring(cleanUrl.indexOf(".com/") + 5);
} else {
objectKey = cleanUrl;
}
if (objectKey.trim().isEmpty()) {
throw new RuntimeException("解析后的ObjectKey为空url: " + mediaUrl);
}
// 下载并动态加水印只给下载的人看OSS原图不改数据库不动
return watermarkUtil.downloadAndWatermark(objectKey, username, userId);
}
}

View File

@@ -0,0 +1,64 @@
package com.bao.dating.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.bao.dating.config.AliyunOSSConfig;
import com.bao.dating.mapper.PostMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.InputStream;
@Component
public class WatermarkUtil {
@Autowired
private AliyunOSSConfig aliyunOSSConfig;
public BufferedImage downloadAndWatermark(String objectKey, String username, Long userId) throws Exception {
OSS ossClient = new OSSClientBuilder().build(
aliyunOSSConfig.getEndpoint(),
aliyunOSSConfig.getAccessKeyId(),
aliyunOSSConfig.getAccessKeySecret()
);
InputStream inputStream = ossClient.getObject(aliyunOSSConfig.getBucketName(), objectKey).getObjectContent();
BufferedImage image = ImageIO.read(inputStream);
Graphics2D g2d = image.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 字体小一点
Font font = new Font("微软雅黑", Font.BOLD, 24);
g2d.setFont(font);
String text = "作者:" + username + " (ID:" + userId + ")";
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(text);
int textHeight = fm.getHeight();
// 右下角留边距
int x = image.getWidth() - textWidth - 20;
int y = image.getHeight() - 20;
// 黑色描边
g2d.setColor(Color.BLACK);
g2d.drawString(text, x - 1, y - 1);
g2d.drawString(text, x + 1, y - 1);
g2d.drawString(text, x - 1, y + 1);
g2d.drawString(text, x + 1, y + 1);
// 白色正文
g2d.setColor(Color.WHITE);
g2d.drawString(text, x, y);
g2d.dispose();
ossClient.shutdown();
return image;
}
}

View File

@@ -122,5 +122,11 @@
<select id="selectFavoriteCount" resultType="java.lang.Integer">
select dating.post.favorite_count from dating.post where post.post_id = #{postId}
</select>
<select id="getUsernameByUserId" resultType="map">
SELECT u.user_name, u.user_id, p.media_oss_keys
FROM post p
LEFT JOIN user u ON p.user_id = u.user_id
WHERE p.post_id = #{postId}
</select>
</mapper>

View File

@@ -5,22 +5,6 @@
<mapper namespace="com.bao.dating.mapper.UserMapper">
<!--根据用户名查询用户-->
<resultMap id="UserInfoVOResultMap" type="com.bao.dating.pojo.vo.UserInfoVO">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="nickname" column="nickname"/>
<result property="avatarUrl" column="avatar_url"/>
<result property="backgroundUrl" column="background_url"/>
<result property="gender" column="gender"/>
<result property="birthday" column="birthday"/>
<result property="hobbies" column="hobbies" typeHandler="com.bao.dating.handler.ListToJsonTypeHandler"/>
<result property="signature" column="signature"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
<result property="latitude" column="user_latitude"/>
<result property="longitude" column="user_longitude"/>
</resultMap>
<select id="getByUsername" resultType="com.bao.dating.pojo.entity.User">
SELECT
user_id,
@@ -44,6 +28,8 @@
<result property="signature" column="signature"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
<result property="latitude" column="user_latitude"/>
<result property="longitude" column="user_longitude"/>
</resultMap>
<select id="selectByUserId" resultMap="UserResultMap">
SELECT
@@ -96,6 +82,22 @@
<select id="selectByPhone" resultType="com.bao.dating.pojo.entity.User">
select * from dating.user where user_phone =#{phone}
</select>
<resultMap id="UserInfoVOResultMap" type="com.bao.dating.pojo.vo.UserInfoVO">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="nickname" column="nickname"/>
<result property="avatarUrl" column="avatar_url"/>
<result property="backgroundUrl" column="background_url"/>
<result property="gender" column="gender"/>
<result property="birthday" column="birthday"/>
<result property="hobbies" column="hobbies" typeHandler="com.bao.dating.handler.ListToJsonTypeHandler"/>
<result property="signature" column="signature"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
<result property="latitude" column="user_latitude"/>
<result property="longitude" column="user_longitude"/>
</resultMap>
<select id="findByLatLngRange" resultMap="UserInfoVOResultMap">
SELECT
user_id,
@@ -113,4 +115,6 @@
user_longitude
FROM user WHERE user_latitude BETWEEN #{minLat} AND #{maxLat} AND user_longitude BETWEEN #{minLng} AND #{maxLng}
</select>
</mapper>