You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

113 lines
5.3 KiB

package com.mydreamjob.profileimage;
import com.mydreamjob.auth.*;
import com.mydreamjob.company.*;
import com.mydreamjob.common.exception.*;
import com.mydreamjob.messaging.*;
import com.mydreamjob.security.*;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
@Service @RequiredArgsConstructor
public class ProfileImageService {
private final ProfileImageRepository images;
private final ProfileImageValidator validator;
private final UserRepository users;
private final CompanyRepository companies;
private final UserRoleGuard roles;
private final CurrentUserAccessor currentUser;
private final ConversationMemberRepository members;
private final ConversationRepository conversations;
private final MessagingService messaging;
@Value("#{environment.getProperty('server.servlet.context-path', '/api/v1')}")
private String contextPath;
public record ImageView(String url, String version, String contentType, long size) {}
@Transactional
public ImageView upload(Long actor, boolean logo, MultipartFile file) {
User user = lockActor(actor, logo);
Company company = logo ? lockCompany(actor) : null;
byte[] content = validator.validate(file);
// 先 flush 删除以释放唯一键;事务失败会完整恢复旧二进制与引用。
String kind = logo ? "LOGO" : "AVATAR";
images.findByOwnerUserIdAndKind(actor, kind).ifPresent(images::delete);
images.flush();
ProfileImage image = new ProfileImage();
image.setId(UUID.randomUUID().toString());
image.setOwnerUserId(actor);
image.setCompanyId(company == null ? null : company.getId());
image.setKind(kind);
image.setContent(content);
images.saveAndFlush(image);
if (logo) company.setLogoUrl(url(image.getId()));
else user.setAvatarUrl(url(image.getId()));
return new ImageView(url(image.getId()), image.getId(), "image/jpeg", content.length);
}
@Transactional
public void delete(Long actor, boolean logo) {
User user = lockActor(actor, logo);
Company company = logo ? lockCompany(actor) : null;
images.findByOwnerUserIdAndKind(actor, logo ? "LOGO" : "AVATAR").ifPresent(images::delete);
if (logo) company.setLogoUrl(null);
else user.setAvatarUrl(null);
}
@Transactional(readOnly = true)
public byte[] read(String id) {
if (id == null || !id.matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))
throw notFound();
ProfileImage image = images.findById(id).orElseThrow(ProfileImageService::notFound);
User owner = users.findById(image.getOwnerUserId()).orElseThrow(ProfileImageService::notFound);
if (!Boolean.TRUE.equals(owner.getActive())) throw notFound();
if ("LOGO".equals(image.getKind())) {
Company company = companies.findById(image.getCompanyId()).orElseThrow(ProfileImageService::notFound);
if (!Objects.equals(company.getOwnerUserId(), owner.getId()) || !url(id).equals(company.getLogoUrl()))
throw notFound();
} else {
Long actor = currentUser.requireUserId();
roles.requireAny(actor, User.Role.STUDENT, User.Role.COMPANY, User.Role.ADMIN);
if (!url(id).equals(owner.getAvatarUrl())) throw notFound();
if (!actor.equals(owner.getId()) && !canReadPeer(actor, owner.getId()))
throw new BusinessException(ErrorCode.FORBIDDEN);
}
return image.getContent().clone();
}
private boolean canReadPeer(Long actor, Long owner) {
for (ConversationMember member : members.findByUserId(actor)) {
Long id = member.getConversationId();
var conversation = conversations.findById(id).orElse(null);
if (conversation == null || !("DIRECT".equals(conversation.getType()) || "TALENT_INVITATION".equals(conversation.getType()))
|| !members.existsByConversationIdAndUserId(id, owner)) continue;
try {
// 非匿名邀约同样展示头像,但不能绕过成员、拉黑、撤回或企业接收门禁。
messaging.assertProfileImageAccess(id, actor);
return true;
} catch (BusinessException denied) {
// 另一条会话可能仍有合法授权;数据库/系统异常不会在此吞掉。
}
}
return false;
}
private User lockActor(Long actor, boolean logo) {
User user = users.findByIdForUpdate(actor).orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
if (logo) roles.requireCompany(user);
else roles.requireAny(user, User.Role.STUDENT, User.Role.COMPANY, User.Role.ADMIN);
return user;
}
private Company lockCompany(Long actor) {
return companies.findByOwnerUserIdForUpdate(actor).orElseThrow(() -> new BusinessException(ErrorCode.COMPANY_NOT_FOUND));
}
private String url(String id) { return contextPath + "/profile-images/" + id; }
private static BusinessException notFound() { return new BusinessException(ErrorCode.PROFILE_NOT_FOUND, "image not found"); }
}