diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..c14e90b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,39 @@ +# ============================================ +# 农产品直销平台 - 后端 Dockerfile +# 多阶段构建: Maven编译 → JRE运行 +# ============================================ + +# ---------- 第一阶段:Maven构建 ---------- +FROM maven:3.8-eclipse-temurin-17 AS builder +WORKDIR /build + +# 配置Maven阿里云镜像(加速依赖下载) +RUN mkdir -p /root/.m2 && \ + echo 'aliyun*https://maven.aliyun.com/repository/public' > /root/.m2/settings.xml + +# 先复制pom.xml,利用Docker缓存层加速(依赖不变时跳过下载) +COPY pom.xml . +RUN mvn dependency:go-offline -B + +# 复制源代码并打包 +COPY src ./src +RUN mvn clean package -DskipTests + +# ---------- 第二阶段:运行时 ---------- +FROM eclipse-temurin:17-jre +WORKDIR /app + +# 设置时区 +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" > /etc/timezone + +# 创建日志目录 +RUN mkdir -p /app/logs + +# 从构建阶段复制JAR包 +COPY --from=builder /build/target/sunny-farm-0.0.1-SNAPSHOT.jar app.jar + +EXPOSE 8080 + +# 启动命令(支持通过JAVA_OPTS传入JVM参数) +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/backend/src/main/java/com/sunnyfarm/dto/express/ExpressInfoDTO.java b/backend/src/main/java/com/sunnyfarm/dto/express/ExpressInfoDTO.java index 63f2115..9f5467e 100644 --- a/backend/src/main/java/com/sunnyfarm/dto/express/ExpressInfoDTO.java +++ b/backend/src/main/java/com/sunnyfarm/dto/express/ExpressInfoDTO.java @@ -2,7 +2,6 @@ package com.sunnyfarm.dto.express; import lombok.Data; import java.util.List; -import java.util.Map; /** * 物流综合信息DTO(包含轨迹和地图数据) @@ -10,103 +9,45 @@ import java.util.Map; @Data public class ExpressInfoDTO { - /** - * 快递单号 - */ + /** 快递单号 */ private String trackingNumber; - /** - * 快递公司名称 - */ + /** 快递公司名称 */ private String companyName; - /** - * 快递公司代码 - */ + /** 快递公司代码 */ private String companyCode; - /** - * 物流状态 - */ + /** 物流状态 */ private String status; - /** - * 当前位置 - */ + /** 当前位置 */ private String currentLocation; - /** - * 更新时间 - */ + /** 更新时间 */ private String updateTime; - /** - * 物流轨迹列表 - */ - private List traces; + /** 运输时长 */ + private String takeTime; - /** - * 地图路线数据 - */ - private MapRouteData mapRoute; + /** 快递员电话 */ + private String courierPhone; + + /** 物流轨迹地图URL(可直接iframe嵌入) */ + private String mapUrl; + + /** 物流轨迹列表 */ + private List traces; @Data public static class TraceItem { - /** - * 时间 - */ + /** 时间 */ private String time; - - /** - * 地点 - */ + /** 地点 */ private String location; - - /** - * 状态描述 - */ + /** 状态描述 */ private String status; - } - - @Data - public static class MapRouteData { - /** - * 城市列表(用于标记) - */ - private List cities; - - /** - * 路线坐标点(如果有路径规划) - */ - private List> routePoints; - - /** - * 路线信息 - */ - private String distance; - private String duration; - } - - @Data - public static class CityPoint { - /** - * 城市名称 - */ - private String name; - - /** - * 经度 - */ - private Double lng; - - /** - * 纬度 - */ - private Double lat; - - /** - * 序号 - */ - private Integer index; + /** 动作类型:1揽收,2运输,202派件,211投柜,311签收 */ + private String action; } } diff --git a/backend/src/main/java/com/sunnyfarm/service/impl/ExpressServiceImpl.java b/backend/src/main/java/com/sunnyfarm/service/impl/ExpressServiceImpl.java index f7cc06d..c21a9ad 100644 --- a/backend/src/main/java/com/sunnyfarm/service/impl/ExpressServiceImpl.java +++ b/backend/src/main/java/com/sunnyfarm/service/impl/ExpressServiceImpl.java @@ -1,8 +1,6 @@ package com.sunnyfarm.service.impl; -import com.fasterxml.jackson.databind.ObjectMapper; import com.sunnyfarm.config.properties.AliyunExpressProperties; -import com.sunnyfarm.config.properties.AmapProperties; import com.sunnyfarm.dto.express.ExpressInfoDTO; import com.sunnyfarm.dto.express.ExpressTraceDTO; import com.sunnyfarm.entity.Order; @@ -13,16 +11,19 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; /** * 物流查询服务实现 + * 使用两个API: + * 1. 旧API(wuliu.market.alicloudapi.com)查询物流轨迹信息 + * 2. 新API(lhkdwlgjdt.market.alicloudapi.com)查询物流轨迹地图 */ @Slf4j @Service @@ -30,57 +31,46 @@ import java.util.stream.Collectors; public class ExpressServiceImpl implements ExpressService { private final AliyunExpressProperties expressProperties; - private final AmapProperties amapProperties; private final OrderMapper orderMapper; private final RestTemplate restTemplate = new RestTemplate(); - private final ObjectMapper objectMapper = new ObjectMapper(); - // 预设主要城市坐标 - private static final Map CITY_COORDINATES = new HashMap<>(); - - static { - CITY_COORDINATES.put("广州市", new double[]{113.264385, 23.129163}); - CITY_COORDINATES.put("深圳市", new double[]{114.057868, 22.543099}); - CITY_COORDINATES.put("北京市", new double[]{116.407526, 39.904030}); - CITY_COORDINATES.put("上海市", new double[]{121.473701, 31.230416}); - CITY_COORDINATES.put("杭州市", new double[]{120.153576, 30.287459}); - CITY_COORDINATES.put("南京市", new double[]{118.796877, 32.060255}); - CITY_COORDINATES.put("成都市", new double[]{104.065735, 30.659462}); - CITY_COORDINATES.put("武汉市", new double[]{114.305393, 30.593099}); - CITY_COORDINATES.put("西安市", new double[]{108.939621, 34.341568}); - CITY_COORDINATES.put("重庆市", new double[]{106.551557, 29.563009}); - CITY_COORDINATES.put("天津市", new double[]{117.200983, 39.084158}); - CITY_COORDINATES.put("苏州市", new double[]{120.585315, 31.298886}); - CITY_COORDINATES.put("东莞市", new double[]{113.751765, 23.020673}); - CITY_COORDINATES.put("佛山市", new double[]{113.121416, 23.021548}); - CITY_COORDINATES.put("惠州市", new double[]{114.416196, 23.111847}); - } + // 物流轨迹地图API地址 + private static final String TRACE_MAP_URL = "https://lhkdwlgjdt.market.alicloudapi.com/express/trace-map/v2"; @Override public ExpressInfoDTO queryExpressInfo(String trackingNumber, String companyCode) { log.info("🔍 查询物流信息 - 单号: {}, 公司代码: {}", trackingNumber, companyCode); try { - // 1. 调用阿里云物流查询API - ExpressTraceDTO traceData = queryExpressTrace(trackingNumber, companyCode); - - if (traceData == null || !"0".equals(traceData.getStatus())) { - throw new BusinessException("物流信息查询失败"); - } + // 1. 用旧API查询物流轨迹 + String oldApiCode = convertToOldApiCode(companyCode); + ExpressTraceDTO traceData = queryExpressTrace(trackingNumber, oldApiCode); // 2. 解析物流轨迹数据 - ExpressInfoDTO result = parseExpressData(traceData); + ExpressInfoDTO result = parseExpressData(traceData, trackingNumber); - // 3. 提取城市并规划路线 - List cities = extractCities(traceData); - if (!cities.isEmpty()) { - ExpressInfoDTO.MapRouteData routeData = planRoute(cities); - result.setMapRoute(routeData); + // 3. 用新API查询物流地图(不影响主流程,失败也不报错) + try { + String mapApiCode = convertToMapApiCode(companyCode); + // 从trackingNumber中提取手机尾号(如果有的话) + String phone = null; + String pureTrackingNumber = trackingNumber; + if (trackingNumber.contains(":")) { + String[] parts = trackingNumber.split(":"); + pureTrackingNumber = parts[0]; + phone = parts[1]; + } + String mapUrl = queryTraceMap(pureTrackingNumber, mapApiCode, phone); + result.setMapUrl(mapUrl); + } catch (Exception e) { + log.warn("⚠️ 物流地图查询失败(不影响轨迹展示): {}", e.getMessage()); } log.info("✅ 物流信息查询成功 - 单号: {}", trackingNumber); return result; + } catch (BusinessException e) { + throw e; } catch (Exception e) { log.error("❌ 物流信息查询失败", e); throw new BusinessException("物流信息查询失败: " + e.getMessage()); @@ -100,14 +90,56 @@ public class ExpressServiceImpl implements ExpressService { throw new BusinessException("订单暂无物流信息"); } - // 将订单的物流公司名称转换为代码 String companyCode = convertCompanyNameToCode(order.getShipCompany()); + String trackingNumber = order.getShipNo(); - return queryExpressInfo(order.getShipNo(), companyCode); + // 顺丰需要拼接手机尾号到trackingNumber(旧API需要) + String phone = null; + if ("SF".equals(companyCode) && order.getPhone() != null && order.getPhone().length() >= 4) { + phone = order.getPhone().substring(order.getPhone().length() - 4); + if (!trackingNumber.contains(":")) { + trackingNumber = trackingNumber + ":" + phone; + log.info("📱 顺丰快递自动拼接手机尾号: {} -> {}", order.getShipNo(), trackingNumber); + } + } + + // 对于新地图API,中通也需要手机号 + if ("ZTO".equals(companyCode) && phone == null && order.getPhone() != null && order.getPhone().length() >= 4) { + phone = order.getPhone().substring(order.getPhone().length() - 4); + } + + try { + // 1. 用旧API查询物流轨迹 + String oldApiCode = convertToOldApiCode(companyCode); + ExpressTraceDTO traceData = queryExpressTrace(trackingNumber, oldApiCode); + + // 2. 解析物流轨迹数据 + ExpressInfoDTO result = parseExpressData(traceData, order.getShipNo()); + + // 3. 用新API查询物流地图 + try { + String mapApiCode = convertToMapApiCode(companyCode); + String mapUrl = queryTraceMap(order.getShipNo(), mapApiCode, phone); + result.setMapUrl(mapUrl); + } catch (Exception e) { + log.warn("⚠️ 物流地图查询失败(不影响轨迹展示): {}", e.getMessage()); + } + + log.info("✅ 物流信息查询成功 - 单号: {}", order.getShipNo()); + return result; + + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + log.error("❌ 物流信息查询失败", e); + throw new BusinessException("物流信息查询失败: " + e.getMessage()); + } } + // ==================== 旧API:查询物流轨迹 ==================== + /** - * 调用阿里云物流查询API + * 调用旧API查询物流轨迹 */ private ExpressTraceDTO queryExpressTrace(String trackingNumber, String companyCode) { try { @@ -123,56 +155,58 @@ public class ExpressServiceImpl implements ExpressService { HttpEntity entity = new HttpEntity<>(headers); - log.info("📡 调用阿里云物流API: {}", url); + log.info("📡 调用物流轨迹API: {}", url); ResponseEntity response = restTemplate.exchange( - url, - HttpMethod.GET, - entity, - ExpressTraceDTO.class - ); + url, HttpMethod.GET, entity, ExpressTraceDTO.class); - log.info("✅ 阿里云物流API响应成功"); - return response.getBody(); + ExpressTraceDTO body = response.getBody(); + log.info("✅ 物流轨迹API响应 - status={}, msg={}", + body != null ? body.getStatus() : "null", + body != null ? body.getMsg() : "null"); + if (body == null || (!"0".equals(body.getStatus()) && !"ok".equals(body.getMsg()))) { + String msg = body != null ? body.getMsg() : "返回为空"; + throw new BusinessException("物流信息查询失败: " + msg); + } + + return body; + + } catch (BusinessException e) { + throw e; } catch (Exception e) { - log.error("❌ 调用阿里云物流API失败", e); + log.error("❌ 调用物流轨迹API失败", e); throw new BusinessException("物流查询服务异常"); } } /** - * 解析物流数据 + * 解析物流轨迹数据 */ - private ExpressInfoDTO parseExpressData(ExpressTraceDTO traceData) { + private ExpressInfoDTO parseExpressData(ExpressTraceDTO traceData, String trackingNumber) { ExpressInfoDTO result = new ExpressInfoDTO(); ExpressTraceDTO.ExpressResult data = traceData.getResult(); - result.setTrackingNumber(data.getNumber()); + result.setTrackingNumber(trackingNumber); result.setCompanyName(data.getExpName()); result.setCompanyCode(data.getType()); result.setStatus(getStatusText(data.getDeliverystatus())); result.setUpdateTime(data.getUpdateTime()); + result.setCourierPhone(data.getCourierPhone()); - // 转换物流轨迹 if (data.getList() != null && !data.getList().isEmpty()) { List traces = data.getList().stream() .map(trace -> { ExpressInfoDTO.TraceItem item = new ExpressInfoDTO.TraceItem(); item.setTime(trace.getTime()); item.setStatus(trace.getStatus()); - - // 提取地点信息 - String location = extractLocation(trace.getStatus()); - item.setLocation(location); - + item.setLocation(extractLocation(trace.getStatus())); return item; }) .collect(Collectors.toList()); result.setTraces(traces); - // 设置当前位置(最新的轨迹) if (!traces.isEmpty()) { result.setCurrentLocation(traces.get(0).getLocation()); } @@ -181,201 +215,114 @@ public class ExpressServiceImpl implements ExpressService { return result; } + // ==================== 新API:查询物流轨迹地图 ==================== + /** - * 提取城市列表 + * 调用新API查询物流轨迹地图,返回mapUrl */ - private List extractCities(ExpressTraceDTO traceData) { - Set citySet = new LinkedHashSet<>(); + @SuppressWarnings("unchecked") + private String queryTraceMap(String trackingNumber, String companyCode, String phone) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "APPCODE " + expressProperties.getAppCode()); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - if (traceData.getResult() != null && traceData.getResult().getList() != null) { - for (ExpressTraceDTO.ExpressTrace trace : traceData.getResult().getList()) { - String location = extractLocation(trace.getStatus()); - if (location != null && !location.isEmpty()) { - // 尝试匹配城市名称 - for (String city : CITY_COORDINATES.keySet()) { - if (location.contains(city.replace("市", ""))) { - citySet.add(city); - break; - } - } - } - } + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("shipperCode", companyCode); + body.add("logisticCode", trackingNumber); + body.add("sort", "asc"); + + if (phone != null && !phone.isEmpty()) { + body.add("phone", phone); } - List cities = new ArrayList<>(citySet); - // 反转列表,使其按照物流顺序排列 - Collections.reverse(cities); + log.info("📡 调用物流地图API - 单号: {}, 公司: {}, phone: {}", trackingNumber, companyCode, phone); - log.info("🏙️ 提取到的城市列表: {}", cities); - return cities; + HttpEntity> request = new HttpEntity<>(body, headers); + + Map response = restTemplate.postForObject(TRACE_MAP_URL, request, Map.class); + + if (response == null || !Boolean.TRUE.equals(response.get("success"))) { + String msg = response != null ? (String) response.get("msg") : "返回为空"; + log.warn("⚠️ 物流地图API返回失败: {}", msg); + return null; + } + + Map data = (Map) response.get("data"); + if (data != null) { + String mapUrl = (String) data.get("mapUrl"); + log.info("🗺️ 获取到物流地图URL: {}", mapUrl); + return mapUrl; + } + + return null; + } + + // ==================== 编码转换 ==================== + + /** + * 统一内部编码(基于快递鸟标准) + */ + private String convertCompanyNameToCode(String companyName) { + if (companyName == null) return null; + + Map companyMap = new LinkedHashMap<>(); + companyMap.put("顺丰", "SF"); + companyMap.put("圆通", "YTO"); + companyMap.put("中通", "ZTO"); + companyMap.put("韵达", "YD"); + companyMap.put("申通", "STO"); + companyMap.put("邮政", "EMS"); + companyMap.put("EMS", "EMS"); + companyMap.put("京东", "JD"); + companyMap.put("极兔", "JTSD"); + + for (Map.Entry entry : companyMap.entrySet()) { + if (companyName.contains(entry.getKey())) { + return entry.getValue(); + } + } + return null; } /** - * 提取地点信息 + * 转换为旧API(wuliu)的编码 */ + private String convertToOldApiCode(String code) { + if (code == null) return null; + Map map = new HashMap<>(); + map.put("SF", "SFEXPRESS"); + map.put("YTO", "YTO"); + map.put("ZTO", "ZTO"); + map.put("YD", "YUNDA"); + map.put("STO", "STO"); + map.put("EMS", "EMS"); + map.put("JD", "JD"); + map.put("JTSD", "JTSD"); + return map.getOrDefault(code, code); + } + + /** + * 转换为新API(快递鸟地图)的编码 + */ + private String convertToMapApiCode(String code) { + // 新API用的就是快递鸟标准编码,和内部编码一致 + return code; + } + + // ==================== 工具方法 ==================== + private String extractLocation(String status) { if (status == null) return ""; - - // 匹配【城市】格式 - Pattern pattern = Pattern.compile("【(.+?)】"); - Matcher matcher = pattern.matcher(status); + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("【(.+?)】"); + java.util.regex.Matcher matcher = pattern.matcher(status); if (matcher.find()) { return matcher.group(1); } - return ""; } - /** - * 规划路线 - */ - private ExpressInfoDTO.MapRouteData planRoute(List cities) { - ExpressInfoDTO.MapRouteData routeData = new ExpressInfoDTO.MapRouteData(); - - // 添加城市标记点 - List cityPoints = new ArrayList<>(); - for (int i = 0; i < cities.size(); i++) { - String city = cities.get(i); - double[] coords = CITY_COORDINATES.get(city); - - if (coords != null) { - ExpressInfoDTO.CityPoint point = new ExpressInfoDTO.CityPoint(); - point.setName(city); - point.setLng(coords[0]); - point.setLat(coords[1]); - point.setIndex(i); - cityPoints.add(point); - } - } - routeData.setCities(cityPoints); - - // 如果有多个城市,规划路线 - if (cityPoints.size() >= 2) { - try { - // 调用高德驾车路径规划API - ExpressInfoDTO.CityPoint origin = cityPoints.get(0); - ExpressInfoDTO.CityPoint destination = cityPoints.get(cityPoints.size() - 1); - - String waypoints = null; - if (cityPoints.size() > 2) { - // 中间途经点 - waypoints = cityPoints.subList(1, cityPoints.size() - 1).stream() - .map(p -> p.getLng() + "," + p.getLat()) - .collect(Collectors.joining(";")); - } - - Map routeResult = queryDrivingRoute( - origin.getLng() + "," + origin.getLat(), - destination.getLng() + "," + destination.getLat(), - waypoints - ); - - // 解析路线数据 - if (routeResult != null && "1".equals(routeResult.get("status"))) { - Map route = (Map) routeResult.get("route"); - if (route != null) { - List> paths = (List>) route.get("paths"); - if (paths != null && !paths.isEmpty()) { - Map path = paths.get(0); - routeData.setDistance(path.get("distance").toString()); - routeData.setDuration(path.get("duration").toString()); - - // 提取路径点 - List> routePoints = extractRoutePoints(path); - routeData.setRoutePoints(routePoints); - } - } - } - - } catch (Exception e) { - log.warn("⚠️ 路径规划失败,使用直线连接: {}", e.getMessage()); - } - } - - return routeData; - } - - /** - * 调用高德驾车路径规划API - */ - private Map queryDrivingRoute(String origin, String destination, String waypoints) { - try { - // 构建URL,确保坐标格式正确 - StringBuilder urlBuilder = new StringBuilder(); - urlBuilder.append(amapProperties.getWebApi().getBaseUrl()) - .append("/v3/direction/driving") - .append("?key=").append(amapProperties.getWebApi().getKey()) - .append("&origin=").append(origin) - .append("&destination=").append(destination) - .append("&output=json"); - - if (waypoints != null && !waypoints.isEmpty()) { - urlBuilder.append("&waypoints=").append(waypoints); - } - - String url = urlBuilder.toString(); - - log.info("🚗 调用高德路径规划API: {}", url); - - // 验证坐标格式 - if (!origin.matches("\\d+\\.\\d+,\\d+\\.\\d+")) { - log.error("❌ 起点坐标格式错误: {}", origin); - return null; - } - if (!destination.matches("\\d+\\.\\d+,\\d+\\.\\d+")) { - log.error("❌ 终点坐标格式错误: {}", destination); - return null; - } - - Map result = restTemplate.getForObject(url, Map.class); - - log.info("✅ 高德路径规划API响应成功"); - return result; - - } catch (Exception e) { - log.error("❌ 高德路径规划API调用失败", e); - return null; - } - } - - /** - * 提取路径点 - */ - private List> extractRoutePoints(Map path) { - List> points = new ArrayList<>(); - - try { - List> steps = (List>) path.get("steps"); - if (steps != null) { - for (Map step : steps) { - String polyline = (String) step.get("polyline"); - if (polyline != null) { - String[] coords = polyline.split(";"); - for (String coord : coords) { - String[] lngLat = coord.split(","); - if (lngLat.length == 2) { - points.add(Arrays.asList( - Double.parseDouble(lngLat[0]), - Double.parseDouble(lngLat[1]) - )); - } - } - } - } - } - } catch (Exception e) { - log.warn("⚠️ 解析路径点失败: {}", e.getMessage()); - } - - return points; - } - - /** - * 获取状态文本 - */ private String getStatusText(String status) { if (status == null) return "未知"; - switch (status) { case "0": return "在途中"; case "1": return "已揽收"; @@ -387,33 +334,4 @@ public class ExpressServiceImpl implements ExpressService { default: return "未知"; } } - - /** - * 转换物流公司名称为代码 - */ - private String convertCompanyNameToCode(String companyName) { - if (companyName == null) return null; - - Map companyMap = new HashMap<>(); - companyMap.put("韵达", "yunda"); - companyMap.put("韵达快递", "yunda"); - companyMap.put("顺丰", "shunfeng"); - companyMap.put("顺丰快递", "shunfeng"); - companyMap.put("圆通", "yuantong"); - companyMap.put("圆通快递", "yuantong"); - companyMap.put("中通", "zhongtong"); - companyMap.put("中通快递", "zhongtong"); - companyMap.put("申通", "shentong"); - companyMap.put("申通快递", "shentong"); - companyMap.put("EMS", "ems"); - companyMap.put("邮政", "ems"); - - for (Map.Entry entry : companyMap.entrySet()) { - if (companyName.contains(entry.getKey())) { - return entry.getValue(); - } - } - - return null; - } } diff --git a/backend/src/main/resources/mapper/MerchantMapper.xml b/backend/src/main/resources/mapper/MerchantMapper.xml index 9a4d3ee..fbe8ee6 100644 --- a/backend/src/main/resources/mapper/MerchantMapper.xml +++ b/backend/src/main/resources/mapper/MerchantMapper.xml @@ -136,8 +136,8 @@ SELECT DATE(created_at) as date, - IFNULL(SUM(actual_amount), 0) as amount, - COUNT(*) as orderCount + IFNULL(SUM(actual_amount), 0) as sales, + COUNT(*) as orders FROM orders WHERE merchant_id = #{merchantId} AND status IN (2, 3, 4) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..95e1c18 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +version: '3.8' + +services: + # ===== 后端服务 - Spring Boot ===== + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: sunnyfarm-backend + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=prod,docker + - TZ=Asia/Shanghai + - JAVA_OPTS=-Xms256m -Xmx512m + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ./logs/backend:/app/logs + restart: always + networks: + - sunnyfarm-net + + # ===== 前端服务 - Nginx ===== + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: sunnyfarm-frontend + ports: + - "53921:80" + environment: + - TZ=Asia/Shanghai + restart: always + depends_on: + - backend + networks: + - sunnyfarm-net + +networks: + sunnyfarm-net: + driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..a2f582d --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,36 @@ +# ============================================ +# 农产品直销平台 - 前端 Dockerfile +# 多阶段构建: Node编译 → Nginx部署 +# ============================================ + +# ---------- 第一阶段:Node构建 ---------- +FROM node:18-alpine AS builder +WORKDIR /app + +# 配置npm镜像(加速依赖下载) +RUN npm config set registry https://registry.npmmirror.com + +# 先复制package文件,利用Docker缓存层 +COPY package*.json ./ +RUN npm install + +# 复制源代码并构建 +COPY . . +RUN npm run build + +# ---------- 第二阶段:Nginx部署 ---------- +FROM nginx:1.25-alpine + +# 设置时区 +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" > /etc/timezone + +# 复制自定义Nginx配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 从构建阶段复制静态文件 +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..a590b0a --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,32 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip压缩 + gzip on; + gzip_min_length 1k; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + gzip_vary on; + + # Vue Router History模式 - SPA路由支持 + location / { + try_files $uri $uri/ /index.html; + } + + # 静态资源长期缓存(Vite构建带hash) + location /assets/ { + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # 图片等资源缓存 + location ~* \.(ico|png|jpg|jpeg|gif|svg|webp)$ { + expires 7d; + add_header Cache-Control "public"; + access_log off; + } +} diff --git a/frontend/src/components/ExpressTracking.vue b/frontend/src/components/ExpressTracking.vue index f41573a..e55b27b 100644 --- a/frontend/src/components/ExpressTracking.vue +++ b/frontend/src/components/ExpressTracking.vue @@ -36,10 +36,14 @@ 更新时间:{{ expressInfo.updateTime }} + + + 快递员电话:{{ expressInfo.courierPhone }} + - - + + @@ -51,7 +55,13 @@ - + @@ -68,7 +78,7 @@ :key="index" :timestamp="trace.time" placement="top" - :type="index === 0 ? 'primary' : 'info'" + :type="getTimelineType(trace, index)" :size="index === 0 ? 'large' : 'normal'" :hollow="index !== 0" > @@ -87,7 +97,7 @@