Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/main/java/iuh/fit/se/ecommerce/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package iuh.fit.se.ecommerce.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
// Dùng in-memory cache (ConcurrentMap)
// Nếu có Redis, thay bằng RedisCacheManager
return new ConcurrentMapCacheManager("nominatim");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package iuh.fit.se.ecommerce.config;

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.RoundingMode;

@Component("nominatimKeyGenerator")
public class NominatimCacheKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
BigDecimal lat = (BigDecimal) params[0];
BigDecimal lng = (BigDecimal) params[1];

// Round to 4 decimals (≈11m accuracy) for cache key
BigDecimal roundedLat = lat.setScale(4, RoundingMode.HALF_UP);
BigDecimal roundedLng = lng.setScale(4, RoundingMode.HALF_UP);

return roundedLat.toString() + ":" + roundedLng.toString();
}
}

7 changes: 7 additions & 0 deletions src/main/java/iuh/fit/se/ecommerce/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/profile.html",
"/promotions.html",
"/products.html",
"/search-results.html",
"/forgot-password.html",
"/payment-success.html",
"/payment-cancel.html",
Expand Down Expand Up @@ -97,6 +98,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers(HttpMethod.POST, "/api/payments/create").authenticated()
.requestMatchers(HttpMethod.GET, "/api/payments/status/**").authenticated()

// Address endpoints (authenticated)
.requestMatchers("/api/addresses/**").authenticated()

// Geocoding endpoints (authenticated)
.requestMatchers(HttpMethod.GET, "/api/geocoding/**").authenticated()

// Admin endpoints (require roles for page access)
.requestMatchers(HttpMethod.GET, "/admin/**").hasAnyRole("ADMIN", "EDITOR")
.requestMatchers(HttpMethod.POST, "/admin/**").hasAnyRole("ADMIN", "EDITOR")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package iuh.fit.se.ecommerce.controller;

