附近门店搜索系统
业务需求:
- 用户可查询当前位置附近5公里内的所有门店
- 结果按距离排序,显示距离信息
- 系统需要支持高频的位置更新和查询
①、数据结构
java
// 门店位置信息
@Data
public class StoreLocation {
private Long storeId;
private String storeName;
private String address;
private Double longitude; // 经度
private Double latitude; // 纬度
}
java
@Data
class NearbyStore {
private Long storeId;
private String storeName;
private String address;
private Double distance;
private String distanceUnit;
}
②、Redis配置
java
@Configuration
public class RedisGeoConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
③、地理位置服务实现
java
@Service
public class GeoLocationService {
private static final String STORE_GEO_KEY = "stores:geo";
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public GeoLocationService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
添加或更新门店位置
*/
public void addOrUpdateStoreLocation(StoreLocation store){
redisTemplate.opsForGeo().add(STORE_GEO_KEY,
new Point(store.getLongitude(), store.getLatitude()),
store.getStoreId().toString());
}
/**
删除门店位置
*/
public void removeStoreLocation(){
redisTemplate.opsForGeo().remove(STORE_GEO_KEY, storeId.toString());
}
/**
查询附近门店
*/
public List<NearByStore> findNearByStores(double longitude, double latitude, double distance, int limit){
Circle within = new Cicle(new Point(longitude,latitude),new Distance(distance, Metrics.KILOMETERS));
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.sortAscending()
.limit(limit);
GeoResults<RedisGeoCommands.GeoLocation<Object>> results = redisTemplate.opsForGeo()
.radius(STORE_GEO_KEY, within, args);
return results.getContent().stream()
.map(this::convertToNearbyStore)
.collect(Collectors.toList());
}
private NearbyStore convertToNearbyStore(GeoResult<RedisGeoCommands.GeoLocation<Object>> geoResult) {
RedisGeoCommands.GeoLocation<Object> location = geoResult.getContent();
Distance distance = geoResult.getDistance();
NearbyStore store = new NearbyStore();
store.setStoreId(Long.parseLong(location.getName().toString()));
store.setDistance(distance.getValue());
store.setDistanceUnit(distance.getUnit());
// 这里可以补充查询门店详细信息
// store.setStoreName(...);
// store.setAddress(...);
return store;
}
/**
* 计算两个位置之间的距离
*/
public Distance calculateDistance(Long storeId1, Long storeId2) {
return redisTemplate.opsForGeo().distance(
STORE_GEO_KEY,
storeId1.toString(),
storeId2.toString(),
Metrics.KILOMETERS
);
}
/**
* 获取门店位置坐标
*/
public Point getStorePosition(Long storeId) {
List<Point> points = redisTemplate.opsForGeo()
.position(STORE_GEO_KEY, storeId.toString());
return points != null && !points.isEmpty() ? points.get(0) : null;
}
}
java
@RestController
@RequestMapping("/api/stores")
public class StoreController {
private final GeoLocationService geoLocationService;
private final StoreService storeService;
@Autowired
public StoreController(GeoLocationService geoLocationService, StoreService storeService) {
this.geoLocationService = geoLocationService;
this.storeService = storeService;
}
@PostMapping("/location")
public ResponseEntity<Void> updateStoreLocation(@RequestBody StoreLocation storeLocation) {
geoLocationService.addOrUpdateStoreLocation(storeLocation);
return ResponseEntity.ok().build();
}
@GetMapping("/nearby")
public ResponseEntity<List<NearbyStore>> findNearbyStores(
@RequestParam double longitude,
@RequestParam double latitude,
@RequestParam(defaultValue = "5") double distance,
@RequestParam(defaultValue = "10") int limit) {
List<NearbyStore> nearbyStores = geoLocationService
.findNearbyStores(longitude, latitude, distance, limit);
// 补充门店详细信息
nearbyStores.forEach(store -> {
Store storeInfo = storeService.getStoreById(store.getStoreId());
if (storeInfo != null) {
store.setStoreName(storeInfo.getName());
store.setAddress(storeInfo.getAddress());
}
});
return ResponseEntity.ok(nearbyStores);
}
}
用户行为实时分析系统
业务需求:
- 实时记录用户点击、浏览、购买等行为
- 统计热门商品实时排行榜
- 分析用户行为路径
- 实时计算关键指标(PV、UV、转化率等)
①、事件驱动模型
java
// 用户行为事件基类
@Data
public abstract class UserEvent {
private String eventId;
private Long userId;
private Long timestamp;
private String deviceId;
private String ipAddress;
}
// 页面浏览事件
@Data
@EqualsAndHashCode(callSuper = true)
public class PageViewEvent extends UserEvent {
private String pageUrl;
private String referrer;
private Long productId; // 如果是商品页
private Long duration; // 停留时长(毫秒)
}
// 商品点击事件
@Data
@EqualsAndHashCode(callSuper = true)
public class ProductClickEvent extends UserEvent {
private Long productId;
private String position; // 页面位置
}
// 购买事件
@Data
@EqualsAndHashCode(callSuper = true)
public class PurchaseEvent extends UserEvent {
private Long orderId;
private List<PurchaseItem> items;
private BigDecimal amount;
@Data
public static class PurchaseItem {
private Long productId;
private Integer quantity;
private BigDecimal price;
}
}
②、实时分析服务实现
java
@Service
public class RealtimeAnalyticsService {
private static final String PAGE_VIEWS_KEY = "stats:pageviews";
private static final String UNIQUE_VISITORS_KEY = "stats:unique_visitors";
private static final String PRODUCT_CLICKS_KEY = "stats:product_clicks";
private static final String PRODUCT_HOT_RANK_KEY = "stats:product_hot_rank";
private static final String USER_SESSION_PATH_KEY = "user:session:path:";
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper;
@Autowired
public RealtimeAnalyticsService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.objectMapper = new ObjectMapper();
}
/**
处理用户行为事件
*/
/**
* 处理用户行为事件
*/
public void processUserEvent(UserEvent event) throws JsonProcessingException {
String eventJson = objectMapper.writeValueAsString(event);
String eventType = event.getClass().getSimpleName().toLowerCase();
// 1. 存储原始事件(使用Stream)
redisTemplate.opsForStream().add("user_events", Collections.singletonMap(
eventType, eventJson
));
// 2. 根据事件类型处理
if (event instanceof PageViewEvent) {
processPageView((PageViewEvent) event);
} else if (event instanceof ProductClickEvent) {
processProductClick((ProductClickEvent) event);
} else if (event instanceof PurchaseEvent) {
processPurchase((PurchaseEvent) event);
}
}
private void processPageView(PageViewEvent event) {
String dayKey = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
// 1. 总PV统计
redisTemplate.opsForHyperLogLog().add(PAGE_VIEWS_KEY + ":" + dayKey, event.getEventId());
// 2. UV统计(按设备ID)
redisTemplate.opsForHyperLogLog().add(UNIQUE_VISITORS_KEY + ":" + dayKey, event.getDeviceId());
// 3. 记录用户会话路径
if (event.getUserId() != null) {
String pathKey = USER_SESSION_PATH_KEY + event.getUserId();
redisTemplate.opsForList().rightPush(pathKey, event.getPageUrl());
redisTemplate.expire(pathKey, 24, TimeUnit.HOURS); // 保留24小时
}
// 4. 如果是商品页,增加商品热度
if (event.getProductId() != null) {
redisTemplate.opsForZSet().incrementScore(
PRODUCT_HOT_RANK_KEY,
event.getProductId().toString(),
1 + (event.getDuration() != null ? event.getDuration() / 10000.0 : 0)
);
}
}
private void processProductClick(ProductClickEvent event) {
// 1. 商品点击计数
redisTemplate.opsForHash().increment(
PRODUCT_CLICKS_KEY,
event.getProductId().toString(),
1
);
// 2. 增加商品热度
redisTemplate.opsForZSet().incrementScore(
PRODUCT_HOT_RANK_KEY,
event.getProductId().toString(),
2 // 点击比浏览权重更高
);
}
private void processPurchase(PurchaseEvent event) {
// 1. 处理每个购买商品
for (PurchaseItem item : event.getItems()) {
// 商品销量统计
redisTemplate.opsForHash().increment(
"stats:product_sales",
item.getProductId().toString(),
item.getQuantity()
);
// 增加商品热度(购买权重最高)
redisTemplate.opsForZSet().incrementScore(
PRODUCT_HOT_RANK_KEY,
item.getProductId().toString(),
10 * item.getQuantity()
);
}
}
/**
* 获取实时PV数据
*/
public Long getPageViews(String date) {
return redisTemplate.opsForHyperLogLog().size(PAGE_VIEWS_KEY + ":" + date);
}
/**
* 获取实时UV数据
*/
public Long getUniqueVisitors(String date) {
return redisTemplate.opsForHyperLogLog().size(UNIQUE_VISITORS_KEY + ":" + date);
}
/**
* 获取热门商品排行榜
*/
public List<ProductRank> getHotProducts(int topN) {
Set<ZSetOperations.TypedTuple<Object>> tuples = redisTemplate.opsForZSet()
.reverseRangeWithScores(PRODUCT_HOT_RANK_KEY, 0, topN - 1);
return tuples.stream()
.map(tuple -> new ProductRank(
Long.parseLong(tuple.getValue().toString()),
tuple.getScore()))
.collect(Collectors.toList());
}
/**
* 获取商品点击量
*/
public Long getProductClicks(Long productId) {
Object clicks = redisTemplate.opsForHash().get(PRODUCT_CLICKS_KEY, productId.toString());
return clicks != null ? Long.parseLong(clicks.toString()) : 0L;
}
/**
* 获取用户行为路径
*/
public List<String> getUserSessionPath(Long userId) {
List<Object> path = redisTemplate.opsForList().range(USER_SESSION_PATH_KEY + userId, 0, -1);
return path != null ? path.stream()
.map(Object::toString)
.collect(Collectors.toList()) : Collections.emptyList();
}
@Data
@AllArgsConstructor
public static class ProductRank {
private Long productId;
private Double hotScore;
}
}
③、事件消费服务
java
@Service
public class UserEventConsumer {
private static final Logger logger = LoggerFactory.getLogger(UserEventConsumer.class);
private final RealtimeAnalyticsService analyticsService;
private final ObjectMapper objectMapper;
@Autowired
public UserEventConsumer(RealtimeAnalyticsService analyticsService, ObjectMapper objectMapper) {
this.analyticsService = analyticsService;
this.objectMapper = objectMapper;
}
@KafkaListener(topics = "user_events")
public void consumeUserEvent(ConsumerRecord<String, String> record) {
try {
JsonNode root = objectMapper.readTree(record.value());
String eventType = root.path("eventType").asText();
String eventData = root.path("data").toString();
UserEvent event = null;
switch (eventType) {
case "pageview":
event = objectMapper.readValue(eventData, PageViewEvent.class);
break;
case "productclick":
event = objectMapper.readValue(eventData, ProductClickEvent.class);
break;
case "purchase":
event = objectMapper.readValue(eventData, PurchaseEvent.class);
break;
default:
logger.warn("Unknown event type: {}", eventType);
}
if (event != null) {
analyticsService.processUserEvent(event);
}
} catch (Exception e) {
logger.error("Error processing user event", e);
}
}
}
④、数据展示控制器
java
@RestController
@RequestMapping("/api/analytics")
public class AnalyticsController {
private final RealtimeAnalyticsService analyticsService;
private final ProductService productService;
@Autowired
public AnalyticsController(RealtimeAnalyticsService analyticsService,
ProductService productService) {
this.analyticsService = analyticsService;
this.productService = productService;
}
@GetMapping("/today-stats")
public ResponseEntity<Map<String, Object>> getTodayStats() {
String today = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
Map<String, Object> stats = new HashMap<>();
stats.put("pageViews", analyticsService.getPageViews(today));
stats.put("uniqueVisitors", analyticsService.getUniqueVisitors(today));
return ResponseEntity.ok(stats);
}
@GetMapping("/hot-products")
public ResponseEntity<List<HotProductDTO>> getHotProducts(
@RequestParam(defaultValue = "10") int topN) {
List<RealtimeAnalyticsService.ProductRank> ranks =
analyticsService.getHotProducts(topN);
List<HotProductDTO> result = ranks.stream()
.map(rank -> {
Product product = productService.getProductById(rank.getProductId());
HotProductDTO dto = new HotProductDTO();
dto.setProductId(rank.getProductId());
dto.setProductName(product != null ? product.getName() : "Unknown");
dto.setHotScore(rank.getHotScore());
dto.setClicks(analyticsService.getProductClicks(rank.getProductId()));
return dto;
})
.collect(Collectors.toList());
return ResponseEntity.ok(result);
}
@GetMapping("/user-path/{userId}")
public ResponseEntity<List<String>> getUserPath(@PathVariable Long userId) {
return ResponseEntity.ok(analyticsService.getUserSessionPath(userId));
}
@Data
public static class HotProductDTO {
private Long productId;
private String productName;
private Double hotScore;
private Long clicks;
}
}