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