import iuh.fit.se.ecommerce.dto.request.AddressRequest;
import iuh.fit.se.ecommerce.dto.response.AddressResponse;
import iuh.fit.se.ecommerce.service.interfaces.AddressService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/addresses")
@RequiredArgsConstructor
public class AddressController {

private final AddressService addressService;

@GetMapping
public ResponseEntity<List<AddressResponse>> getUserAddresses(
@AuthenticationPrincipal UserDetails userDetails) {
List<AddressResponse> addresses = addressService.getUserAddresses(userDetails.getUsername());
return ResponseEntity.ok(addresses);
}

@GetMapping("/{id}")
public ResponseEntity<AddressResponse> getAddress(
@PathVariable Long id,
@AuthenticationPrincipal UserDetails userDetails) {
AddressResponse address = addressService.getAddressById(id, userDetails.getUsername());
return ResponseEntity.ok(address);
}

@PostMapping
public ResponseEntity<AddressResponse> createAddress(
@AuthenticationPrincipal UserDetails userDetails,
@Valid @RequestBody AddressRequest request) {
AddressResponse address = addressService.createAddress(request, userDetails.getUsername());
return ResponseEntity.ok(address);
}

@PutMapping("/{id}")
public ResponseEntity<AddressResponse> updateAddress(
@PathVariable Long id,
@AuthenticationPrincipal UserDetails userDetails,
@Valid @RequestBody AddressRequest request) {
AddressResponse address = addressService.updateAddress(id, request, userDetails.getUsername());
return ResponseEntity.ok(address);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteAddress(
@PathVariable Long id,
@AuthenticationPrincipal UserDetails userDetails) {
addressService.deleteAddress(id, userDetails.getUsername());
return ResponseEntity.noContent().build();
}

@PutMapping("/{id}/set-default")
public ResponseEntity<AddressResponse> setDefaultAddress(
@PathVariable Long id,
@AuthenticationPrincipal UserDetails userDetails) {
AddressResponse address = addressService.setDefaultAddress(id, userDetails.getUsername());
return ResponseEntity.ok(address);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package iuh.fit.se.ecommerce.controller;

import iuh.fit.se.ecommerce.dto.response.AddressGeocodeResponse;
import iuh.fit.se.ecommerce.service.interfaces.NominatimService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;

@RestController
@RequestMapping("/api/geocoding")
@RequiredArgsConstructor
public class GeocodingController {

private final NominatimService nominatimService;

@GetMapping("/reverse")
public ResponseEntity<AddressGeocodeResponse> reverseGeocode(
@RequestParam BigDecimal lat,
@RequestParam BigDecimal lng) {

AddressGeocodeResponse result = nominatimService.reverseGeocode(lat, lng);
return ResponseEntity.ok(result);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import iuh.fit.se.ecommerce.dto.response.ProductResponse;
import iuh.fit.se.ecommerce.service.interfaces.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand Down Expand Up @@ -54,4 +55,20 @@ public ResponseEntity<List<ProductResponse>> getHotSaleProducts(
@RequestParam(required = false, defaultValue = "6") int limit) {
return ResponseEntity.ok(productService.getHotSaleProducts(limit));
}

@GetMapping("/search/autocomplete")
public ResponseEntity<Map<String, Object>> searchAutocomplete(
@RequestParam String q,
@RequestParam(required = false, defaultValue = "5") int limit) {
return ResponseEntity.ok(productService.searchAutocomplete(q, limit));
}

@GetMapping("/search")
public ResponseEntity<Page<ProductResponse>> searchProducts(
@RequestParam String q,
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "20") int size,
@RequestParam(required = false, defaultValue = "default") String sort) {
return ResponseEntity.ok(productService.searchProducts(q, page, size, sort));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public String products() {
return "products";
}

@GetMapping("/search-results.html")
public String searchResults() {
return "search-results";
}

@GetMapping("/oauth2/callback")
public String oauth2Callback() {
return "auth/oauth2-callback";
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/iuh/fit/se/ecommerce/dto/request/AddressRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package iuh.fit.se.ecommerce.dto.request;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AddressRequest {
private String label; // Nhãn địa chỉ (tùy chọn): "Nhà riêng", "Công ty", etc.

@NotBlank(message = "Tên người nhận không được để trống")
private String receiverName;

@NotBlank(message = "Số điện thoại không được để trống")
private String receiverPhone;

@NotBlank(message = "Tỉnh/TP không được để trống")
private String province;

@NotBlank(message = "Phường/Xã không được để trống")
private String ward;

@NotBlank(message = "Địa chỉ chi tiết không được để trống")
private String detail;

private Boolean isDefault = false;

// Tọa độ (optional - có thể null nếu user nhập tay không dùng map)
private Double latitude; // Vĩ độ
private Double longitude; // Kinh độ
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package iuh.fit.se.ecommerce.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AddressGeocodeResponse {
private String houseNumber;
private String road;
private String ward;
private String district;
private String province;
private String country;
private String countryCode;
private String postcode;
private String displayName;
private String fullAddress;
}

Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
@AllArgsConstructor
public class AddressResponse {
private Long id;
private String label;
private String receiverName;
private String receiverPhone;
private String receiverEmail;
private String country;
private String province;
private String district;
private String ward;
private String addressDetail;
private boolean isDefault;
private Double latitude; // Vĩ độ (có thể null)
private Double longitude; // Kinh độ (có thể null)
}

17 changes: 16 additions & 1 deletion src/main/java/iuh/fit/se/ecommerce/entity/Address.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;

@Entity
@Table(name = "addresses")
Expand All @@ -20,14 +24,25 @@ public class Address {
private String receiverName;
private String receiverPhone;
private String province;
private String district;
private String ward;
private String detail;

private boolean isDefault = false;

@Column(nullable = true)
private Double latitude; // Vĩ độ (optional - có thể null nếu user nhập tay)

@Column(nullable = true)
private Double longitude; // Kinh độ (optional - có thể null nếu user nhập tay)

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@JsonBackReference
private User user;

@CreationTimestamp
private LocalDateTime createdAt;

@UpdateTimestamp
private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
package iuh.fit.se.ecommerce.repository;

import iuh.fit.se.ecommerce.entity.Address;
import iuh.fit.se.ecommerce.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

public interface AddressRepository extends JpaRepository<Address, Long> {}
import java.util.List;
import java.util.Optional;

@Repository
public interface AddressRepository extends JpaRepository<Address, Long> {
List<Address> findByUserOrderByIsDefaultDescCreatedAtDesc(User user);
Optional<Address> findByIdAndUser(Long id, User user);
Optional<Address> findByUserAndIsDefaultTrue(User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@

public interface ProductRepositoryCustom {
Page<Product> search(ProductSearchCriteria criteria, Pageable pageable);
Page<Product> search(ProductSearchCriteria criteria, Pageable pageable, String sort);
}

Loading