AddressService.java
package com.ecommerce.identity.application.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.ecommerce.identity.application.exception.IdentityError;
import com.ecommerce.identity.application.exception.IdentityException;
import com.ecommerce.identity.application.model.AddressModels.AddressCommand;
import com.ecommerce.identity.application.model.AddressModels.AddressView;
import com.ecommerce.identity.infrastructure.persistence.entity.UserAddressEntity;
import com.ecommerce.identity.infrastructure.persistence.mapper.UserAccountMapper;
import com.ecommerce.identity.infrastructure.persistence.mapper.UserAddressMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.List;
@Service
public class AddressService {
private static final int MAX_ADDRESSES = 20;
private final UserAddressMapper addressMapper;
private final UserAccountMapper userMapper;
public AddressService(UserAddressMapper addressMapper, UserAccountMapper userMapper) {
this.addressMapper = addressMapper;
this.userMapper = userMapper;
}
@Transactional
public AddressView create(Long userId, AddressCommand command) {
lockUser(userId);
long count = addressMapper.selectCount(new LambdaQueryWrapper<UserAddressEntity>()
.eq(UserAddressEntity::getUserId, userId));
if (count >= MAX_ADDRESSES) {
throw new IdentityException(IdentityError.ADDRESS_LIMIT_REACHED);
}
Instant now = userMapper.currentTime();
boolean defaultAddress = count == 0 || command.setDefault();
if (defaultAddress) {
addressMapper.clearDefault(userId, now);
}
UserAddressEntity address = new UserAddressEntity();
address.setId(IdWorker.getId());
address.setUserId(userId);
apply(address, command);
address.setIsDefault(defaultAddress);
address.setVersion(0);
address.setCreatedAt(now);
address.setUpdatedAt(now);
addressMapper.insert(address);
return view(address);
}
public List<AddressView> list(Long userId) {
return addressMapper.selectList(new LambdaQueryWrapper<UserAddressEntity>()
.eq(UserAddressEntity::getUserId, userId)
.orderByDesc(UserAddressEntity::getIsDefault)
.orderByDesc(UserAddressEntity::getUpdatedAt))
.stream().map(this::view).toList();
}
public AddressView getOwned(Long userId, Long addressId) {
UserAddressEntity address = addressMapper.selectOne(new LambdaQueryWrapper<UserAddressEntity>()
.eq(UserAddressEntity::getId, addressId)
.eq(UserAddressEntity::getUserId, userId));
if (address == null) {
throw new IdentityException(IdentityError.ADDRESS_NOT_FOUND);
}
return view(address);
}
@Transactional
public AddressView update(Long userId, Long addressId, AddressCommand command) {
lockUser(userId);
UserAddressEntity address = requireLocked(userId, addressId);
Instant now = userMapper.currentTime();
if (command.setDefault()) {
addressMapper.clearDefault(userId, now);
address.setIsDefault(true);
}
apply(address, command);
address.setUpdatedAt(now);
requireUpdated(addressMapper.updateById(address));
return view(address);
}
@Transactional
public AddressView setDefault(Long userId, Long addressId) {
lockUser(userId);
UserAddressEntity address = requireLocked(userId, addressId);
if (Boolean.TRUE.equals(address.getIsDefault())) {
return view(address);
}
Instant now = userMapper.currentTime();
addressMapper.clearDefault(userId, now);
address.setIsDefault(true);
address.setUpdatedAt(now);
requireUpdated(addressMapper.updateById(address));
return view(address);
}
@Transactional
public void delete(Long userId, Long addressId) {
lockUser(userId);
UserAddressEntity address = requireLocked(userId, addressId);
boolean wasDefault = Boolean.TRUE.equals(address.getIsDefault());
addressMapper.deleteById(address.getId());
if (wasDefault) {
UserAddressEntity replacement = addressMapper.selectOne(
new LambdaQueryWrapper<UserAddressEntity>()
.eq(UserAddressEntity::getUserId, userId)
.orderByDesc(UserAddressEntity::getUpdatedAt)
.last("LIMIT 1"));
if (replacement != null) {
replacement.setIsDefault(true);
replacement.setUpdatedAt(userMapper.currentTime());
requireUpdated(addressMapper.updateById(replacement));
}
}
}
private void lockUser(Long userId) {
if (userMapper.lockUser(userId) == null) {
throw new IdentityException(IdentityError.ACCOUNT_NOT_FOUND);
}
}
private UserAddressEntity requireLocked(Long userId, Long addressId) {
UserAddressEntity address = addressMapper.selectOwnedForUpdate(userId, addressId);
if (address == null) {
throw new IdentityException(IdentityError.ADDRESS_NOT_FOUND);
}
return address;
}
private void apply(UserAddressEntity address, AddressCommand command) {
address.setRecipientName(command.recipientName().trim());
address.setPhone(command.phone().trim());
address.setProvince(command.province().trim());
address.setProvinceCode(command.provinceCode().trim());
address.setCity(command.city().trim());
address.setCityCode(command.cityCode().trim());
address.setDistrict(command.district().trim());
address.setDistrictCode(command.districtCode().trim());
address.setDetailAddress(command.detailAddress().trim());
address.setPostalCode(command.postalCode() == null || command.postalCode().isBlank()
? null : command.postalCode().trim());
}
private void requireUpdated(int rows) {
if (rows != 1) {
throw new IdentityException(IdentityError.CONCURRENT_MODIFICATION);
}
}
private AddressView view(UserAddressEntity address) {
return new AddressView(address.getId(), address.getRecipientName(), address.getPhone(),
address.getProvince(), address.getProvinceCode(), address.getCity(), address.getCityCode(),
address.getDistrict(), address.getDistrictCode(), address.getDetailAddress(),
address.getPostalCode(), Boolean.TRUE.equals(address.getIsDefault()), address.getVersion(),
address.getCreatedAt(), address.getUpdatedAt());
}
}