Commit a5543e29205911fc16ef924f798a02f8f69de78b

Authored by 杨刚
0 parents

初始化项目

Showing 158 changed files with 9319 additions and 0 deletions
.gitignore 0 → 100644
  1 +++ a/.gitignore
  1 +/.idea/
  2 +target
0 3 \ No newline at end of file
... ...
pom.xml 0 → 100644
  1 +++ a/pom.xml
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <modelVersion>4.0.0</modelVersion>
  6 +
  7 + <parent>
  8 + <groupId>org.springframework.boot</groupId>
  9 + <artifactId>spring-boot-starter-parent</artifactId>
  10 + <version>3.2.0</version>
  11 + <relativePath/>
  12 + </parent>
  13 +
  14 + <groupId>com.diligrp.tms</groupId>
  15 + <artifactId>rider-service</artifactId>
  16 + <version>1.0.0</version>
  17 + <name>rider-service</name>
  18 + <description>地利外卖骑手配送系统</description>
  19 +
  20 + <properties>
  21 + <java.version>17</java.version>
  22 + <mybatis-plus.version>3.5.7</mybatis-plus.version>
  23 + </properties>
  24 +
  25 + <dependencies>
  26 + <!-- Spring Boot Web -->
  27 + <dependency>
  28 + <groupId>org.springframework.boot</groupId>
  29 + <artifactId>spring-boot-starter-web</artifactId>
  30 + </dependency>
  31 +
  32 + <!-- MyBatis-Plus (Spring Boot 3 专用 starter) -->
  33 + <dependency>
  34 + <groupId>com.baomidou</groupId>
  35 + <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
  36 + <version>${mybatis-plus.version}</version>
  37 + </dependency>
  38 +
  39 + <!-- MySQL -->
  40 + <dependency>
  41 + <groupId>com.mysql</groupId>
  42 + <artifactId>mysql-connector-j</artifactId>
  43 + <scope>runtime</scope>
  44 + </dependency>
  45 +
  46 + <!-- Redis -->
  47 + <dependency>
  48 + <groupId>org.springframework.boot</groupId>
  49 + <artifactId>spring-boot-starter-data-redis</artifactId>
  50 + </dependency>
  51 +
  52 + <!-- Validation -->
  53 + <dependency>
  54 + <groupId>org.springframework.boot</groupId>
  55 + <artifactId>spring-boot-starter-validation</artifactId>
  56 + </dependency>
  57 +
  58 + <!-- Lombok -->
  59 + <dependency>
  60 + <groupId>org.projectlombok</groupId>
  61 + <artifactId>lombok</artifactId>
  62 + <optional>true</optional>
  63 + </dependency>
  64 +
  65 + <!-- JWT -->
  66 + <dependency>
  67 + <groupId>io.jsonwebtoken</groupId>
  68 + <artifactId>jjwt-api</artifactId>
  69 + <version>0.11.5</version>
  70 + </dependency>
  71 + <dependency>
  72 + <groupId>io.jsonwebtoken</groupId>
  73 + <artifactId>jjwt-impl</artifactId>
  74 + <version>0.11.5</version>
  75 + <scope>runtime</scope>
  76 + </dependency>
  77 + <dependency>
  78 + <groupId>io.jsonwebtoken</groupId>
  79 + <artifactId>jjwt-jackson</artifactId>
  80 + <version>0.11.5</version>
  81 + <scope>runtime</scope>
  82 + </dependency>
  83 +
  84 + <!-- Test -->
  85 + <dependency>
  86 + <groupId>org.springframework.boot</groupId>
  87 + <artifactId>spring-boot-starter-test</artifactId>
  88 + <scope>test</scope>
  89 + </dependency>
  90 + </dependencies>
  91 +
  92 + <build>
  93 + <plugins>
  94 + <plugin>
  95 + <groupId>org.springframework.boot</groupId>
  96 + <artifactId>spring-boot-maven-plugin</artifactId>
  97 + <configuration>
  98 + <excludes>
  99 + <exclude>
  100 + <groupId>org.projectlombok</groupId>
  101 + <artifactId>lombok</artifactId>
  102 + </exclude>
  103 + </excludes>
  104 + </configuration>
  105 + </plugin>
  106 + </plugins>
  107 + </build>
  108 +</project>
... ...
src/main/java/com/diligrp/rider/RiderServiceApplication.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/RiderServiceApplication.java
  1 +package com.diligrp.rider;
  2 +
  3 +import org.mybatis.spring.annotation.MapperScan;
  4 +import org.springframework.boot.SpringApplication;
  5 +import org.springframework.boot.autoconfigure.SpringBootApplication;
  6 +import org.springframework.scheduling.annotation.EnableAsync;
  7 +
  8 +@SpringBootApplication
  9 +@MapperScan("com.diligrp.rider.mapper")
  10 +@EnableAsync
  11 +public class RiderServiceApplication {
  12 + public static void main(String[] args) {
  13 + SpringApplication.run(RiderServiceApplication.class, args);
  14 + }
  15 +}
... ...
src/main/java/com/diligrp/rider/common/enums/IncomeStatusEnum.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/common/enums/IncomeStatusEnum.java
  1 +package com.diligrp.rider.common.enums;
  2 +
  3 +import lombok.Getter;
  4 +
  5 +/**
  6 + * 结算状态枚举
  7 + * 0=未结算 1=待结算 2=已结算
  8 + */
  9 +@Getter
  10 +public enum IncomeStatusEnum {
  11 + NOT_SETTLED(0, "未结算"),
  12 + PENDING(1, "待结算"),
  13 + SETTLED(2, "已结算");
  14 +
  15 + private final int code;
  16 + private final String desc;
  17 +
  18 + IncomeStatusEnum(int code, String desc) {
  19 + this.code = code;
  20 + this.desc = desc;
  21 + }
  22 +}
... ...
src/main/java/com/diligrp/rider/common/enums/OrderStatusEnum.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/common/enums/OrderStatusEnum.java
  1 +package com.diligrp.rider.common.enums;
  2 +
  3 +import lombok.Getter;
  4 +
  5 +/**
  6 + * 订单状态枚举
  7 + * 1=待支付 2=已支付 3=已接单 4=服务中 6=已完成
  8 + * 7=退款申请 8=退款成功 9=退款拒绝 10=已取消
  9 + */
  10 +@Getter
  11 +public enum OrderStatusEnum {
  12 + WAIT_PAY(1, "待支付"),
  13 + PAID(2, "已支付"),
  14 + ACCEPTED(3, "已接单"),
  15 + IN_SERVICE(4, "服务中"),
  16 + COMPLETED(6, "已完成"),
  17 + REFUND_APPLY(7, "退款申请"),
  18 + REFUND_SUCCESS(8, "退款成功"),
  19 + REFUND_REJECT(9, "退款拒绝"),
  20 + CANCELLED(10, "已取消");
  21 +
  22 + private final int code;
  23 + private final String desc;
  24 +
  25 + OrderStatusEnum(int code, String desc) {
  26 + this.code = code;
  27 + this.desc = desc;
  28 + }
  29 +}
... ...
src/main/java/com/diligrp/rider/common/enums/RiderStatusEnum.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/common/enums/RiderStatusEnum.java
  1 +package com.diligrp.rider.common.enums;
  2 +
  3 +import lombok.Getter;
  4 +
  5 +/**
  6 + * 骑手状态枚举
  7 + * 0=拒绝 1=通过 2=待审核
  8 + */
  9 +@Getter
  10 +public enum RiderStatusEnum {
  11 + REJECTED(0, "拒绝"),
  12 + APPROVED(1, "通过"),
  13 + PENDING(2, "待审核");
  14 +
  15 + private final int code;
  16 + private final String desc;
  17 +
  18 + RiderStatusEnum(int code, String desc) {
  19 + this.code = code;
  20 + this.desc = desc;
  21 + }
  22 +}
... ...
src/main/java/com/diligrp/rider/common/enums/TransStatusEnum.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/common/enums/TransStatusEnum.java
  1 +package com.diligrp.rider.common.enums;
  2 +
  3 +import lombok.Getter;
  4 +
  5 +/**
  6 + * 转单状态枚举
  7 + * 0=未转单 1=转单申请通过 2=转单申请中 3=转单申请拒绝
  8 + */
  9 +@Getter
  10 +public enum TransStatusEnum {
  11 + NONE(0, "未转单"),
  12 + APPROVED(1, "转单申请通过"),
  13 + PENDING(2, "转单申请中"),
  14 + REJECTED(3, "转单申请拒绝");
  15 +
  16 + private final int code;
  17 + private final String desc;
  18 +
  19 + TransStatusEnum(int code, String desc) {
  20 + this.code = code;
  21 + this.desc = desc;
  22 + }
  23 +}
... ...
src/main/java/com/diligrp/rider/common/exception/BizException.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/common/exception/BizException.java
  1 +package com.diligrp.rider.common.exception;
  2 +
  3 +import lombok.Getter;
  4 +
  5 +@Getter
  6 +public class BizException extends RuntimeException {
  7 + private final int code;
  8 +
  9 + public BizException(String message) {
  10 + super(message);
  11 + this.code = 1;
  12 + }
  13 +
  14 + public BizException(int code, String message) {
  15 + super(message);
  16 + this.code = code;
  17 + }
  18 +}
... ...
src/main/java/com/diligrp/rider/common/exception/GlobalExceptionHandler.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/common/exception/GlobalExceptionHandler.java
  1 +package com.diligrp.rider.common.exception;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.validation.BindException;
  6 +import org.springframework.web.bind.MethodArgumentNotValidException;
  7 +import org.springframework.web.bind.annotation.ExceptionHandler;
  8 +import org.springframework.web.bind.annotation.RestControllerAdvice;
  9 +
  10 +@Slf4j
  11 +@RestControllerAdvice
  12 +public class GlobalExceptionHandler {
  13 +
  14 + @ExceptionHandler(BizException.class)
  15 + public Result<Void> handleBizException(BizException e) {
  16 + return Result.error(e.getCode(), e.getMessage());
  17 + }
  18 +
  19 + @ExceptionHandler(MethodArgumentNotValidException.class)
  20 + public Result<Void> handleValidException(MethodArgumentNotValidException e) {
  21 + String msg = e.getBindingResult().getFieldErrors().stream()
  22 + .map(f -> f.getField() + ": " + f.getDefaultMessage())
  23 + .findFirst().orElse("参数错误");
  24 + return Result.error(400, msg);
  25 + }
  26 +
  27 + @ExceptionHandler(BindException.class)
  28 + public Result<Void> handleBindException(BindException e) {
  29 + String msg = e.getFieldErrors().stream()
  30 + .map(f -> f.getField() + ": " + f.getDefaultMessage())
  31 + .findFirst().orElse("参数错误");
  32 + return Result.error(400, msg);
  33 + }
  34 +
  35 + @ExceptionHandler(Exception.class)
  36 + public Result<Void> handleException(Exception e) {
  37 + log.error("系统异常", e);
  38 + return Result.error(500, "系统内部错误");
  39 + }
  40 +}
... ...
src/main/java/com/diligrp/rider/common/result/Result.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/common/result/Result.java
  1 +package com.diligrp.rider.common.result;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class Result<T> {
  7 + private int code;
  8 + private String msg;
  9 + private T data;
  10 +
  11 + private Result(int code, String msg, T data) {
  12 + this.code = code;
  13 + this.msg = msg;
  14 + this.data = data;
  15 + }
  16 +
  17 + public static <T> Result<T> success(T data) {
  18 + return new Result<>(0, "success", data);
  19 + }
  20 +
  21 + public static <T> Result<T> success() {
  22 + return new Result<>(0, "success", null);
  23 + }
  24 +
  25 + public static <T> Result<T> error(int code, String msg) {
  26 + return new Result<>(code, msg, null);
  27 + }
  28 +
  29 + public static <T> Result<T> error(String msg) {
  30 + return new Result<>(1, msg, null);
  31 + }
  32 +}
... ...
src/main/java/com/diligrp/rider/config/AuthInterceptor.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/config/AuthInterceptor.java
  1 +package com.diligrp.rider.config;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.diligrp.rider.common.exception.BizException;
  5 +import com.diligrp.rider.common.result.Result;
  6 +import com.diligrp.rider.entity.Substation;
  7 +import com.diligrp.rider.mapper.SubstationMapper;
  8 +import jakarta.servlet.http.HttpServletRequest;
  9 +import jakarta.servlet.http.HttpServletResponse;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.stereotype.Component;
  12 +import org.springframework.util.StringUtils;
  13 +import org.springframework.web.servlet.HandlerInterceptor;
  14 +
  15 +@Component
  16 +@RequiredArgsConstructor
  17 +public class AuthInterceptor implements HandlerInterceptor {
  18 +
  19 + private final JwtUtil jwtUtil;
  20 + private final ObjectMapper objectMapper;
  21 + private final SubstationMapper substationMapper;
  22 +
  23 + @Override
  24 + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  25 + String token = request.getHeader("Authorization");
  26 + if (!StringUtils.hasText(token)) {
  27 + token = request.getParameter("token");
  28 + }
  29 + if (!StringUtils.hasText(token)) {
  30 + writeError(response, 700, "请先登录");
  31 + return false;
  32 + }
  33 + if (token.startsWith("Bearer ")) {
  34 + token = token.substring(7);
  35 + }
  36 +
  37 + String path = request.getRequestURI();
  38 +
  39 + try {
  40 + io.jsonwebtoken.Claims claims = jwtUtil.getAdminClaims(token);
  41 +
  42 + if (claims.get("adminId") != null) {
  43 + // 管理员 token
  44 + Long adminId = ((Number) claims.get("adminId")).longValue();
  45 + String role = (String) claims.get("role");
  46 +
  47 + // /api/platform/** 仅超级管理员可访问
  48 + if (path.startsWith("/api/platform/") && !"admin".equals(role)) {
  49 + writeError(response, 403, "权限不足,需要超级管理员权限");
  50 + return false;
  51 + }
  52 +
  53 + request.setAttribute("adminId", adminId);
  54 + request.setAttribute("role", role);
  55 +
  56 + // 分站管理员:注入 cityId 供 Service 层做城市隔离
  57 + if ("substation".equals(role)) {
  58 + Substation sub = substationMapper.selectById(adminId);
  59 + if (sub != null) {
  60 + request.setAttribute("cityId", sub.getCityId());
  61 + }
  62 + }
  63 +
  64 + } else if (claims.get("riderId") != null) {
  65 + // 骑手 token
  66 + request.setAttribute("riderId", ((Number) claims.get("riderId")).longValue());
  67 + if (claims.get("cityId") != null) {
  68 + request.setAttribute("cityId", ((Number) claims.get("cityId")).longValue());
  69 + }
  70 + } else {
  71 + writeError(response, 700, "登录状态失效,请重新登录");
  72 + return false;
  73 + }
  74 + } catch (BizException e) {
  75 + writeError(response, e.getCode(), e.getMessage());
  76 + return false;
  77 + }
  78 + return true;
  79 + }
  80 +
  81 + private void writeError(HttpServletResponse response, int code, String msg) throws Exception {
  82 + response.setContentType("application/json;charset=UTF-8");
  83 + response.getWriter().write(objectMapper.writeValueAsString(Result.error(code, msg)));
  84 + }
  85 +}
... ...
src/main/java/com/diligrp/rider/config/CorsConfig.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/config/CorsConfig.java
  1 +package com.diligrp.rider.config;
  2 +
  3 +import org.springframework.context.annotation.Bean;
  4 +import org.springframework.context.annotation.Configuration;
  5 +import org.springframework.web.cors.CorsConfiguration;
  6 +import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
  7 +import org.springframework.web.filter.CorsFilter;
  8 +
  9 +@Configuration
  10 +public class CorsConfig {
  11 +
  12 + @Bean
  13 + public CorsFilter corsFilter() {
  14 + CorsConfiguration config = new CorsConfiguration();
  15 + // 允许的来源(开发环境放开,生产环境改为具体域名)
  16 + config.addAllowedOriginPattern("*");
  17 + config.addAllowedHeader("*");
  18 + config.addAllowedMethod("*");
  19 + config.setAllowCredentials(true);
  20 + config.setMaxAge(3600L);
  21 +
  22 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  23 + source.registerCorsConfiguration("/**", config);
  24 + return new CorsFilter(source);
  25 + }
  26 +}
... ...
src/main/java/com/diligrp/rider/config/JwtUtil.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/config/JwtUtil.java
  1 +package com.diligrp.rider.config;
  2 +
  3 +import com.diligrp.rider.common.exception.BizException;
  4 +import io.jsonwebtoken.*;
  5 +import io.jsonwebtoken.security.Keys;
  6 +import org.springframework.beans.factory.annotation.Value;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import java.nio.charset.StandardCharsets;
  10 +import java.security.Key;
  11 +import java.util.Date;
  12 +import java.util.HashMap;
  13 +import java.util.Map;
  14 +
  15 +@Component
  16 +public class JwtUtil {
  17 +
  18 + @Value("${jwt.secret}")
  19 + private String secret;
  20 +
  21 + @Value("${jwt.expire}")
  22 + private long expire;
  23 +
  24 + private Key getKey() {
  25 + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
  26 + }
  27 +
  28 + public String generateToken(Long riderId) {
  29 + Map<String, Object> claims = new HashMap<>();
  30 + claims.put("riderId", riderId);
  31 + return buildToken(claims);
  32 + }
  33 +
  34 + /** 生成骑手 token,携带 riderId + cityId */
  35 + public String generateRiderToken(Long riderId, Long cityId) {
  36 + Map<String, Object> claims = new HashMap<>();
  37 + claims.put("riderId", riderId);
  38 + claims.put("cityId", cityId);
  39 + return buildToken(claims);
  40 + }
  41 +
  42 + /** 生成管理员 token,携带 role 和 id */
  43 + public String generateAdminToken(Long adminId, String role) {
  44 + Map<String, Object> claims = new HashMap<>();
  45 + claims.put("adminId", adminId);
  46 + claims.put("role", role);
  47 + return buildToken(claims);
  48 + }
  49 +
  50 + private String buildToken(Map<String, Object> claims) {
  51 + return Jwts.builder()
  52 + .setClaims(claims)
  53 + .setIssuedAt(new Date())
  54 + .setExpiration(new Date(System.currentTimeMillis() + expire * 1000))
  55 + .signWith(getKey(), SignatureAlgorithm.HS256)
  56 + .compact();
  57 + }
  58 +
  59 + public Long getRiderIdFromToken(String token) {
  60 + try {
  61 + Claims claims = parseClaims(token);
  62 + return ((Number) claims.get("riderId")).longValue();
  63 + } catch (ExpiredJwtException e) {
  64 + throw new BizException(700, "登录状态已过期,请重新登录");
  65 + } catch (Exception e) {
  66 + throw new BizException(700, "登录状态失效,请重新登录");
  67 + }
  68 + }
  69 +
  70 + public Claims getAdminClaims(String token) {
  71 + try {
  72 + return parseClaims(token);
  73 + } catch (ExpiredJwtException e) {
  74 + throw new BizException(700, "登录状态已过期,请重新登录");
  75 + } catch (Exception e) {
  76 + throw new BizException(700, "登录状态失效,请重新登录");
  77 + }
  78 + }
  79 +
  80 + private Claims parseClaims(String token) {
  81 + return Jwts.parserBuilder()
  82 + .setSigningKey(getKey())
  83 + .build()
  84 + .parseClaimsJws(token)
  85 + .getBody();
  86 + }
  87 +}
... ...
src/main/java/com/diligrp/rider/config/OpenApiInterceptor.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/config/OpenApiInterceptor.java
  1 +package com.diligrp.rider.config;
  2 +
  3 +import com.fasterxml.jackson.databind.ObjectMapper;
  4 +import com.diligrp.rider.common.result.Result;
  5 +import com.diligrp.rider.service.OpenAppService;
  6 +import jakarta.servlet.http.HttpServletRequest;
  7 +import jakarta.servlet.http.HttpServletResponse;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.stereotype.Component;
  10 +import org.springframework.util.StringUtils;
  11 +import org.springframework.web.servlet.HandlerInterceptor;
  12 +
  13 +/**
  14 + * 开放平台签名拦截器
  15 + * 验证请求头:X-App-Key, X-Timestamp, X-Nonce, X-Sign
  16 + */
  17 +@Component
  18 +@RequiredArgsConstructor
  19 +public class OpenApiInterceptor implements HandlerInterceptor {
  20 +
  21 + private final OpenAppService openAppService;
  22 + private final ObjectMapper objectMapper;
  23 +
  24 + @Override
  25 + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  26 + String appKey = request.getHeader("X-App-Key");
  27 + String timestamp = request.getHeader("X-Timestamp");
  28 + String nonce = request.getHeader("X-Nonce");
  29 + String sign = request.getHeader("X-Sign");
  30 +
  31 + if (!StringUtils.hasText(appKey) || !StringUtils.hasText(timestamp)
  32 + || !StringUtils.hasText(nonce) || !StringUtils.hasText(sign)) {
  33 + writeError(response, 401, "缺少认证头信息(X-App-Key/X-Timestamp/X-Nonce/X-Sign)");
  34 + return false;
  35 + }
  36 +
  37 + boolean valid = openAppService.verifySign(appKey, timestamp, nonce, sign);
  38 + if (!valid) {
  39 + writeError(response, 401, "签名验证失败或已过期");
  40 + return false;
  41 + }
  42 + return true;
  43 + }
  44 +
  45 + private void writeError(HttpServletResponse response, int code, String msg) throws Exception {
  46 + response.setContentType("application/json;charset=UTF-8");
  47 + response.setStatus(200);
  48 + response.getWriter().write(objectMapper.writeValueAsString(Result.error(code, msg)));
  49 + }
  50 +}
... ...
src/main/java/com/diligrp/rider/config/WebMvcConfig.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/config/WebMvcConfig.java
  1 +package com.diligrp.rider.config;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import org.springframework.context.annotation.Configuration;
  5 +import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  6 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  7 +
  8 +@Configuration
  9 +@RequiredArgsConstructor
  10 +public class WebMvcConfig implements WebMvcConfigurer {
  11 +
  12 + private final AuthInterceptor authInterceptor;
  13 + private final OpenApiInterceptor openApiInterceptor;
  14 +
  15 + @Override
  16 + public void addInterceptors(InterceptorRegistry registry) {
  17 + // JWT 鉴权:骑手端、管理端、平台端
  18 + registry.addInterceptor(authInterceptor)
  19 + .addPathPatterns("/api/rider/**", "/api/admin/**", "/api/platform/**")
  20 + .excludePathPatterns(
  21 + "/api/rider/login/**",
  22 + "/api/rider/apply",
  23 + "/api/merchant/enter",
  24 + "/api/admin/auth/login" // 管理员登录无需鉴权
  25 + );
  26 +
  27 + // 开放平台签名鉴权:/api/open/** 需要 AppKey+签名
  28 + registry.addInterceptor(openApiInterceptor)
  29 + .addPathPatterns("/api/open/**");
  30 +
  31 + // /api/delivery/fee/** 对内中台接口,内网调用,不做鉴权
  32 + }
  33 +}
... ...
src/main/java/com/diligrp/rider/controller/AdminAuthController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/AdminAuthController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.AdminLoginDTO;
  5 +import com.diligrp.rider.service.impl.AdminAuthServiceImpl;
  6 +import com.diligrp.rider.vo.AdminLoginVO;
  7 +import jakarta.validation.Valid;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.*;
  10 +
  11 +@RestController
  12 +@RequestMapping("/api/admin/auth")
  13 +@RequiredArgsConstructor
  14 +public class AdminAuthController {
  15 +
  16 + private final AdminAuthServiceImpl adminAuthService;
  17 +
  18 + /**
  19 + * 管理员登录(超级管理员 + 分站管理员统一入口)
  20 + * 请求体:{ "account": "gz_admin", "pass": "admin123", "role": "substation" }
  21 + * role 可选值:admin(超级管理员)| substation(分站管理员,默认)
  22 + */
  23 + @PostMapping("/login")
  24 + public Result<AdminLoginVO> login(@Valid @RequestBody AdminLoginDTO dto) {
  25 + return Result.success(adminAuthService.login(dto));
  26 + }
  27 +}
... ...
src/main/java/com/diligrp/rider/controller/AdminRefundController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/AdminRefundController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.entity.OrderRefundReason;
  5 +import com.diligrp.rider.entity.OrderRefundRecord;
  6 +import com.diligrp.rider.service.RefundService;
  7 +import com.diligrp.rider.service.RiderEvaluateService;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.*;
  10 +
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * 平台/分站管理:退款审核 + 评价管理
  15 + */
  16 +@RestController
  17 +@RequestMapping("/api/admin")
  18 +@RequiredArgsConstructor
  19 +public class AdminRefundController {
  20 +
  21 + private final RefundService refundService;
  22 + private final RiderEvaluateService evaluateService;
  23 +
  24 + /** 退款原因列表管理(全部角色) */
  25 + @GetMapping("/refund/reasons")
  26 + public Result<List<OrderRefundReason>> reasons(@RequestParam(defaultValue = "0") int role) {
  27 + return Result.success(refundService.getReasons(role));
  28 + }
  29 +
  30 + /** 查看订单退款记录 */
  31 + @GetMapping("/refund/record")
  32 + public Result<OrderRefundRecord> record(@RequestParam Long orderId) {
  33 + return Result.success(refundService.getByOrderId(orderId));
  34 + }
  35 +
  36 + /**
  37 + * 审核退款申请
  38 + * status=1 通过(退款成功)
  39 + * status=2 拒绝
  40 + */
  41 + @PostMapping("/refund/handle")
  42 + public Result<Void> handle(
  43 + @RequestParam Long recordId,
  44 + @RequestParam int status,
  45 + @RequestParam(required = false, defaultValue = "") String remark) {
  46 + refundService.handleRefund(recordId, status, remark);
  47 + return Result.success();
  48 + }
  49 +
  50 + /** 骑手评价列表(运营查看) */
  51 + @GetMapping("/evaluate/list")
  52 + public Result<List<?>> evaluateList(
  53 + @RequestParam Long riderId,
  54 + @RequestParam(defaultValue = "0") int type,
  55 + @RequestParam(defaultValue = "1") int page) {
  56 + return Result.success(evaluateService.getRiderEvaluates(riderId, type, page));
  57 + }
  58 +}
... ...
src/main/java/com/diligrp/rider/controller/AdminRiderController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/AdminRiderController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.result.Result;
  5 +import com.diligrp.rider.dto.AdminRiderAddDTO;
  6 +import com.diligrp.rider.entity.Orders;
  7 +import com.diligrp.rider.entity.Rider;
  8 +import com.diligrp.rider.mapper.OrdersMapper;
  9 +import com.diligrp.rider.service.AdminRiderService;
  10 +import jakarta.servlet.http.HttpServletRequest;
  11 +import jakarta.validation.Valid;
  12 +import lombok.RequiredArgsConstructor;
  13 +import org.springframework.web.bind.annotation.*;
  14 +
  15 +import java.util.List;
  16 +
  17 +@RestController
  18 +@RequestMapping("/api/admin/rider")
  19 +@RequiredArgsConstructor
  20 +public class AdminRiderController {
  21 +
  22 + private final AdminRiderService adminRiderService;
  23 + private final OrdersMapper ordersMapper;
  24 +
  25 + /** 手动新增骑手(分站管理员自动绑定本城市) */
  26 + @PostMapping("/add")
  27 + public Result<Void> add(@Valid @RequestBody AdminRiderAddDTO dto, HttpServletRequest request) {
  28 + Long cityId = "substation".equals(request.getAttribute("role"))
  29 + ? (Long) request.getAttribute("cityId")
  30 + : dto.getCityId();
  31 + adminRiderService.add(dto, cityId);
  32 + return Result.success();
  33 + }
  34 +
  35 + /** 骑手列表(分站管理员仅看本城市,超管看全部) */
  36 + @GetMapping("/list")
  37 + public Result<List<Rider>> list(
  38 + @RequestParam(required = false) String keyword,
  39 + @RequestParam(required = false) Integer userStatus,
  40 + HttpServletRequest request) {
  41 + Long cityId = "substation".equals(request.getAttribute("role"))
  42 + ? (Long) request.getAttribute("cityId")
  43 + : null;
  44 + return Result.success(adminRiderService.list(keyword, userStatus, cityId));
  45 + }
  46 +
  47 + /** 指派骑手候选列表 */
  48 + @GetMapping("/order/candidates")
  49 + public Result<List<Rider>> designateCandidates(@RequestParam Long orderId, HttpServletRequest request) {
  50 + Long cityId = "substation".equals(request.getAttribute("role"))
  51 + ? (Long) request.getAttribute("cityId")
  52 + : null;
  53 + return Result.success(adminRiderService.designateCandidates(orderId, cityId));
  54 + }
  55 +
  56 + /** 审核骑手:status=0拒绝 1通过 */
  57 + @PostMapping("/setStatus")
  58 + public Result<Void> setStatus(@RequestParam Long riderId, @RequestParam int status) {
  59 + adminRiderService.setStatus(riderId, status);
  60 + return Result.success();
  61 + }
  62 +
  63 + /** 设置骑手等级,levelId 为空时使用默认等级 */
  64 + @PostMapping("/setLevel")
  65 + public Result<Void> setLevel(@RequestParam Long riderId,
  66 + @RequestParam(required = false) Long levelId,
  67 + HttpServletRequest request) {
  68 + Long cityId = "substation".equals(request.getAttribute("role"))
  69 + ? (Long) request.getAttribute("cityId")
  70 + : null;
  71 + adminRiderService.setLevel(riderId, levelId, cityId);
  72 + return Result.success();
  73 + }
  74 +
  75 + /** 启用/禁用骑手账号:status=0禁用 1启用 */
  76 + @PostMapping("/setEnableStatus")
  77 + public Result<Void> setEnableStatus(@RequestParam Long riderId, @RequestParam int status) {
  78 + adminRiderService.setEnableStatus(riderId, status);
  79 + return Result.success();
  80 + }
  81 +
  82 + /** 切换骑手类型:type=1兼职 2全职 */
  83 + @PostMapping("/setType")
  84 + public Result<Void> setType(@RequestParam Long riderId, @RequestParam int type) {
  85 + adminRiderService.setType(riderId, type);
  86 + return Result.success();
  87 + }
  88 +
  89 + /** 指派骑手接单 */
  90 + @PostMapping("/order/designate")
  91 + public Result<Void> designate(@RequestParam Long orderId, @RequestParam Long riderId) {
  92 + adminRiderService.designate(orderId, riderId);
  93 + return Result.success();
  94 + }
  95 +
  96 + /** 处理转单申请:trans=1通过 3拒绝 */
  97 + @PostMapping("/order/setTrans")
  98 + public Result<Void> setTrans(@RequestParam Long orderId, @RequestParam int trans) {
  99 + adminRiderService.setTrans(orderId, trans);
  100 + return Result.success();
  101 + }
  102 +
  103 + /**
  104 + * 订单列表(分站管理员查看本城市订单 / 超管查看所有)
  105 + * 分站管理员:cityId 从 token 自动注入,无需传参
  106 + * 超管:可传 cityId 筛选某城市
  107 + */
  108 + @GetMapping("/order/list")
  109 + public Result<List<Orders>> orderList(
  110 + @RequestParam(required = false) Integer status,
  111 + @RequestParam(required = false) Integer isTrans,
  112 + @RequestParam(required = false) String keyword,
  113 + @RequestParam(defaultValue = "1") int page,
  114 + HttpServletRequest request) {
  115 + Long cityId = (Long) request.getAttribute("cityId");
  116 + String role = (String) request.getAttribute("role");
  117 +
  118 + LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<Orders>()
  119 + .eq(Orders::getIsDel, 0)
  120 + .orderByDesc(Orders::getId);
  121 +
  122 + if ("substation".equals(role) && cityId != null) {
  123 + wrapper.eq(Orders::getCityId, cityId);
  124 + }
  125 + if (status != null) wrapper.eq(Orders::getStatus, status);
  126 + if (isTrans != null) wrapper.eq(Orders::getIsTrans, isTrans);
  127 + if (keyword != null && !keyword.isBlank()) {
  128 + wrapper.and(w -> w.like(Orders::getOrderNo, keyword)
  129 + .or().like(Orders::getOutOrderNo, keyword)
  130 + .or().like(Orders::getRecipName, keyword)
  131 + .or().like(Orders::getRecipPhone, keyword));
  132 + }
  133 + int offset = (page - 1) * 20;
  134 + wrapper.last("LIMIT " + offset + ",20");
  135 +
  136 + return Result.success(ordersMapper.selectList(wrapper));
  137 + }
  138 +
  139 + /**
  140 + * 配送订单列表(平台管理员查看外部系统推入的订单)
  141 + * 按 appKey / outOrderNo 查询
  142 + */
  143 + @GetMapping("/order/delivery/list")
  144 + public Result<List<Orders>> deliveryOrderList(
  145 + @RequestParam(required = false) String appKey,
  146 + @RequestParam(required = false) String outOrderNo,
  147 + @RequestParam(required = false) Integer status,
  148 + @RequestParam(defaultValue = "1") int page) {
  149 + LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<Orders>()
  150 + .eq(Orders::getIsDel, 0)
  151 + .orderByDesc(Orders::getId);
  152 + if (appKey != null && !appKey.isBlank()) wrapper.eq(Orders::getAppKey, appKey);
  153 + if (outOrderNo != null && !outOrderNo.isBlank()) wrapper.like(Orders::getOutOrderNo, outOrderNo);
  154 + if (status != null) wrapper.eq(Orders::getStatus, status);
  155 + int offset = (page - 1) * 20;
  156 + wrapper.last("LIMIT " + offset + ",20");
  157 + return Result.success(ordersMapper.selectList(wrapper));
  158 + }
  159 +}
... ...
src/main/java/com/diligrp/rider/controller/AdminRiderLevelController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/AdminRiderLevelController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.exception.BizException;
  4 +import com.diligrp.rider.common.result.Result;
  5 +import com.diligrp.rider.dto.AdminRiderLevelSaveDTO;
  6 +import com.diligrp.rider.entity.RiderLevel;
  7 +import com.diligrp.rider.service.AdminRiderLevelService;
  8 +import jakarta.servlet.http.HttpServletRequest;
  9 +import jakarta.validation.Valid;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.web.bind.annotation.*;
  12 +
  13 +import java.util.List;
  14 +
  15 +@RestController
  16 +@RequestMapping("/api/admin/rider/level")
  17 +@RequiredArgsConstructor
  18 +public class AdminRiderLevelController {
  19 +
  20 + private final AdminRiderLevelService adminRiderLevelService;
  21 +
  22 + @GetMapping("/list")
  23 + public Result<List<RiderLevel>> list(@RequestParam(required = false) Long cityId, HttpServletRequest request) {
  24 + return Result.success(adminRiderLevelService.list(resolveCityId(cityId, request)));
  25 + }
  26 +
  27 + @PostMapping("/add")
  28 + public Result<Void> add(@Valid @RequestBody AdminRiderLevelSaveDTO dto, HttpServletRequest request) {
  29 + adminRiderLevelService.add(dto, resolveCityId(dto.getCityId(), request));
  30 + return Result.success();
  31 + }
  32 +
  33 + @PutMapping("/edit")
  34 + public Result<Void> edit(@Valid @RequestBody AdminRiderLevelSaveDTO dto, HttpServletRequest request) {
  35 + adminRiderLevelService.edit(dto, resolveCityId(dto.getCityId(), request));
  36 + return Result.success();
  37 + }
  38 +
  39 + @PostMapping("/setDefault")
  40 + public Result<Void> setDefault(@RequestParam Long id,
  41 + @RequestParam(required = false) Long cityId,
  42 + HttpServletRequest request) {
  43 + adminRiderLevelService.setDefault(id, resolveCityId(cityId, request));
  44 + return Result.success();
  45 + }
  46 +
  47 + @DeleteMapping("/del")
  48 + public Result<Void> delete(@RequestParam Long id,
  49 + @RequestParam(required = false) Long cityId,
  50 + HttpServletRequest request) {
  51 + adminRiderLevelService.delete(id, resolveCityId(cityId, request));
  52 + return Result.success();
  53 + }
  54 +
  55 + private Long resolveCityId(Long cityId, HttpServletRequest request) {
  56 + if ("substation".equals(request.getAttribute("role"))) {
  57 + Long requestCityId = (Long) request.getAttribute("cityId");
  58 + if (requestCityId == null || requestCityId < 1) {
  59 + throw new BizException("当前账号未绑定城市");
  60 + }
  61 + return requestCityId;
  62 + }
  63 + if (cityId == null || cityId < 1) {
  64 + throw new BizException("城市不能为空");
  65 + }
  66 + return cityId;
  67 + }
  68 +}
... ...
src/main/java/com/diligrp/rider/controller/DeliveryFeeController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/DeliveryFeeController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
  5 +import com.diligrp.rider.service.DeliveryFeeService;
  6 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  7 +import jakarta.validation.Valid;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.*;
  10 +
  11 +/**
  12 + * 配送费计算接口(对内中台核心接口)
  13 + * 电商系统下单前调用此接口获取配送费
  14 + */
  15 +@RestController
  16 +@RequestMapping("/api/delivery/fee")
  17 +@RequiredArgsConstructor
  18 +public class DeliveryFeeController {
  19 +
  20 + private final DeliveryFeeService deliveryFeeService;
  21 +
  22 + /**
  23 + * 计算配送费
  24 + * 入参:cityId, orderType, startLng, startLat, endLng, endLat, weight, serviceTime
  25 + * 出参:各项费用明细 + 总费用 + 预计送达时间
  26 + */
  27 + @PostMapping("/calc")
  28 + public Result<DeliveryFeeResultVO> calc(@Valid @RequestBody DeliveryFeeCalcDTO dto) {
  29 + return Result.success(deliveryFeeService.calcFee(dto));
  30 + }
  31 +
  32 + /**
  33 + * 检查城市是否开通某服务
  34 + * orderType: 1=帮我送 2=帮我取 6=外卖配送
  35 + */
  36 + @GetMapping("/check")
  37 + public Result<Boolean> check(@RequestParam Long cityId, @RequestParam int orderType) {
  38 + return Result.success(deliveryFeeService.isServiceEnabled(cityId, orderType));
  39 + }
  40 +}
... ...
src/main/java/com/diligrp/rider/controller/OpenApiController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/OpenApiController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.exception.BizException;
  4 +import com.diligrp.rider.common.result.Result;
  5 +import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
  6 +import com.diligrp.rider.dto.OpenDeliveryFeeCalcDTO;
  7 +import com.diligrp.rider.entity.OpenApp;
  8 +import com.diligrp.rider.service.DeliveryFeeService;
  9 +import com.diligrp.rider.service.OpenAppService;
  10 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  11 +import jakarta.servlet.http.HttpServletRequest;
  12 +import jakarta.validation.Valid;
  13 +import lombok.RequiredArgsConstructor;
  14 +import org.springframework.web.bind.annotation.*;
  15 +
  16 +/**
  17 + * 开放平台对外接口
  18 + * 路径前缀 /api/open/** 受 OpenApiInterceptor 签名鉴权保护
  19 + * 第三方系统需携带 X-App-Key / X-Timestamp / X-Nonce / X-Sign 请求头
  20 + */
  21 +@RestController
  22 +@RequestMapping("/api/open")
  23 +@RequiredArgsConstructor
  24 +public class OpenApiController {
  25 +
  26 + private final DeliveryFeeService deliveryFeeService;
  27 + private final OpenAppService openAppService;
  28 +
  29 + /**
  30 + * 计算配送费(对外开放版)
  31 + * cityId 由 AppKey 绑定的租户自动确定,不信任外部传参
  32 + */
  33 + @PostMapping("/delivery/fee/calc")
  34 + public Result<DeliveryFeeResultVO> calcFee(@Valid @RequestBody OpenDeliveryFeeCalcDTO dto,
  35 + HttpServletRequest request) {
  36 + DeliveryFeeCalcDTO calcDTO = new DeliveryFeeCalcDTO();
  37 + calcDTO.setCityId(resolveCityId(request));
  38 + calcDTO.setOrderType(dto.getOrderType());
  39 + calcDTO.setStartLng(dto.getStartLng());
  40 + calcDTO.setStartLat(dto.getStartLat());
  41 + calcDTO.setEndLng(dto.getEndLng());
  42 + calcDTO.setEndLat(dto.getEndLat());
  43 + calcDTO.setWeight(dto.getWeight());
  44 + calcDTO.setPieces(dto.getPieces());
  45 + calcDTO.setServiceTime(dto.getServiceTime());
  46 + return Result.success(deliveryFeeService.calcFee(calcDTO));
  47 + }
  48 +
  49 + /**
  50 + * 检查城市服务是否开通
  51 + * cityId 由 AppKey 绑定的租户自动确定
  52 + */
  53 + @GetMapping("/delivery/check")
  54 + public Result<Boolean> check(@RequestParam int orderType, HttpServletRequest request) {
  55 + return Result.success(deliveryFeeService.isServiceEnabled(resolveCityId(request), orderType));
  56 + }
  57 +
  58 + private Long resolveCityId(HttpServletRequest request) {
  59 + String appKey = request.getHeader("X-App-Key");
  60 + OpenApp app = openAppService.getByAppKey(appKey);
  61 + if (app == null || app.getStatus() != 1) {
  62 + throw new BizException("应用不存在或已禁用");
  63 + }
  64 + if (app.getCityId() == null || app.getCityId() < 1) {
  65 + throw new BizException("该应用未绑定租户,请联系平台管理员");
  66 + }
  67 + return app.getCityId();
  68 + }
  69 +}
... ...
src/main/java/com/diligrp/rider/controller/OpenDeliveryOrderController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/OpenDeliveryOrderController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.DeliveryOrderCreateDTO;
  5 +import com.diligrp.rider.service.DeliveryOrderService;
  6 +import com.diligrp.rider.vo.DeliveryOrderCreateVO;
  7 +import jakarta.servlet.http.HttpServletRequest;
  8 +import jakarta.validation.Valid;
  9 +import lombok.RequiredArgsConstructor;
  10 +import org.springframework.web.bind.annotation.*;
  11 +
  12 +/**
  13 + * 开放平台:配送订单接口
  14 + * 路径 /api/open/** 由 OpenApiInterceptor 做签名鉴权
  15 + * X-App-Key 会由拦截器写入 request attribute
  16 + */
  17 +@RestController
  18 +@RequestMapping("/api/open/delivery/order")
  19 +@RequiredArgsConstructor
  20 +public class OpenDeliveryOrderController {
  21 +
  22 + private final DeliveryOrderService deliveryOrderService;
  23 +
  24 + /**
  25 + * 创建配送订单(推单)
  26 + * 外部系统在自己完成支付后调用此接口,中台接管配送
  27 + */
  28 + @PostMapping("/create")
  29 + public Result<DeliveryOrderCreateVO> create(
  30 + @Valid @RequestBody DeliveryOrderCreateDTO dto,
  31 + HttpServletRequest request) {
  32 + String appKey = request.getHeader("X-App-Key");
  33 + return Result.success(deliveryOrderService.create(appKey, dto));
  34 + }
  35 +
  36 + /**
  37 + * 查询配送订单状态
  38 + * 接入方可轮询此接口跟踪配送进度
  39 + */
  40 + @GetMapping("/query")
  41 + public Result<DeliveryOrderCreateVO> query(
  42 + @RequestParam String outOrderNo,
  43 + HttpServletRequest request) {
  44 + String appKey = request.getHeader("X-App-Key");
  45 + return Result.success(deliveryOrderService.queryByOutOrderNo(appKey, outOrderNo));
  46 + }
  47 +
  48 + /**
  49 + * 取消配送订单
  50 + * 仅 status=2(待接单)可取消
  51 + */
  52 + @PostMapping("/cancel")
  53 + public Result<Void> cancel(
  54 + @RequestParam String outOrderNo,
  55 + HttpServletRequest request) {
  56 + String appKey = request.getHeader("X-App-Key");
  57 + deliveryOrderService.cancel(appKey, outOrderNo);
  58 + return Result.success();
  59 + }
  60 +}
... ...
src/main/java/com/diligrp/rider/controller/OpenStoreController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/OpenStoreController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.MerchantStoreDTO;
  5 +import com.diligrp.rider.entity.MerchantStore;
  6 +import com.diligrp.rider.service.MerchantService;
  7 +import jakarta.servlet.http.HttpServletRequest;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.*;
  10 +
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * 开放平台:门店同步接口
  15 + * 接入方用 AppKey 签名后调用,同步自己系统的门店数据到配送中台
  16 + */
  17 +@RestController
  18 +@RequestMapping("/api/open/store")
  19 +@RequiredArgsConstructor
  20 +public class OpenStoreController {
  21 +
  22 + private final MerchantService merchantService;
  23 +
  24 + /**
  25 + * 同步门店(新增或更新)
  26 + * 以 appKey + outStoreId 为唯一键,存在则更新,不存在则新增
  27 + * dto.outStoreId 必填
  28 + */
  29 + @PostMapping("/sync")
  30 + public Result<MerchantStore> sync(
  31 + @RequestBody MerchantStoreDTO dto,
  32 + HttpServletRequest request) {
  33 + String appKey = request.getHeader("X-App-Key");
  34 + return Result.success(merchantService.syncStore(appKey, dto));
  35 + }
  36 +
  37 + /**
  38 + * 查询本应用下的门店列表
  39 + */
  40 + @GetMapping("/list")
  41 + public Result<List<MerchantStore>> list(
  42 + HttpServletRequest request,
  43 + @RequestParam(required = false) Long cityId,
  44 + @RequestParam(defaultValue = "1") int page) {
  45 + String appKey = request.getHeader("X-App-Key");
  46 + return Result.success(merchantService.storeList(cityId, null, page));
  47 + }
  48 +
  49 + /**
  50 + * 根据外部门店编号查询
  51 + */
  52 + @GetMapping("/getByOutStoreId")
  53 + public Result<MerchantStore> getByOutStoreId(
  54 + @RequestParam String outStoreId,
  55 + HttpServletRequest request) {
  56 + String appKey = request.getHeader("X-App-Key");
  57 + return Result.success(merchantService.getByOutStoreId(appKey, outStoreId));
  58 + }
  59 +}
... ...
src/main/java/com/diligrp/rider/controller/PlatformCityController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/PlatformCityController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.entity.City;
  5 +import com.diligrp.rider.service.CityService;
  6 +import com.diligrp.rider.vo.CityVO;
  7 +import lombok.RequiredArgsConstructor;
  8 +import org.springframework.web.bind.annotation.*;
  9 +
  10 +import java.util.List;
  11 +
  12 +@RestController
  13 +@RequestMapping("/api/platform/city")
  14 +@RequiredArgsConstructor
  15 +public class PlatformCityController {
  16 +
  17 + private final CityService cityService;
  18 +
  19 + /** 获取两级城市树(省→市) */
  20 + @GetMapping("/tree")
  21 + public Result<List<CityVO>> tree() {
  22 + return Result.success(cityService.getTree());
  23 + }
  24 +
  25 + /** 已开通城市列表 */
  26 + @GetMapping("/open")
  27 + public Result<List<CityVO>> openList() {
  28 + return Result.success(cityService.getOpenList());
  29 + }
  30 +
  31 + /** 新增城市 */
  32 + @PostMapping("/add")
  33 + public Result<Void> add(@RequestBody City city) {
  34 + cityService.add(city);
  35 + return Result.success();
  36 + }
  37 +
  38 + /** 编辑城市基础信息 */
  39 + @PutMapping("/edit")
  40 + public Result<Void> edit(@RequestBody City city) {
  41 + cityService.edit(city);
  42 + return Result.success();
  43 + }
  44 +
  45 + /** 开通/关闭城市:status=0关闭 1开通 */
  46 + @PostMapping("/setStatus")
  47 + public Result<Void> setStatus(@RequestParam Long cityId, @RequestParam int status) {
  48 + cityService.setStatus(cityId, status);
  49 + return Result.success();
  50 + }
  51 +}
... ...
src/main/java/com/diligrp/rider/controller/PlatformCityFeePlanController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/PlatformCityFeePlanController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.DeliveryFeePlanPreviewDTO;
  5 +import com.diligrp.rider.dto.DeliveryFeePlanSaveDTO;
  6 +import com.diligrp.rider.service.DeliveryFeePlanService;
  7 +import com.diligrp.rider.vo.DeliveryFeePlanDetailVO;
  8 +import com.diligrp.rider.vo.DeliveryFeePlanVO;
  9 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.web.bind.annotation.*;
  12 +
  13 +import java.util.List;
  14 +
  15 +@RestController
  16 +@RequestMapping("/api/platform/city")
  17 +@RequiredArgsConstructor
  18 +public class PlatformCityFeePlanController {
  19 +
  20 + private final DeliveryFeePlanService deliveryFeePlanService;
  21 +
  22 + @GetMapping("/{cityId}/fee-plans")
  23 + public Result<List<DeliveryFeePlanVO>> listPlans(@PathVariable Long cityId) {
  24 + return Result.success(deliveryFeePlanService.listPlans(cityId));
  25 + }
  26 +
  27 + @GetMapping("/{cityId}/fee-plans/{planId}")
  28 + public Result<DeliveryFeePlanDetailVO> getPlanDetail(@PathVariable Long cityId, @PathVariable Long planId) {
  29 + return Result.success(deliveryFeePlanService.getPlanDetail(cityId, planId));
  30 + }
  31 +
  32 + @PostMapping("/{cityId}/fee-plans")
  33 + public Result<Long> createPlan(@PathVariable Long cityId, @RequestBody DeliveryFeePlanSaveDTO dto) {
  34 + return Result.success(deliveryFeePlanService.createPlan(cityId, dto));
  35 + }
  36 +
  37 + @PostMapping("/{cityId}/fee-plans/init-default")
  38 + public Result<Long> initializeDefaultPlan(@PathVariable Long cityId) {
  39 + return Result.success(deliveryFeePlanService.initializeDefaultPlan(cityId));
  40 + }
  41 +
  42 + @PutMapping("/{cityId}/fee-plans/{planId}")
  43 + public Result<Void> updatePlan(@PathVariable Long cityId,
  44 + @PathVariable Long planId,
  45 + @RequestBody DeliveryFeePlanSaveDTO dto) {
  46 + deliveryFeePlanService.updatePlan(cityId, planId, dto);
  47 + return Result.success();
  48 + }
  49 +
  50 + @PostMapping("/{cityId}/fee-plans/{planId}/copy")
  51 + public Result<Long> copyPlan(@PathVariable Long cityId, @PathVariable Long planId) {
  52 + return Result.success(deliveryFeePlanService.copyPlan(cityId, planId));
  53 + }
  54 +
  55 + @PostMapping("/{cityId}/fee-plans/{planId}/default")
  56 + public Result<Void> setDefaultPlan(@PathVariable Long cityId, @PathVariable Long planId) {
  57 + deliveryFeePlanService.setDefaultPlan(cityId, planId);
  58 + return Result.success();
  59 + }
  60 +
  61 + @DeleteMapping("/{cityId}/fee-plans/{planId}")
  62 + public Result<Void> deletePlan(@PathVariable Long cityId, @PathVariable Long planId) {
  63 + deliveryFeePlanService.deletePlan(cityId, planId);
  64 + return Result.success();
  65 + }
  66 +
  67 + @PostMapping("/{cityId}/fee-plans/preview")
  68 + public Result<DeliveryFeeResultVO> preview(@PathVariable Long cityId,
  69 + @RequestBody DeliveryFeePlanPreviewDTO dto) {
  70 + return Result.success(deliveryFeePlanService.preview(cityId, dto));
  71 + }
  72 +}
... ...
src/main/java/com/diligrp/rider/controller/PlatformMerchantController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/PlatformMerchantController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.MerchantStoreDTO;
  5 +import com.diligrp.rider.entity.MerchantStore;
  6 +import com.diligrp.rider.service.MerchantService;
  7 +import jakarta.validation.Valid;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.*;
  10 +
  11 +import java.math.BigDecimal;
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * 平台后台:门店管理
  16 + * - 外部门店通过开放平台 /api/open/store/sync 同步,统一存入 merchant_store 表
  17 + * - 平台管理员也可直接新建门店,选填 outStoreId 与外部系统对账
  18 + */
  19 +@RestController
  20 +@RequestMapping("/api/platform/merchant")
  21 +@RequiredArgsConstructor
  22 +public class PlatformMerchantController {
  23 +
  24 + private final MerchantService merchantService;
  25 +
  26 + /** 门店列表(含外部同步的和平台自建的) */
  27 + @GetMapping("/store/list")
  28 + public Result<List<MerchantStore>> storeList(
  29 + @RequestParam(required = false) Long cityId,
  30 + @RequestParam(required = false) String keyword,
  31 + @RequestParam(defaultValue = "1") int page) {
  32 + return Result.success(merchantService.storeList(cityId, keyword, page));
  33 + }
  34 +
  35 + /** 新增门店(平台直接建,无需审核) */
  36 + @PostMapping("/store/add")
  37 + public Result<Long> addStore(@Valid @RequestBody MerchantStoreDTO dto) {
  38 + return Result.success(merchantService.addStore(dto));
  39 + }
  40 +
  41 + /** 编辑门店 */
  42 + @PutMapping("/store/edit")
  43 + public Result<Void> editStore(@Valid @RequestBody MerchantStoreDTO dto) {
  44 + merchantService.editStore(dto);
  45 + return Result.success();
  46 + }
  47 +
  48 + /** 门店详情 */
  49 + @GetMapping("/store/detail")
  50 + public Result<MerchantStore> storeDetail(@RequestParam Long storeId) {
  51 + return Result.success(merchantService.getStore(storeId));
  52 + }
  53 +
  54 + /** 设置营业/打烊 */
  55 + @PostMapping("/store/setOperatingState")
  56 + public Result<Void> setOperatingState(@RequestParam Long storeId, @RequestParam int state) {
  57 + merchantService.setOperatingState(storeId, state);
  58 + return Result.success();
  59 + }
  60 +
  61 + /** 更新免运费和起送金额 */
  62 + @PostMapping("/store/updateFeeConfig")
  63 + public Result<Void> updateFeeConfig(
  64 + @RequestParam Long storeId,
  65 + @RequestParam(defaultValue = "0") BigDecimal freeShipping,
  66 + @RequestParam(defaultValue = "0") BigDecimal upToSend) {
  67 + merchantService.updateFeeConfig(storeId, freeShipping, upToSend);
  68 + return Result.success();
  69 + }
  70 +
  71 + /** 删除门店 */
  72 + @DeleteMapping("/store/del")
  73 + public Result<Void> delStore(@RequestParam Long storeId) {
  74 + merchantService.delStore(storeId);
  75 + return Result.success();
  76 + }
  77 +}
... ...
src/main/java/com/diligrp/rider/controller/PlatformOpenAppController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/PlatformOpenAppController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.result.Result;
  5 +import com.diligrp.rider.dto.PlatformMockDeliveryCreateDTO;
  6 +import com.diligrp.rider.entity.OpenApp;
  7 +import com.diligrp.rider.entity.WebhookLog;
  8 +import com.diligrp.rider.mapper.OpenAppMapper;
  9 +import com.diligrp.rider.mapper.WebhookLogMapper;
  10 +import com.diligrp.rider.service.DeliveryOrderService;
  11 +import com.diligrp.rider.service.OpenAppService;
  12 +import com.diligrp.rider.service.WebhookService;
  13 +import com.diligrp.rider.vo.DeliveryOrderCreateVO;
  14 +import jakarta.validation.Valid;
  15 +import lombok.RequiredArgsConstructor;
  16 +import org.springframework.web.bind.annotation.*;
  17 +
  18 +import java.util.List;
  19 +
  20 +/**
  21 + * 平台后台:开放平台应用管理
  22 + */
  23 +@RestController
  24 +@RequestMapping("/api/platform/open")
  25 +@RequiredArgsConstructor
  26 +public class PlatformOpenAppController {
  27 +
  28 + private final OpenAppService openAppService;
  29 + private final WebhookService webhookService;
  30 + private final WebhookLogMapper webhookLogMapper;
  31 + private final OpenAppMapper openAppMapper;
  32 + private final DeliveryOrderService deliveryOrderService;
  33 +
  34 + /** 应用列表 */
  35 + @GetMapping("/list")
  36 + public Result<List<OpenApp>> list(@RequestParam(defaultValue = "1") int page) {
  37 + return Result.success(openAppService.list(page));
  38 + }
  39 +
  40 + /** 创建应用 */
  41 + @PostMapping("/create")
  42 + public Result<OpenApp> create(@RequestParam String appName,
  43 + @RequestParam Long cityId,
  44 + @RequestParam(required = false) Long storeId,
  45 + @RequestParam(required = false) String webhookUrl,
  46 + @RequestParam(required = false) String webhookEvents,
  47 + @RequestParam(required = false) String remark) {
  48 + return Result.success(openAppService.create(appName, cityId, storeId, webhookUrl, webhookEvents, remark));
  49 + }
  50 +
  51 + /** 平台后台模拟推送配送单 */
  52 + @PostMapping("/mockDelivery/create")
  53 + public Result<DeliveryOrderCreateVO> mockDeliveryCreate(@Valid @RequestBody PlatformMockDeliveryCreateDTO dto) {
  54 + OpenApp app = openAppMapper.selectById(dto.getAppId());
  55 + if (app == null) {
  56 + return Result.error("应用不存在");
  57 + }
  58 + return Result.success(deliveryOrderService.create(app.getAppKey(), dto));
  59 + }
  60 +
  61 + /** 重置 AppSecret */
  62 + @PostMapping("/resetSecret")
  63 + public Result<String> resetSecret(@RequestParam Long appId) {
  64 + return Result.success(openAppService.resetSecret(appId));
  65 + }
  66 +
  67 + /** 启用/禁用应用:status=0禁用 1启用 */
  68 + @PostMapping("/setStatus")
  69 + public Result<Void> setStatus(@RequestParam Long appId, @RequestParam int status) {
  70 + openAppService.setStatus(appId, status);
  71 + return Result.success();
  72 + }
  73 +
  74 + /** 更新 Webhook 配置 */
  75 + @PostMapping("/updateWebhook")
  76 + public Result<Void> updateWebhook(@RequestParam Long appId,
  77 + @RequestParam String webhookUrl,
  78 + @RequestParam String webhookEvents) {
  79 + openAppService.updateWebhook(appId, webhookUrl, webhookEvents);
  80 + return Result.success();
  81 + }
  82 +
  83 + /** Webhook 推送日志 */
  84 + @GetMapping("/webhook/logs")
  85 + public Result<List<WebhookLog>> webhookLogs(@RequestParam Long appId,
  86 + @RequestParam(defaultValue = "1") int page) {
  87 + int offset = (page - 1) * 20;
  88 + List<WebhookLog> logs = webhookLogMapper.selectList(
  89 + new LambdaQueryWrapper<WebhookLog>()
  90 + .eq(WebhookLog::getAppId, appId)
  91 + .orderByDesc(WebhookLog::getId)
  92 + .last("LIMIT " + offset + ",20"));
  93 + return Result.success(logs);
  94 + }
  95 +
  96 + /** 重试失败的 Webhook */
  97 + @PostMapping("/webhook/retry")
  98 + public Result<Void> retryWebhook(@RequestParam Long logId) {
  99 + webhookService.retry(logId);
  100 + return Result.success();
  101 + }
  102 +}
... ...
src/main/java/com/diligrp/rider/controller/PlatformSubstationController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/PlatformSubstationController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.ChangePasswordDTO;
  5 +import com.diligrp.rider.entity.Substation;
  6 +import com.diligrp.rider.service.SubstationService;
  7 +import jakarta.validation.Valid;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.*;
  10 +
  11 +import java.util.List;
  12 +
  13 +@RestController
  14 +@RequestMapping("/api/platform/substation")
  15 +@RequiredArgsConstructor
  16 +public class PlatformSubstationController {
  17 +
  18 + private final SubstationService substationService;
  19 +
  20 + /** 分站管理员列表 */
  21 + @GetMapping("/list")
  22 + public Result<List<Substation>> list(@RequestParam(required = false) String keyword) {
  23 + return Result.success(substationService.list(keyword));
  24 + }
  25 +
  26 + /** 新增分站管理员 */
  27 + @PostMapping("/add")
  28 + public Result<Void> add(@RequestBody Substation substation) {
  29 + substationService.add(substation);
  30 + return Result.success();
  31 + }
  32 +
  33 + /** 编辑分站管理员 */
  34 + @PutMapping("/edit")
  35 + public Result<Void> edit(@RequestBody Substation substation) {
  36 + substationService.edit(substation);
  37 + return Result.success();
  38 + }
  39 +
  40 + /** 禁用 */
  41 + @PostMapping("/ban")
  42 + public Result<Void> ban(@RequestParam Long id) {
  43 + substationService.ban(id);
  44 + return Result.success();
  45 + }
  46 +
  47 + /** 启用 */
  48 + @PostMapping("/cancelBan")
  49 + public Result<Void> cancelBan(@RequestParam Long id) {
  50 + substationService.cancelBan(id);
  51 + return Result.success();
  52 + }
  53 +
  54 + /** 删除 */
  55 + @DeleteMapping("/del")
  56 + public Result<Void> del(@RequestParam Long id) {
  57 + substationService.del(id);
  58 + return Result.success();
  59 + }
  60 +
  61 + /** 根据城市ID查分站管理员 */
  62 + @GetMapping("/getByCity")
  63 + public Result<Substation> getByCity(@RequestParam Long cityId) {
  64 + return Result.success(substationService.getByCityId(cityId));
  65 + }
  66 +
  67 + /**
  68 + * 分站管理员修改自己的密码
  69 + * 需要携带分站管理员 token(role=substation)
  70 + */
  71 + @PostMapping("/changePassword")
  72 + public Result<Void> changePassword(
  73 + @Valid @RequestBody ChangePasswordDTO dto,
  74 + jakarta.servlet.http.HttpServletRequest request) {
  75 + Long adminId = (Long) request.getAttribute("adminId");
  76 + substationService.changePassword(adminId, dto.getOldPassword(), dto.getNewPassword());
  77 + return Result.success();
  78 + }
  79 +}
... ...
src/main/java/com/diligrp/rider/controller/RiderAuthController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/RiderAuthController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.ApplyDTO;
  5 +import com.diligrp.rider.dto.LoginDTO;
  6 +import com.diligrp.rider.service.RiderAuthService;
  7 +import com.diligrp.rider.vo.RiderVO;
  8 +import jakarta.validation.Valid;
  9 +import lombok.RequiredArgsConstructor;
  10 +import org.springframework.web.bind.annotation.*;
  11 +
  12 +@RestController
  13 +@RequestMapping("/api/rider")
  14 +@RequiredArgsConstructor
  15 +public class RiderAuthController {
  16 +
  17 + private final RiderAuthService authService;
  18 +
  19 + /** 骑手申请注册 */
  20 + @PostMapping("/apply")
  21 + public Result<Void> apply(@Valid @RequestBody ApplyDTO dto) {
  22 + authService.apply(dto);
  23 + return Result.success();
  24 + }
  25 +
  26 + /** 密码登录 */
  27 + @PostMapping("/login/pass")
  28 + public Result<RiderVO> loginByPass(@Valid @RequestBody LoginDTO dto) {
  29 + return Result.success(authService.loginByPass(dto));
  30 + }
  31 +
  32 + /** 获取个人信息 */
  33 + @GetMapping("/user/info")
  34 + public Result<RiderVO> getInfo(jakarta.servlet.http.HttpServletRequest request) {
  35 + Long riderId = (Long) request.getAttribute("riderId");
  36 + return Result.success(authService.getInfo(riderId));
  37 + }
  38 +
  39 + /** 切换休息/接单状态 */
  40 + @PostMapping("/user/toggleRest")
  41 + public Result<Void> toggleRest(jakarta.servlet.http.HttpServletRequest request) {
  42 + Long riderId = (Long) request.getAttribute("riderId");
  43 + authService.toggleRest(riderId);
  44 + return Result.success();
  45 + }
  46 +}
... ...
src/main/java/com/diligrp/rider/controller/RiderBalanceController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/RiderBalanceController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.service.RiderBalanceService;
  5 +import com.diligrp.rider.vo.BalanceVO;
  6 +import jakarta.servlet.http.HttpServletRequest;
  7 +import lombok.RequiredArgsConstructor;
  8 +import org.springframework.web.bind.annotation.*;
  9 +
  10 +import java.math.BigDecimal;
  11 +
  12 +@RestController
  13 +@RequestMapping("/api/rider/balance")
  14 +@RequiredArgsConstructor
  15 +public class RiderBalanceController {
  16 +
  17 + private final RiderBalanceService balanceService;
  18 +
  19 + /** 查询余额及流水 */
  20 + @GetMapping
  21 + public Result<BalanceVO> getBalance(@RequestParam(defaultValue = "1") int page,
  22 + HttpServletRequest request) {
  23 + Long riderId = (Long) request.getAttribute("riderId");
  24 + return Result.success(balanceService.getBalance(riderId, page));
  25 + }
  26 +
  27 + /** 今日收入统计 */
  28 + @GetMapping("/today")
  29 + public Result<BigDecimal> todayIncome(HttpServletRequest request) {
  30 + Long riderId = (Long) request.getAttribute("riderId");
  31 + return Result.success(balanceService.getTodayIncome(riderId));
  32 + }
  33 +}
... ...
src/main/java/com/diligrp/rider/controller/RiderExtController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/RiderExtController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.service.RiderEvaluateService;
  5 +import com.diligrp.rider.service.RiderOrderService;
  6 +import com.diligrp.rider.service.RefundService;
  7 +import com.diligrp.rider.entity.OrderRefundReason;
  8 +import com.diligrp.rider.vo.RiderMonthCountVO;
  9 +import com.diligrp.rider.vo.RiderTodayCountVO;
  10 +import com.diligrp.rider.vo.OrderVO;
  11 +import jakarta.servlet.http.HttpServletRequest;
  12 +import lombok.Data;
  13 +import lombok.RequiredArgsConstructor;
  14 +import org.springframework.web.bind.annotation.*;
  15 +
  16 +import java.util.List;
  17 +
  18 +/**
  19 + * 骑手端扩展接口:转单/退款/评价/统计
  20 + */
  21 +@RestController
  22 +@RequestMapping("/api/rider")
  23 +@RequiredArgsConstructor
  24 +public class RiderExtController {
  25 +
  26 + private final RiderOrderService orderService;
  27 + private final RiderEvaluateService evaluateService;
  28 + private final RefundService refundService;
  29 +
  30 + @Data
  31 + static class OrderIdReq { private Long orderId; }
  32 + @Data
  33 + static class RefundReq {
  34 + private Long orderId;
  35 + private Long reasonId = 0L;
  36 + private String reason = "";
  37 + }
  38 +
  39 + // ---- 转单 ----
  40 +
  41 + /** 骑手申请转单(仅 status=3 可申请) */
  42 + @PostMapping("/order/applyTrans")
  43 + public Result<Void> applyTrans(@RequestBody OrderIdReq req, HttpServletRequest request) {
  44 + Long riderId = (Long) request.getAttribute("riderId");
  45 + orderService.applyTrans(riderId, req.getOrderId());
  46 + return Result.success();
  47 + }
  48 +
  49 + // ---- 统计 ----
  50 +
  51 + /** 今日统计 */
  52 + @GetMapping("/order/count/today")
  53 + public Result<RiderTodayCountVO> todayCount(HttpServletRequest request) {
  54 + Long riderId = (Long) request.getAttribute("riderId");
  55 + return Result.success(orderService.getTodayCount(riderId));
  56 + }
  57 +
  58 + /** 月度统计(year=0表示本年) */
  59 + @GetMapping("/order/count/month")
  60 + public Result<List<RiderMonthCountVO>> monthCount(
  61 + @RequestParam(defaultValue = "0") int year,
  62 + HttpServletRequest request) {
  63 + Long riderId = (Long) request.getAttribute("riderId");
  64 + return Result.success(orderService.getMonthCount(riderId, year));
  65 + }
  66 +
  67 + /**
  68 + * 骑手历史订单明细列表
  69 + * type=0全部 1已完成 2已转单
  70 + */
  71 + @GetMapping("/order/count/list")
  72 + public Result<List<OrderVO>> countList(
  73 + @RequestParam(defaultValue = "0") int type,
  74 + @RequestParam(defaultValue = "1") int page,
  75 + HttpServletRequest request) {
  76 + Long riderId = (Long) request.getAttribute("riderId");
  77 + return Result.success(orderService.getCountList(riderId, type, page));
  78 + }
  79 +
  80 + // ---- 退款 ----
  81 +
  82 + /** 退款原因列表(role=2骑手) */
  83 + @GetMapping("/order/refund/reasons")
  84 + public Result<List<OrderRefundReason>> refundReasons() {
  85 + return Result.success(refundService.getReasons(2));
  86 + }
  87 +
  88 + /** 骑手申请退款 */
  89 + @PostMapping("/order/refund/apply")
  90 + public Result<Void> applyRefund(@RequestBody RefundReq req, HttpServletRequest request) {
  91 + Long riderId = (Long) request.getAttribute("riderId");
  92 + Long reasonId = req.getReasonId() != null ? req.getReasonId() : 0L;
  93 + String reason = req.getReason() != null ? req.getReason() : "";
  94 + refundService.applyRefund(req.getOrderId(), riderId, 2, reasonId, reason);
  95 + return Result.success();
  96 + }
  97 +
  98 + // ---- 评价 ----
  99 +
  100 + /** 骑手查看自己的评价列表(type=1好评 2中评 3差评 0全部) */
  101 + @GetMapping("/evaluate/list")
  102 + public Result<List<?>> evaluateList(
  103 + @RequestParam(defaultValue = "0") int type,
  104 + @RequestParam(defaultValue = "1") int page,
  105 + HttpServletRequest request) {
  106 + Long riderId = (Long) request.getAttribute("riderId");
  107 + return Result.success(evaluateService.getRiderEvaluates(riderId, type, page));
  108 + }
  109 +
  110 + /** 本月好评数 */
  111 + @GetMapping("/evaluate/month/good")
  112 + public Result<Integer> monthGoodCount(HttpServletRequest request) {
  113 + Long riderId = (Long) request.getAttribute("riderId");
  114 + return Result.success(evaluateService.getMonthGoodCount(riderId));
  115 + }
  116 +}
... ...
src/main/java/com/diligrp/rider/controller/RiderLocationController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/RiderLocationController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.LocationDTO;
  5 +import com.diligrp.rider.service.RiderLocationService;
  6 +import com.diligrp.rider.vo.NearbyRiderVO;
  7 +import jakarta.servlet.http.HttpServletRequest;
  8 +import jakarta.validation.Valid;
  9 +import lombok.RequiredArgsConstructor;
  10 +import org.springframework.web.bind.annotation.*;
  11 +
  12 +import java.util.List;
  13 +
  14 +@RestController
  15 +@RequestMapping("/api/rider/location")
  16 +@RequiredArgsConstructor
  17 +public class RiderLocationController {
  18 +
  19 + private final RiderLocationService locationService;
  20 +
  21 + /** 上报位置 */
  22 + @PostMapping("/update")
  23 + public Result<Void> update(@Valid @RequestBody LocationDTO dto, HttpServletRequest request) {
  24 + Long riderId = (Long) request.getAttribute("riderId");
  25 + locationService.updateLocation(riderId, dto);
  26 + return Result.success();
  27 + }
  28 +
  29 + /** 查看自己位置 */
  30 + @GetMapping
  31 + public Result<LocationDTO> get(HttpServletRequest request) {
  32 + Long riderId = (Long) request.getAttribute("riderId");
  33 + return Result.success(locationService.getLocation(riderId));
  34 + }
  35 +
  36 + /**
  37 + * 附近在线骑手列表(分站地图查看用)
  38 + * Location.getNearby()
  39 + */
  40 + @GetMapping("/nearby")
  41 + public Result<List<NearbyRiderVO>> nearby(
  42 + @RequestParam Long cityId,
  43 + @RequestParam String lng,
  44 + @RequestParam String lat,
  45 + HttpServletRequest request) {
  46 + return Result.success(locationService.getNearby(cityId, lng, lat));
  47 + }
  48 +}
... ...
src/main/java/com/diligrp/rider/controller/RiderOrderController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/RiderOrderController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.service.RiderOrderService;
  5 +import com.diligrp.rider.vo.OrderVO;
  6 +import jakarta.servlet.http.HttpServletRequest;
  7 +import lombok.Data;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.web.bind.annotation.*;
  10 +
  11 +import java.util.List;
  12 +
  13 +@RestController
  14 +@RequestMapping("/api/rider/order")
  15 +@RequiredArgsConstructor
  16 +public class RiderOrderController {
  17 +
  18 + private final RiderOrderService orderService;
  19 +
  20 + @Data
  21 + static class OrderIdCityReq { private Long orderId; private Long cityId; }
  22 + @Data
  23 + static class OrderCompleteReq { private Long orderId; private String thumbs; }
  24 + @Data
  25 + static class OrderStartReq { private Long orderId; private String code; }
  26 +
  27 + /**
  28 + * 订单列表
  29 + * @param type 1=待接单 2=待取货 3=待完成
  30 + * @param cityId 城市ID
  31 + * @param page 页码
  32 + */
  33 + @GetMapping("/list")
  34 + public Result<List<OrderVO>> list(@RequestParam Integer type,
  35 + @RequestParam Long cityId,
  36 + @RequestParam(defaultValue = "1") int page,
  37 + HttpServletRequest request) {
  38 + Long riderId = (Long) request.getAttribute("riderId");
  39 + return Result.success(orderService.getList(riderId, cityId, type, page));
  40 + }
  41 +
  42 + /** 订单详情 */
  43 + @GetMapping("/detail")
  44 + public Result<OrderVO> detail(@RequestParam Long orderId, HttpServletRequest request) {
  45 + Long riderId = (Long) request.getAttribute("riderId");
  46 + return Result.success(orderService.getDetail(riderId, orderId));
  47 + }
  48 +
  49 + /** 拒单 */
  50 + @PostMapping("/refuse")
  51 + public Result<Void> refuse(@RequestBody OrderIdCityReq req, HttpServletRequest request) {
  52 + Long riderId = (Long) request.getAttribute("riderId");
  53 + orderService.refuse(riderId, req.getCityId(), req.getOrderId());
  54 + return Result.success();
  55 + }
  56 +
  57 + /** 抢单 */
  58 + @PostMapping("/grap")
  59 + public Result<Void> grap(@RequestBody OrderIdCityReq req, HttpServletRequest request) {
  60 + Long riderId = (Long) request.getAttribute("riderId");
  61 + orderService.grap(riderId, req.getCityId(), req.getOrderId());
  62 + return Result.success();
  63 + }
  64 +
  65 + /** 开始服务(取件),输入完成码 */
  66 + @PostMapping("/start")
  67 + public Result<Void> start(@RequestBody OrderStartReq req, HttpServletRequest request) {
  68 + Long riderId = (Long) request.getAttribute("riderId");
  69 + orderService.start(riderId, req.getOrderId(), req.getCode());
  70 + return Result.success();
  71 + }
  72 +
  73 + /** 完成订单,上传照片 */
  74 + @PostMapping("/complete")
  75 + public Result<Void> complete(@RequestBody OrderCompleteReq req, HttpServletRequest request) {
  76 + Long riderId = (Long) request.getAttribute("riderId");
  77 + orderService.complete(riderId, req.getOrderId(), req.getThumbs());
  78 + return Result.success();
  79 + }
  80 +}
... ...
src/main/java/com/diligrp/rider/controller/RiderUploadController.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/controller/RiderUploadController.java
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import lombok.RequiredArgsConstructor;
  5 +import org.springframework.web.bind.annotation.*;
  6 +import org.springframework.web.multipart.MultipartFile;
  7 +
  8 +import java.util.Map;
  9 +import java.util.UUID;
  10 +
  11 +@RestController
  12 +@RequestMapping("/api/rider/upload")
  13 +@RequiredArgsConstructor
  14 +public class RiderUploadController {
  15 +
  16 + /** 文件上传(mock:返回占位图URL)TODO: 接入实际存储服务 */
  17 + @PostMapping("")
  18 + public Result<Map<String, Object>> upload(@RequestParam("file") MultipartFile file) {
  19 + String filename = UUID.randomUUID().toString().replace("-", "") + ".jpg";
  20 + String url = "https://diligrp.com/static/upload/" + filename;
  21 + return Result.success(Map.of("url", url));
  22 + }
  23 +
  24 + @GetMapping("/config")
  25 + public Result<Map<String, Object>> getUploadConfig() {
  26 + return Result.success(Map.of(
  27 + "url", 0,
  28 + "domain", "",
  29 + "qiniu", Map.of(),
  30 + "ali", Map.of(),
  31 + "txcos", Map.of()
  32 + ));
  33 + }
  34 +
  35 + @GetMapping("/qiniu/token")
  36 + public Result<Map<String, Object>> getQiniuToken() {
  37 + return Result.success(Map.of("token", ""));
  38 + }
  39 +
  40 + @GetMapping("/ali/sts")
  41 + public Result<Map<String, Object>> getAliSts() {
  42 + return Result.success(Map.of(
  43 + "accessKeyId", "",
  44 + "accessKeySecret", "",
  45 + "securityToken", "",
  46 + "endpoint", "",
  47 + "bucket", ""
  48 + ));
  49 + }
  50 +
  51 + @GetMapping("/txcos/sts")
  52 + public Result<Map<String, Object>> getTxSts() {
  53 + return Result.success(Map.of(
  54 + "tmpSecretId", "",
  55 + "tmpSecretKey", "",
  56 + "sessionToken", "",
  57 + "region", "",
  58 + "bucket", "",
  59 + "appid", ""
  60 + ));
  61 + }
  62 +}
... ...
src/main/java/com/diligrp/rider/dto/AdminLoginDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/AdminLoginDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import lombok.Data;
  5 +
  6 +@Data
  7 +public class AdminLoginDTO {
  8 + @NotBlank(message = "账号不能为空")
  9 + private String account;
  10 + @NotBlank(message = "密码不能为空")
  11 + private String pass;
  12 + /** 登录角色:admin=超级管理员 substation=分站管理员 */
  13 + private String role = "substation";
  14 +}
... ...
src/main/java/com/diligrp/rider/dto/AdminRiderAddDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/AdminRiderAddDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import lombok.Data;
  5 +
  6 +@Data
  7 +public class AdminRiderAddDTO {
  8 + @NotBlank(message = "昵称不能为空")
  9 + private String userNickname;
  10 +
  11 + @NotBlank(message = "手机号不能为空")
  12 + private String mobile;
  13 +
  14 + @NotBlank(message = "密码不能为空")
  15 + private String password;
  16 +
  17 + private Long cityId;
  18 +}
... ...
src/main/java/com/diligrp/rider/dto/AdminRiderLevelSaveDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/AdminRiderLevelSaveDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +import java.math.BigDecimal;
  8 +
  9 +@Data
  10 +public class AdminRiderLevelSaveDTO {
  11 + private Long id;
  12 +
  13 + @NotNull(message = "城市不能为空")
  14 + private Long cityId;
  15 +
  16 + @NotNull(message = "等级编号不能为空")
  17 + private Integer levelId;
  18 +
  19 + @NotBlank(message = "等级名称不能为空")
  20 + private String name;
  21 +
  22 + @NotNull(message = "转单次数上限不能为空")
  23 + private Integer transNums;
  24 +
  25 + @NotNull(message = "收入模式不能为空")
  26 + private Integer runFeeMode;
  27 +
  28 + private BigDecimal runFixMoney;
  29 + private BigDecimal runRate;
  30 + private Integer distanceBasic;
  31 + private BigDecimal distanceBasicMoney;
  32 + private BigDecimal distanceMoreMoney;
  33 + private BigDecimal distanceMaxMoney;
  34 +}
... ...
src/main/java/com/diligrp/rider/dto/ApplyDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/ApplyDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Data
  8 +public class ApplyDTO {
  9 + @NotBlank(message = "姓名不能为空")
  10 + private String name;
  11 + @NotBlank(message = "手机号不能为空")
  12 + private String mobile;
  13 + @NotBlank(message = "密码不能为空")
  14 + private String pass;
  15 + @NotBlank(message = "验证码不能为空")
  16 + private String code;
  17 + private String idNo;
  18 + private String thumb;
  19 + @NotNull(message = "城市不能为空")
  20 + private Long cityId;
  21 +}
... ...
src/main/java/com/diligrp/rider/dto/ChangePasswordDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/ChangePasswordDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import lombok.Data;
  5 +
  6 +@Data
  7 +public class ChangePasswordDTO {
  8 + @NotBlank(message = "原密码不能为空")
  9 + private String oldPassword;
  10 + @NotBlank(message = "新密码不能为空")
  11 + private String newPassword;
  12 +}
... ...
src/main/java/com/diligrp/rider/dto/DeliveryFeeCalcDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/DeliveryFeeCalcDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotNull;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 配送费计算请求 DTO
  10 + */
  11 +@Data
  12 +public class DeliveryFeeCalcDTO {
  13 +
  14 + @NotNull(message = "城市ID不能为空")
  15 + private Long cityId;
  16 +
  17 + /** 订单类型:1=帮我送 2=帮我取 6=外卖配送 */
  18 + @NotNull(message = "订单类型不能为空")
  19 + private Integer orderType;
  20 +
  21 + /** 起点经度 */
  22 + @NotNull(message = "起点经度不能为空")
  23 + private String startLng;
  24 +
  25 + /** 起点纬度 */
  26 + @NotNull(message = "起点纬度不能为空")
  27 + private String startLat;
  28 +
  29 + /** 终点经度 */
  30 + @NotNull(message = "终点经度不能为空")
  31 + private String endLng;
  32 +
  33 + /** 终点纬度 */
  34 + @NotNull(message = "终点纬度不能为空")
  35 + private String endLat;
  36 +
  37 + /** 重量(kg),不传默认0 */
  38 + private BigDecimal weight = BigDecimal.ZERO;
  39 +
  40 + /** 件数,不传默认0 */
  41 + private Integer pieces = 0;
  42 +
  43 + /** 服务时间戳(秒),用于时段附加费匹配,0=当前时间 */
  44 + private Long serviceTime = 0L;
  45 +}
... ...
src/main/java/com/diligrp/rider/dto/DeliveryFeePlanPreviewDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/DeliveryFeePlanPreviewDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class DeliveryFeePlanPreviewDTO {
  7 +
  8 + private DeliveryPricingConfigDTO config;
  9 +
  10 + private DeliveryFeeCalcDTO calc;
  11 +}
... ...
src/main/java/com/diligrp/rider/dto/DeliveryFeePlanSaveDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/DeliveryFeePlanSaveDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class DeliveryFeePlanSaveDTO {
  7 +
  8 + private String name;
  9 +
  10 + private Integer status;
  11 +
  12 + private Integer listOrder;
  13 +
  14 + private String remark;
  15 +
  16 + private DeliveryPricingConfigDTO config;
  17 +}
... ...
src/main/java/com/diligrp/rider/dto/DeliveryOrderCreateDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/DeliveryOrderCreateDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +import java.math.BigDecimal;
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * 外部系统推单请求 DTO
  12 + * 对应 POST /api/open/delivery/order/create
  13 + */
  14 +@Data
  15 +public class DeliveryOrderCreateDTO {
  16 +
  17 + /** 城市ID */
  18 + private Long cityId;
  19 +
  20 + /**
  21 + * 接入方门店编号(对应 merchant_store.out_store_id)
  22 + * 传入后中台自动查找门店,填充名称/地址/经纬度,无需重复传 storeName/storeLng/storeLat
  23 + */
  24 + private String outStoreId;
  25 +
  26 + /** 发货门店名称(outStoreId 为空时必填) */
  27 + private String storeName;
  28 +
  29 + /** 发货门店地址 */
  30 + private String storeAddr;
  31 +
  32 + /** 发货门店经度(extStoreId 为空时必填) */
  33 + private String storeLng;
  34 +
  35 + /** 发货门店纬度(extStoreId 为空时必填) */
  36 + private String storeLat;
  37 +
  38 + /** 收件人姓名 */
  39 + @NotBlank(message = "收件人姓名不能为空")
  40 + private String recipName;
  41 +
  42 + /** 收件人电话 */
  43 + @NotBlank(message = "收件人电话不能为空")
  44 + private String recipPhone;
  45 +
  46 + /** 收件人地址 */
  47 + @NotBlank(message = "收件人地址不能为空")
  48 + private String recipAddr;
  49 +
  50 + /** 收件人经度 */
  51 + @NotBlank(message = "收件人经度不能为空")
  52 + private String recipLng;
  53 +
  54 + /** 收件人纬度 */
  55 + @NotBlank(message = "收件人纬度不能为空")
  56 + private String recipLat;
  57 +
  58 + /** 外部系统订单号(用于回调对账,需唯一) */
  59 + @NotBlank(message = "外部订单号不能为空")
  60 + private String outOrderNo;
  61 +
  62 + /** 货物重量(kg),用于重量计费,0=不计重 */
  63 + private BigDecimal weight = BigDecimal.ZERO;
  64 +
  65 + /** 期望配送时间戳(秒),0=立即配送 */
  66 + private Long serviceTime = 0L;
  67 +
  68 + /** 状态变更回调地址(可覆盖应用级配置) */
  69 + private String callbackUrl;
  70 +
  71 + /** 整单备注 */
  72 + private String remark;
  73 +
  74 + /**
  75 + * 货物清单(骑手端可见)
  76 + * 配送中台不解析具体业务含义,只做透传快照
  77 + */
  78 + private List<DeliveryItemDTO> items;
  79 +
  80 + /** 整单货物备注(如:放门口、不要辣等) */
  81 + private String itemRemark;
  82 +
  83 + @Data
  84 + public static class DeliveryItemDTO {
  85 + /** 货物名称 */
  86 + private String name;
  87 + /** 数量 */
  88 + private Integer quantity = 1;
  89 + /** 规格/型号(如大份、500ml等) */
  90 + private String spec;
  91 + /** 单项备注 */
  92 + private String remark;
  93 + }
  94 +}
... ...
src/main/java/com/diligrp/rider/dto/DeliveryPricingConfigDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/DeliveryPricingConfigDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.math.BigDecimal;
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * 外卖配送计价方案 DTO
  10 + */
  11 +@Data
  12 +public class DeliveryPricingConfigDTO {
  13 +
  14 + private List<Integer> type;
  15 + private DeliveryPricingRuleDTO type6;
  16 + private DeliveryPricingRuleDTO type1;
  17 + private DeliveryPricingRuleDTO type2;
  18 + private BigDecimal distanceBasic;
  19 + private Integer distanceBasicTime;
  20 + private Integer distanceMoreTime;
  21 + private BigDecimal riderDistance;
  22 + private Integer riderTime;
  23 +}
... ...
src/main/java/com/diligrp/rider/dto/DeliveryPricingRuleDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/DeliveryPricingRuleDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.math.BigDecimal;
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * 外卖配送计价维度 DTO
  10 + */
  11 +@Data
  12 +public class DeliveryPricingRuleDTO {
  13 +
  14 + private BigDecimal minFee = BigDecimal.ZERO;
  15 + private Integer baseSwitch = 0;
  16 + private BigDecimal baseFee = BigDecimal.ZERO;
  17 + private Integer feeMode = 1;
  18 + private BigDecimal fixMoney = BigDecimal.ZERO;
  19 + private Integer distanceSwitch = 0;
  20 + private BigDecimal distanceBasic = BigDecimal.ZERO;
  21 + private BigDecimal distanceBasicMoney = BigDecimal.ZERO;
  22 + private BigDecimal distanceMoreMoney = BigDecimal.ZERO;
  23 + private Integer distanceMode = 1;
  24 + private Integer distanceType = 1;
  25 + private List<DistanceStepDTO> distanceSteps;
  26 + private Integer weightSwitch = 0;
  27 + private BigDecimal weightFirst = BigDecimal.ZERO;
  28 + private BigDecimal weightFirstFee = BigDecimal.ZERO;
  29 + private BigDecimal weightUnitFee = BigDecimal.ZERO;
  30 + private BigDecimal weightCapFee = BigDecimal.ZERO;
  31 + private BigDecimal weightBasic = BigDecimal.ZERO;
  32 + private BigDecimal weightBasicMoney = BigDecimal.ZERO;
  33 + private BigDecimal weightMoreMoney = BigDecimal.ZERO;
  34 + private Integer weightType = 1;
  35 + private Integer pieceSwitch = 0;
  36 + private List<PieceRuleDTO> pieceRules;
  37 + private List<TimePeriodDTO> times;
  38 +
  39 + @Data
  40 + public static class DistanceStepDTO {
  41 + private BigDecimal endDistance;
  42 + private BigDecimal unitDistance;
  43 + private BigDecimal unitFee;
  44 + private Integer listOrder;
  45 + }
  46 +
  47 + @Data
  48 + public static class PieceRuleDTO {
  49 + private Integer startPiece;
  50 + private Integer endPiece;
  51 + private BigDecimal fee;
  52 + private Integer listOrder;
  53 + }
  54 +
  55 + @Data
  56 + public static class TimePeriodDTO {
  57 + private Integer start;
  58 + private Integer end;
  59 + private Integer isOpen;
  60 + private BigDecimal money;
  61 + }
  62 +}
... ...
src/main/java/com/diligrp/rider/dto/LocationDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/LocationDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotNull;
  4 +import lombok.Data;
  5 +
  6 +@Data
  7 +public class LocationDTO {
  8 + @NotNull(message = "经度不能为空")
  9 + private String lng;
  10 + @NotNull(message = "纬度不能为空")
  11 + private String lat;
  12 +}
... ...
src/main/java/com/diligrp/rider/dto/LoginDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/LoginDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import lombok.Data;
  5 +
  6 +@Data
  7 +public class LoginDTO {
  8 + @NotBlank(message = "手机号不能为空")
  9 + private String username;
  10 + @NotBlank(message = "密码不能为空")
  11 + private String pass;
  12 +}
... ...
src/main/java/com/diligrp/rider/dto/MerchantEnterDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/MerchantEnterDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Data
  8 +public class MerchantEnterDTO {
  9 + @NotBlank(message = "联系人姓名不能为空")
  10 + private String name;
  11 + @NotBlank(message = "手机号不能为空")
  12 + private String mobile;
  13 + @NotBlank(message = "店铺名称不能为空")
  14 + private String storeName;
  15 + @NotNull(message = "城市不能为空")
  16 + private Long cityId;
  17 + private String remark;
  18 + /** 类型:1=商家入驻 2=骑手入驻 3=商务合作 */
  19 + private Integer type = 1;
  20 +}
... ...
src/main/java/com/diligrp/rider/dto/MerchantStoreDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/MerchantStoreDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +import java.math.BigDecimal;
  8 +
  9 +@Data
  10 +public class MerchantStoreDTO {
  11 + private Long id;
  12 + @NotBlank(message = "店铺名称不能为空")
  13 + private String name;
  14 + @NotNull(message = "城市不能为空")
  15 + private Long cityId;
  16 + private String thumb;
  17 + private String address;
  18 + private String lng;
  19 + private String lat;
  20 + /** 营业状态:0=打烊 1=营业 */
  21 + private Integer operatingState = 1;
  22 + /** 是否自动接单 */
  23 + private Integer automaticOrder = 0;
  24 + /** 配送类型:1=外卖配送 2=到店自提 */
  25 + private Integer shippingType = 1;
  26 + /** 免运费门槛 */
  27 + private BigDecimal freeShipping = BigDecimal.ZERO;
  28 + /** 起送金额 */
  29 + private BigDecimal upToSend = BigDecimal.ZERO;
  30 + /** 营业日期JSON */
  31 + private String openDate;
  32 + /** 营业时间JSON */
  33 + private String openTime;
  34 + private String about;
  35 + /** 商家账号手机号(用于创建登录账号) */
  36 + private String accountMobile;
  37 + /**
  38 + * 接入方自己系统的门店编号(外部门店ID)
  39 + * 外部系统同步门店时必填;平台自建时选填,便于与外部系统对账
  40 + */
  41 + private String outStoreId;
  42 +}
... ...
src/main/java/com/diligrp/rider/dto/OpenDeliveryFeeCalcDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/OpenDeliveryFeeCalcDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotNull;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 开放平台配送费计算请求 DTO
  10 + * cityId 由 X-App-Key 绑定的租户自动带出,不允许外部传入。
  11 + */
  12 +@Data
  13 +public class OpenDeliveryFeeCalcDTO {
  14 +
  15 + /** 订单类型:1=帮我送 2=帮我取 6=外卖配送 */
  16 + @NotNull(message = "订单类型不能为空")
  17 + private Integer orderType;
  18 +
  19 + /** 起点经度 */
  20 + @NotNull(message = "起点经度不能为空")
  21 + private String startLng;
  22 +
  23 + /** 起点纬度 */
  24 + @NotNull(message = "起点纬度不能为空")
  25 + private String startLat;
  26 +
  27 + /** 终点经度 */
  28 + @NotNull(message = "终点经度不能为空")
  29 + private String endLng;
  30 +
  31 + /** 终点纬度 */
  32 + @NotNull(message = "终点纬度不能为空")
  33 + private String endLat;
  34 +
  35 + /** 重量(kg),不传默认0 */
  36 + private BigDecimal weight = BigDecimal.ZERO;
  37 +
  38 + /** 件数,不传默认0 */
  39 + private Integer pieces = 0;
  40 +
  41 + /** 服务时间戳(秒),用于时段附加费匹配,0=当前时间 */
  42 + private Long serviceTime = 0L;
  43 +}
... ...
src/main/java/com/diligrp/rider/dto/PlatformMockDeliveryCreateDTO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/dto/PlatformMockDeliveryCreateDTO.java
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotNull;
  4 +import lombok.Data;
  5 +import lombok.EqualsAndHashCode;
  6 +
  7 +/**
  8 + * 平台后台模拟推单 DTO
  9 + */
  10 +@Data
  11 +@EqualsAndHashCode(callSuper = true)
  12 +public class PlatformMockDeliveryCreateDTO extends DeliveryOrderCreateDTO {
  13 +
  14 + /** 开放平台应用ID */
  15 + @NotNull(message = "应用ID不能为空")
  16 + private Long appId;
  17 +}
... ...
src/main/java/com/diligrp/rider/entity/AdminUser.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/AdminUser.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 超级管理员表
  8 + */
  9 +@Data
  10 +@TableName("admin_user")
  11 +public class AdminUser {
  12 +
  13 + @TableId(type = IdType.AUTO)
  14 + private Long id;
  15 +
  16 + /** 登录账号 */
  17 + private String userLogin;
  18 +
  19 + /** 密码(MD5) */
  20 + private String userPass;
  21 +
  22 + /** 昵称 */
  23 + private String userNickname;
  24 +
  25 + /** 状态:0=禁用 1=正常 */
  26 + private Integer userStatus;
  27 +
  28 + private Long createTime;
  29 +}
... ...
src/main/java/com/diligrp/rider/entity/City.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/City.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 城市表
  10 + * city表,是配送中台的核心配置单元
  11 + */
  12 +@Data
  13 +@TableName("city")
  14 +public class City {
  15 +
  16 + @TableId(type = IdType.AUTO)
  17 + private Long id;
  18 +
  19 + /** 父级ID,0=省级 */
  20 + private Long pid;
  21 +
  22 + /** 城市名称 */
  23 + private String name;
  24 +
  25 + /** 行政区划码 */
  26 + private String areaCode;
  27 +
  28 + /** 状态:0=未开通 1=已开通 */
  29 + private Integer status;
  30 +
  31 + /** 平台抽成比例(%) */
  32 + private BigDecimal rate;
  33 +
  34 + /** 排序 */
  35 + private Integer listOrder;
  36 +}
... ...
src/main/java/com/diligrp/rider/entity/DeliveryFeePlan.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/DeliveryFeePlan.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +@Data
  11 +@TableName("delivery_fee_plan")
  12 +public class DeliveryFeePlan {
  13 +
  14 + @TableId(type = IdType.AUTO)
  15 + private Long id;
  16 +
  17 + private Long cityId;
  18 +
  19 + private String name;
  20 +
  21 + private Integer isDefault;
  22 +
  23 + private BigDecimal minFee;
  24 +
  25 + private BigDecimal distanceBasic;
  26 +
  27 + private Integer distanceBasicTime;
  28 +
  29 + private Integer distanceMoreTime;
  30 +
  31 + private BigDecimal riderDistance;
  32 +
  33 + private Integer riderTime;
  34 +
  35 + private Integer status;
  36 +
  37 + private Integer listOrder;
  38 +
  39 + private String remark;
  40 +
  41 + private Long createTime;
  42 +
  43 + private Long updateTime;
  44 +}
... ...
src/main/java/com/diligrp/rider/entity/DeliveryFeePlanDimension.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/DeliveryFeePlanDimension.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +@Data
  11 +@TableName("delivery_fee_plan_dimension")
  12 +public class DeliveryFeePlanDimension {
  13 +
  14 + @TableId(type = IdType.AUTO)
  15 + private Long id;
  16 +
  17 + private Long planId;
  18 +
  19 + private String dimensionType;
  20 +
  21 + private Integer enabled;
  22 +
  23 + private BigDecimal baseFee;
  24 +
  25 + private BigDecimal startDistance;
  26 +
  27 + private BigDecimal startFee;
  28 +
  29 + private BigDecimal firstWeight;
  30 +
  31 + private BigDecimal firstFee;
  32 +
  33 + private BigDecimal unitWeightFee;
  34 +
  35 + private BigDecimal capFee;
  36 +
  37 + private String extraJson;
  38 +
  39 + private Long createTime;
  40 +
  41 + private Long updateTime;
  42 +}
... ...
src/main/java/com/diligrp/rider/entity/DeliveryFeePlanDistanceStep.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/DeliveryFeePlanDistanceStep.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +@Data
  11 +@TableName("delivery_fee_plan_distance_step")
  12 +public class DeliveryFeePlanDistanceStep {
  13 +
  14 + @TableId(type = IdType.AUTO)
  15 + private Long id;
  16 +
  17 + private Long planId;
  18 +
  19 + private BigDecimal endDistance;
  20 +
  21 + private BigDecimal unitDistance;
  22 +
  23 + private BigDecimal unitFee;
  24 +
  25 + private Integer listOrder;
  26 +
  27 + private Long createTime;
  28 +
  29 + private Long updateTime;
  30 +}
... ...
src/main/java/com/diligrp/rider/entity/DeliveryFeePlanPieceRule.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/DeliveryFeePlanPieceRule.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +@Data
  11 +@TableName("delivery_fee_plan_piece_rule")
  12 +public class DeliveryFeePlanPieceRule {
  13 +
  14 + @TableId(type = IdType.AUTO)
  15 + private Long id;
  16 +
  17 + private Long planId;
  18 +
  19 + private Integer startPiece;
  20 +
  21 + private Integer endPiece;
  22 +
  23 + private BigDecimal fee;
  24 +
  25 + private Integer listOrder;
  26 +
  27 + private Long createTime;
  28 +
  29 + private Long updateTime;
  30 +}
... ...
src/main/java/com/diligrp/rider/entity/DeliveryFeePlanTimeRule.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/DeliveryFeePlanTimeRule.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +@Data
  11 +@TableName("delivery_fee_plan_time_rule")
  12 +public class DeliveryFeePlanTimeRule {
  13 +
  14 + @TableId(type = IdType.AUTO)
  15 + private Long id;
  16 +
  17 + private Long planId;
  18 +
  19 + private Integer startMinute;
  20 +
  21 + private Integer endMinute;
  22 +
  23 + private BigDecimal fee;
  24 +
  25 + private Integer enabled;
  26 +
  27 + private Integer listOrder;
  28 +
  29 + private Long createTime;
  30 +
  31 + private Long updateTime;
  32 +}
... ...
src/main/java/com/diligrp/rider/entity/ExtStore.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/ExtStore.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 外部门店表
  8 + * 接入方通过 API 注册/同步自己系统的门店到配送中台
  9 + * 与 AppKey 绑定,一个应用可有多个门店
  10 + */
  11 +@Data
  12 +@TableName("ext_store")
  13 +public class ExtStore {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 所属应用 AppKey */
  19 + private String appKey;
  20 +
  21 + /** 接入方自己系统的门店ID(外部系统的原始ID,用于对账) */
  22 + private String outStoreId;
  23 +
  24 + /** 门店名称 */
  25 + private String name;
  26 +
  27 + /** 门店地址 */
  28 + private String address;
  29 +
  30 + /** 经度 */
  31 + private String lng;
  32 +
  33 + /** 纬度 */
  34 + private String lat;
  35 +
  36 + /** 所属城市ID */
  37 + private Long cityId;
  38 +
  39 + /** 门店联系电话 */
  40 + private String phone;
  41 +
  42 + /** 营业状态:0=关闭 1=营业 */
  43 + private Integer status;
  44 +
  45 + /** 备注 */
  46 + private String remark;
  47 +
  48 + private Long createTime;
  49 +
  50 + private Long updateTime;
  51 +}
... ...
src/main/java/com/diligrp/rider/entity/MerchantEnter.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/MerchantEnter.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 商家入驻申请表
  10 + * merchant_enter表
  11 + */
  12 +@Data
  13 +@TableName("merchant_enter")
  14 +public class MerchantEnter {
  15 +
  16 + @TableId(type = IdType.AUTO)
  17 + private Long id;
  18 +
  19 + /** 联系人姓名 */
  20 + private String name;
  21 +
  22 + /** 手机号 */
  23 + private String mobile;
  24 +
  25 + /** 店铺名称 */
  26 + private String storeName;
  27 +
  28 + /** 申请类型:1=商家入驻 2=骑手入驻 3=商务合作 */
  29 + private Integer type;
  30 +
  31 + /** 所在城市ID */
  32 + private Long cityId;
  33 +
  34 + /** 备注 */
  35 + private String remark;
  36 +
  37 + /** 状态:0=未处理 1=已通过 -1=已拒绝 */
  38 + private Integer status;
  39 +
  40 + private Long addTime;
  41 +}
... ...
src/main/java/com/diligrp/rider/entity/MerchantStore.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/MerchantStore.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 商家店铺表
  10 + * merchant_store表
  11 + */
  12 +@Data
  13 +@TableName("merchant_store")
  14 +public class MerchantStore {
  15 +
  16 + @TableId(type = IdType.AUTO)
  17 + private Long id;
  18 +
  19 + /** 店铺名称 */
  20 + private String name;
  21 +
  22 + /** 店铺封面图 */
  23 + private String thumb;
  24 +
  25 + /** 所属城市ID */
  26 + private Long cityId;
  27 +
  28 + /** 店铺地址 */
  29 + private String address;
  30 +
  31 + /** 经度 */
  32 + private String lng;
  33 +
  34 + /** 纬度 */
  35 + private String lat;
  36 +
  37 + /** 营业状态:0=打烊 1=营业 */
  38 + private Integer operatingState;
  39 +
  40 + /** 是否自动接单:0=否 1=是 */
  41 + private Integer automaticOrder;
  42 +
  43 + /** 配送类型:1=外卖配送 2=到店自提 */
  44 + private Integer shippingType;
  45 +
  46 + /** 免运费门槛(订单金额满X元免运费,0=不免) */
  47 + private BigDecimal freeShipping;
  48 +
  49 + /** 起送金额(订单金额低于此值不接单,0=不限) */
  50 + private BigDecimal upToSend;
  51 +
  52 + /** 营业日期JSON,如[1,2,3,4,5]表示周一到周五 */
  53 + private String openDate;
  54 +
  55 + /** 营业时间JSON,如["09:00","22:00"] */
  56 + private String openTime;
  57 +
  58 + /** 店铺简介 */
  59 + private String about;
  60 +
  61 + /** 关联账号ID(merchant_users表) */
  62 + private Long accountId;
  63 +
  64 + /**
  65 + * 关联接入方 AppKey(为空=平台自建门店)
  66 + * 外部系统同步门店时写入,用于标识门店归属哪个接入方
  67 + */
  68 + private String appKey;
  69 +
  70 + /**
  71 + * 接入方自己系统的门店ID(外部门店编号)
  72 + * 推单时传此字段,中台自动匹配门店经纬度
  73 + * 平台自建门店也可手动填入,便于与外部系统对账
  74 + */
  75 + private String outStoreId;
  76 +
  77 + /** 排序 */
  78 + private Integer listOrder;
  79 +
  80 + /** 是否删除 */
  81 + @TableLogic
  82 + private Integer isDel;
  83 +
  84 + private Long addTime;
  85 +}
... ...
src/main/java/com/diligrp/rider/entity/MerchantUsers.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/MerchantUsers.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 商家账号表
  10 + * merchant_users(商家登录账号,与店铺1:1)
  11 + */
  12 +@Data
  13 +@TableName("merchant_users")
  14 +public class MerchantUsers {
  15 +
  16 + @TableId(type = IdType.AUTO)
  17 + private Long id;
  18 +
  19 + /** 关联店铺ID */
  20 + private Long storeId;
  21 +
  22 + /** 手机号(即登录账号) */
  23 + private String mobile;
  24 +
  25 + /** 昵称 */
  26 + private String userNickname;
  27 +
  28 + /** 账号状态:0=禁用 1=正常 */
  29 + private Integer userStatus;
  30 +
  31 + /** 账号类型:1=商家 */
  32 + private Integer type;
  33 +
  34 + private Long createTime;
  35 +}
... ...
src/main/java/com/diligrp/rider/entity/OpenApp.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/OpenApp.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 开放平台应用表
  8 + * 第三方系统接入时申请 AppKey/AppSecret
  9 + */
  10 +@Data
  11 +@TableName("open_app")
  12 +public class OpenApp {
  13 +
  14 + @TableId(type = IdType.AUTO)
  15 + private Long id;
  16 +
  17 + /** 应用名称 */
  18 + private String appName;
  19 +
  20 + /** AppKey(唯一标识) */
  21 + private String appKey;
  22 +
  23 + /** AppSecret(签名密钥,不对外展示) */
  24 + private String appSecret;
  25 +
  26 + /** 关联商家店铺ID,0=平台级接入 */
  27 + private Long storeId;
  28 +
  29 + /**
  30 + * 关联租户/城市ID(必填)
  31 + * 该 AppKey 只能在此城市下推单、查询、调度骑手
  32 + */
  33 + private Long cityId;
  34 +
  35 + /** 状态:0=禁用 1=正常 */
  36 + private Integer status;
  37 +
  38 + /** Webhook 回调地址 */
  39 + private String webhookUrl;
  40 +
  41 + /** 回调事件订阅(JSON数组),如["order.paid","order.completed"] */
  42 + private String webhookEvents;
  43 +
  44 + /** 备注 */
  45 + private String remark;
  46 +
  47 + private Long createTime;
  48 +}
... ...
src/main/java/com/diligrp/rider/entity/OrderRefundReason.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/OrderRefundReason.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 退款原因配置表
  8 + */
  9 +@Data
  10 +@TableName("orders_refund_reason")
  11 +public class OrderRefundReason {
  12 +
  13 + @TableId(type = IdType.AUTO)
  14 + private Long id;
  15 +
  16 + /** 原因描述 */
  17 + private String name;
  18 +
  19 + /** 适用角色:1=用户申请 2=骑手申请 */
  20 + private Integer role;
  21 +
  22 + private Integer listOrder;
  23 +}
... ...
src/main/java/com/diligrp/rider/entity/OrderRefundRecord.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/OrderRefundRecord.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 退款申请记录表
  10 + */
  11 +@Data
  12 +@TableName("orders_refund_record")
  13 +public class OrderRefundRecord {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 订单ID */
  19 + private Long oid;
  20 +
  21 + /** 订单号 */
  22 + private String orderNo;
  23 +
  24 + /** 申请人ID */
  25 + private Long uid;
  26 +
  27 + /** 申请角色:1=用户 2=骑手 */
  28 + private Integer role;
  29 +
  30 + /** 退款原因ID */
  31 + private Long reasonId;
  32 +
  33 + /** 退款原因描述 */
  34 + private String reason;
  35 +
  36 + /** 退款金额 */
  37 + private BigDecimal money;
  38 +
  39 + /** 处理状态:0=待处理 1=通过 2=拒绝 */
  40 + private Integer status;
  41 +
  42 + /** 处理备注 */
  43 + private String remark;
  44 +
  45 + private Long addTime;
  46 + private Long handleTime;
  47 +}
... ...
src/main/java/com/diligrp/rider/entity/Orders.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/Orders.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 订单主表
  10 + */
  11 +@Data
  12 +@TableName("orders")
  13 +public class Orders {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 订单号 */
  19 + private String orderNo;
  20 +
  21 + /** 用户ID */
  22 + private Long uid;
  23 +
  24 + /** 骑手ID */
  25 + private Long riderId;
  26 +
  27 + /** 原始骑手ID(转单前) */
  28 + private Long oldRiderId;
  29 +
  30 + /** 城市ID */
  31 + private Long cityId;
  32 +
  33 + /** 订单类型:6=外卖配送 */
  34 + private Integer type;
  35 +
  36 + /**
  37 + * 状态:1=待支付 2=已支付 3=已接单 4=服务中
  38 + * 6=已完成 7=退款申请 8=退款成功 9=退款拒绝 10=已取消
  39 + */
  40 + private Integer status;
  41 +
  42 + /** 支付类型:1=支付宝 2=微信 */
  43 + private Integer payType;
  44 +
  45 + /** 订单金额 */
  46 + private BigDecimal money;
  47 +
  48 + /** 配送费 */
  49 + private BigDecimal moneyDelivery;
  50 +
  51 + /** 总金额 */
  52 + private BigDecimal moneyTotal;
  53 +
  54 + /** 骑手收入 */
  55 + private BigDecimal riderIncome;
  56 +
  57 + /** 站点收入 */
  58 + private BigDecimal substationIncome;
  59 +
  60 + /** 结算状态:0=未结算 1=待结算 2=已结算 */
  61 + private Integer isIncome;
  62 +
  63 + /** 转单状态:0=未转单 1=通过 2=申请中 3=拒绝 */
  64 + private Integer isTrans;
  65 +
  66 + /** 完成码(用户给骑手输入确认完成) */
  67 + private String code;
  68 +
  69 + /** 起点名称 */
  70 + private String fName;
  71 +
  72 + /** 起点地址 */
  73 + private String fAddr;
  74 +
  75 + /** 起点经度 */
  76 + private String fLng;
  77 +
  78 + /** 起点纬度 */
  79 + private String fLat;
  80 +
  81 + /** 终点名称 */
  82 + private String tName;
  83 +
  84 + /** 终点地址 */
  85 + private String tAddr;
  86 +
  87 + /** 终点经度 */
  88 + private String tLng;
  89 +
  90 + /** 终点纬度 */
  91 + private String tLat;
  92 +
  93 + /** 收件人姓名 */
  94 + private String recipName;
  95 +
  96 + /** 收件人电话 */
  97 + private String recipPhone;
  98 +
  99 + /** 附加信息JSON */
  100 + private String extra;
  101 +
  102 + /** 取件照片JSON数组 */
  103 + private String thumbs;
  104 +
  105 + /** 关联店铺订单ID */
  106 + private Long storeOid;
  107 +
  108 + /** 外部业务系统订单号(接入方传入,用于回调对账) */
  109 + private String outOrderNo;
  110 +
  111 + /** 接入方 AppKey */
  112 + private String appKey;
  113 +
  114 + /** 状态变更回调地址(接入方提供,覆盖应用级 webhookUrl) */
  115 + private String callbackUrl;
  116 +
  117 + /**
  118 + * 货物清单快照(JSON数组,接入方推单时传入,不耦合业务商品表)
  119 + * 格式:[{"name":"宫保鸡丁","quantity":2,"spec":"大份","remark":""}]
  120 + */
  121 + private String itemsJson;
  122 +
  123 + /** 货物整单备注(如:放门口、不要辣) */
  124 + private String itemRemark;
  125 +
  126 + /** 关联外部门店ID(接入方在平台注册的门店) */
  127 + private Long extStoreId;
  128 +
  129 + /** 是否删除 */
  130 + @TableLogic
  131 + private Integer isDel;
  132 +
  133 + /** 下单时间 */
  134 + private Long addTime;
  135 +
  136 + /** 支付时间 */
  137 + private Long payTime;
  138 +
  139 + /** 接单时间 */
  140 + private Long grapTime;
  141 +
  142 + /** 取件时间 */
  143 + private Long pickTime;
  144 +
  145 + /** 完成时间 */
  146 + private Long completeTime;
  147 +
  148 + /** 转单时间 */
  149 + private Long transTime;
  150 +}
... ...
src/main/java/com/diligrp/rider/entity/Rider.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/Rider.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +import java.time.LocalDateTime;
  8 +
  9 +/**
  10 + * 骑手信息表
  11 + */
  12 +@Data
  13 +@TableName("rider")
  14 +public class Rider {
  15 +
  16 + @TableId(type = IdType.AUTO)
  17 + private Long id;
  18 +
  19 + /** 手机号 */
  20 + private String mobile;
  21 +
  22 + /** 登录名 */
  23 + private String userLogin;
  24 +
  25 + /** 昵称 */
  26 + private String userNickname;
  27 +
  28 + /** 密码(加密) */
  29 + private String userPass;
  30 +
  31 + /** 头像 */
  32 + private String avatar;
  33 +
  34 + /** 头像缩略图 */
  35 + private String avatarThumb;
  36 +
  37 + /** 城市ID */
  38 + private Long cityId;
  39 +
  40 + /** 等级ID */
  41 + private Long levelId;
  42 +
  43 + /** 等级名称(非表字段) */
  44 + @TableField(exist = false)
  45 + private String levelName;
  46 +
  47 + /** 类型:1=兼职 2=全职 */
  48 + private Integer type;
  49 +
  50 + /** 审核状态:0=拒绝 1=通过 2=待审核 */
  51 + private Integer userStatus;
  52 +
  53 + /** 账号状态:0=禁用 1=正常 */
  54 + private Integer status;
  55 +
  56 + /** 余额(兼职骑手用) */
  57 + private BigDecimal balance;
  58 +
  59 + /** 是否休息:0=否 1=是 */
  60 + private Integer isRest;
  61 +
  62 + /** 身份证号 */
  63 + private String idNo;
  64 +
  65 + /** 手持身份证照片 */
  66 + private String thumb;
  67 +
  68 + @TableField(fill = FieldFill.INSERT)
  69 + private Long createTime;
  70 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderBalance.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/RiderBalance.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 骑手余额流水表
  10 + */
  11 +@Data
  12 +@TableName("rider_balance")
  13 +public class RiderBalance {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 骑手ID */
  19 + private Long uid;
  20 +
  21 + /** 变动类型:1=收入 2=提现 */
  22 + private Integer type;
  23 +
  24 + /** 关联动作(如:order_complete) */
  25 + private String action;
  26 +
  27 + /** 关联ID(如订单ID) */
  28 + private Long actionId;
  29 +
  30 + /** 订单号 */
  31 + private String orderNo;
  32 +
  33 + /** 变动金额 */
  34 + private BigDecimal nums;
  35 +
  36 + /** 变动后余额 */
  37 + private BigDecimal total;
  38 +
  39 + private Long addTime;
  40 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderEvaluate.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/RiderEvaluate.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 骑手评价表(用户对骑手的配送评价)
  8 + */
  9 +@Data
  10 +@TableName("rider_evaluate")
  11 +public class RiderEvaluate {
  12 +
  13 + @TableId(type = IdType.AUTO)
  14 + private Long id;
  15 +
  16 + /** 用户ID(评价人) */
  17 + private Long uid;
  18 +
  19 + /** 订单ID */
  20 + private Long oid;
  21 +
  22 + /** 骑手ID */
  23 + private Long rid;
  24 +
  25 + /** 评价内容 */
  26 + private String content;
  27 +
  28 + /** 星级:1-5 */
  29 + private Integer star;
  30 +
  31 + /** 城市ID */
  32 + private Long cityId;
  33 +
  34 + private Long addTime;
  35 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderLevel.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/RiderLevel.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 骑手等级配置表
  10 + */
  11 +@Data
  12 +@TableName("rider_level")
  13 +public class RiderLevel {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 城市ID */
  19 + private Long cityId;
  20 +
  21 + /** 等级编号 */
  22 + private Integer levelId;
  23 +
  24 + /** 等级名称 */
  25 + private String name;
  26 +
  27 + /** 是否默认等级 */
  28 + private Integer isDefault;
  29 +
  30 + /** 转单次数上限 */
  31 + private Integer transNums;
  32 +
  33 + /**
  34 + * 跑腿类收入模式:1=固定金额 2=按比例 3=按距离
  35 + */
  36 + private Integer runFeeMode;
  37 +
  38 + /** 跑腿固定金额(mode=1) */
  39 + private BigDecimal runFixMoney;
  40 +
  41 + /** 跑腿比例(mode=2,百分比) */
  42 + private BigDecimal runRate;
  43 +
  44 + /** 起始距离(mode=3,米) */
  45 + private Integer distanceBasic;
  46 +
  47 + /** 基础配送费(mode=3) */
  48 + private BigDecimal distanceBasicMoney;
  49 +
  50 + /** 超出每公里费用(mode=3) */
  51 + private BigDecimal distanceMoreMoney;
  52 +
  53 + /** 最高配送费上限(mode=3) */
  54 + private BigDecimal distanceMaxMoney;
  55 +
  56 + /**
  57 + * 办事类收入模式:1=固定金额 2=按比例
  58 + */
  59 + private Integer workFeeMode;
  60 +
  61 + /** 办事固定金额(mode=1) */
  62 + private BigDecimal workFixMoney;
  63 +
  64 + /** 办事比例(mode=2,百分比) */
  65 + private BigDecimal workRate;
  66 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderLocation.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/RiderLocation.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 骑手位置表
  8 + */
  9 +@Data
  10 +@TableName("rider_location")
  11 +public class RiderLocation {
  12 +
  13 + @TableId(type = IdType.AUTO)
  14 + private Long id;
  15 +
  16 + /** 骑手ID */
  17 + private Long uid;
  18 +
  19 + /** 经度 */
  20 + private String lng;
  21 +
  22 + /** 纬度 */
  23 + private String lat;
  24 +
  25 + /** 更新时间 */
  26 + private Long updateTime;
  27 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderOrderCount.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/RiderOrderCount.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * 骑手订单统计表
  10 + */
  11 +@Data
  12 +@TableName("rider_order_count")
  13 +public class RiderOrderCount {
  14 +
  15 + @TableId(type = IdType.AUTO)
  16 + private Long id;
  17 +
  18 + /** 骑手ID */
  19 + private Long uid;
  20 +
  21 + /** 统计日期(yyyyMMdd) */
  22 + private Integer countDate;
  23 +
  24 + /** 完成订单数 */
  25 + private Integer orders;
  26 +
  27 + /** 转单数 */
  28 + private Integer transfers;
  29 +
  30 + /** 配送距离(米) */
  31 + private Long distance;
  32 +}
... ...
src/main/java/com/diligrp/rider/entity/RiderOrderRefuse.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/RiderOrderRefuse.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 骑手拒单记录表
  8 + */
  9 +@Data
  10 +@TableName("rider_orders_refuse")
  11 +public class RiderOrderRefuse {
  12 +
  13 + @TableId(type = IdType.AUTO)
  14 + private Long id;
  15 +
  16 + /** 骑手ID */
  17 + private Long riderId;
  18 +
  19 + /** 订单ID */
  20 + private Long oid;
  21 +
  22 + /** 拒单时间 */
  23 + private Long addTime;
  24 +}
... ...
src/main/java/com/diligrp/rider/entity/Substation.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/Substation.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 分站管理员表
  8 + * 每个城市对应一个分站管理员,负责该城市骑手/订单日常运营
  9 + */
  10 +@Data
  11 +@TableName("substation")
  12 +public class Substation {
  13 +
  14 + @TableId(type = IdType.AUTO)
  15 + private Long id;
  16 +
  17 + /** 管理的城市ID */
  18 + private Long cityId;
  19 +
  20 + /** 登录账号 */
  21 + private String userLogin;
  22 +
  23 + /** 昵称 */
  24 + private String userNickname;
  25 +
  26 + /** 密码(MD5) */
  27 + private String userPass;
  28 +
  29 + /** 手机号 */
  30 + private String mobile;
  31 +
  32 + /** 头像 */
  33 + private String avatar;
  34 +
  35 + /** 状态:0=禁用 1=正常 */
  36 + private Integer userStatus;
  37 +
  38 + private Long createTime;
  39 +}
... ...
src/main/java/com/diligrp/rider/entity/WebhookLog.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/entity/WebhookLog.java
  1 +package com.diligrp.rider.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.*;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * Webhook 推送日志表
  8 + * 记录每次回调的发送结果,便于排查问题和重试
  9 + */
  10 +@Data
  11 +@TableName("webhook_log")
  12 +public class WebhookLog {
  13 +
  14 + @TableId(type = IdType.AUTO)
  15 + private Long id;
  16 +
  17 + /** 应用ID */
  18 + private Long appId;
  19 +
  20 + /** 事件类型,如 order.completed */
  21 + private String event;
  22 +
  23 + /** 推送的业务ID(如订单ID) */
  24 + private Long bizId;
  25 +
  26 + /** 推送URL */
  27 + private String url;
  28 +
  29 + /** 推送内容(JSON) */
  30 + private String payload;
  31 +
  32 + /** HTTP响应码 */
  33 + private Integer responseCode;
  34 +
  35 + /** 响应内容(截取前500字) */
  36 + private String responseBody;
  37 +
  38 + /** 状态:0=失败 1=成功 */
  39 + private Integer status;
  40 +
  41 + /** 重试次数 */
  42 + private Integer retryCount;
  43 +
  44 + private Long createTime;
  45 +}
... ...
src/main/java/com/diligrp/rider/mapper/AdminUserMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/AdminUserMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.AdminUser;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface AdminUserMapper extends BaseMapper<AdminUser> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/CityMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/CityMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.City;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface CityMapper extends BaseMapper<City> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanDimensionMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanDimensionMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.DeliveryFeePlanDimension;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface DeliveryFeePlanDimensionMapper extends BaseMapper<DeliveryFeePlanDimension> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanDistanceStepMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanDistanceStepMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.DeliveryFeePlanDistanceStep;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface DeliveryFeePlanDistanceStepMapper extends BaseMapper<DeliveryFeePlanDistanceStep> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.DeliveryFeePlan;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface DeliveryFeePlanMapper extends BaseMapper<DeliveryFeePlan> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanPieceRuleMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanPieceRuleMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.DeliveryFeePlanPieceRule;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface DeliveryFeePlanPieceRuleMapper extends BaseMapper<DeliveryFeePlanPieceRule> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanTimeRuleMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/DeliveryFeePlanTimeRuleMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.DeliveryFeePlanTimeRule;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface DeliveryFeePlanTimeRuleMapper extends BaseMapper<DeliveryFeePlanTimeRule> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/ExtStoreMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/ExtStoreMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.ExtStore;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface ExtStoreMapper extends BaseMapper<ExtStore> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/MerchantEnterMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/MerchantEnterMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.MerchantEnter;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface MerchantEnterMapper extends BaseMapper<MerchantEnter> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/MerchantStoreMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/MerchantStoreMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.MerchantStore;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface MerchantStoreMapper extends BaseMapper<MerchantStore> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/MerchantUsersMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/MerchantUsersMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.MerchantUsers;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface MerchantUsersMapper extends BaseMapper<MerchantUsers> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/OpenAppMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/OpenAppMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.OpenApp;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface OpenAppMapper extends BaseMapper<OpenApp> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/OrderRefundReasonMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/OrderRefundReasonMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.OrderRefundReason;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface OrderRefundReasonMapper extends BaseMapper<OrderRefundReason> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/OrderRefundRecordMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/OrderRefundRecordMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.OrderRefundRecord;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface OrderRefundRecordMapper extends BaseMapper<OrderRefundRecord> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/OrdersMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/OrdersMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.Orders;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +import org.apache.ibatis.annotations.Param;
  7 +
  8 +@Mapper
  9 +public interface OrdersMapper extends BaseMapper<Orders> {
  10 +
  11 + /** 查询骑手今日转单次数 */
  12 + int countTodayTrans(@Param("riderId") Long riderId, @Param("todayStart") long todayStart);
  13 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderBalanceMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/RiderBalanceMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderBalance;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface RiderBalanceMapper extends BaseMapper<RiderBalance> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderEvaluateMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/RiderEvaluateMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderEvaluate;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface RiderEvaluateMapper extends BaseMapper<RiderEvaluate> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderLevelMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/RiderLevelMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderLevel;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface RiderLevelMapper extends BaseMapper<RiderLevel> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderLocationMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/RiderLocationMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderLocation;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface RiderLocationMapper extends BaseMapper<RiderLocation> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/RiderMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.Rider;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface RiderMapper extends BaseMapper<Rider> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderOrderCountMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/RiderOrderCountMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderOrderCount;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +import org.apache.ibatis.annotations.Param;
  7 +
  8 +@Mapper
  9 +public interface RiderOrderCountMapper extends BaseMapper<RiderOrderCount> {
  10 +
  11 + /** 累加骑手订单统计 */
  12 + void upsertCount(@Param("uid") Long uid, @Param("countDate") int countDate,
  13 + @Param("orders") int orders, @Param("distance") long distance,
  14 + @Param("transfers") int transfers);
  15 +}
... ...
src/main/java/com/diligrp/rider/mapper/RiderOrderRefuseMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/RiderOrderRefuseMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.RiderOrderRefuse;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +import org.apache.ibatis.annotations.Param;
  7 +
  8 +import java.util.List;
  9 +
  10 +@Mapper
  11 +public interface RiderOrderRefuseMapper extends BaseMapper<RiderOrderRefuse> {
  12 +
  13 + /** 查询骑手已拒绝的订单ID列表 */
  14 + List<Long> selectRefuseOrderIds(@Param("riderId") Long riderId);
  15 +}
... ...
src/main/java/com/diligrp/rider/mapper/SubstationMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/SubstationMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.Substation;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface SubstationMapper extends BaseMapper<Substation> {
  9 +}
... ...
src/main/java/com/diligrp/rider/mapper/WebhookLogMapper.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/mapper/WebhookLogMapper.java
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.WebhookLog;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface WebhookLogMapper extends BaseMapper<WebhookLog> {
  9 +}
... ...
src/main/java/com/diligrp/rider/service/AdminRiderLevelService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/AdminRiderLevelService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.AdminRiderLevelSaveDTO;
  4 +import com.diligrp.rider.entity.RiderLevel;
  5 +
  6 +import java.util.List;
  7 +
  8 +public interface AdminRiderLevelService {
  9 + List<RiderLevel> list(Long cityId);
  10 +
  11 + void add(AdminRiderLevelSaveDTO dto, Long cityId);
  12 +
  13 + void edit(AdminRiderLevelSaveDTO dto, Long cityId);
  14 +
  15 + void setDefault(Long id, Long cityId);
  16 +
  17 + void delete(Long id, Long cityId);
  18 +}
... ...
src/main/java/com/diligrp/rider/service/AdminRiderService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/AdminRiderService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.AdminRiderAddDTO;
  4 +import com.diligrp.rider.entity.Rider;
  5 +
  6 +import java.util.List;
  7 +
  8 +/** 后台管理:骑手管理 */
  9 +public interface AdminRiderService {
  10 + /** 新增骑手 */
  11 + void add(AdminRiderAddDTO dto, Long cityId);
  12 + /** 骑手列表 */
  13 + List<Rider> list(String keyword, Integer userStatus, Long cityId);
  14 + /** 指派候选骑手列表 */
  15 + List<Rider> designateCandidates(Long orderId, Long cityId);
  16 + /** 审核骑手(通过/拒绝) */
  17 + void setStatus(Long riderId, int status);
  18 + /** 设置骑手等级,为空则使用默认等级 */
  19 + void setLevel(Long riderId, Long levelId, Long cityId);
  20 + /** 启用/禁用骑手账号 */
  21 + void setEnableStatus(Long riderId, int status);
  22 + /** 切换全职/兼职 */
  23 + void setType(Long riderId, int type);
  24 + /** 指派骑手接单 */
  25 + void designate(Long orderId, Long riderId);
  26 + /** 处理转单申请(1=通过 3=拒绝) */
  27 + void setTrans(Long orderId, int trans);
  28 +}
... ...
src/main/java/com/diligrp/rider/service/CityService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/CityService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
  4 +import com.diligrp.rider.entity.City;
  5 +import com.diligrp.rider.vo.CityVO;
  6 +
  7 +import java.util.List;
  8 +
  9 +public interface CityService {
  10 + /** 获取两级城市树(省→市) */
  11 + List<CityVO> getTree();
  12 + /** 获取已开通城市列表(市级) */
  13 + List<CityVO> getOpenList();
  14 + /** 新增城市 */
  15 + void add(City city);
  16 + /** 编辑城市基础信息 */
  17 + void edit(City city);
  18 + /** 设置开通/关闭状态 */
  19 + void setStatus(Long cityId, int status);
  20 + /** 获取某城市配送费配置 */
  21 + DeliveryPricingConfigDTO getConfig(Long cityId);
  22 + /** 根据ID获取城市 */
  23 + City getById(Long cityId);
  24 +}
... ...
src/main/java/com/diligrp/rider/service/DeliveryFeePlanService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/DeliveryFeePlanService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.DeliveryFeePlanPreviewDTO;
  4 +import com.diligrp.rider.dto.DeliveryFeePlanSaveDTO;
  5 +import com.diligrp.rider.vo.DeliveryFeePlanDetailVO;
  6 +import com.diligrp.rider.vo.DeliveryFeePlanVO;
  7 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  8 +
  9 +import java.util.List;
  10 +
  11 +public interface DeliveryFeePlanService {
  12 +
  13 + List<DeliveryFeePlanVO> listPlans(Long cityId);
  14 +
  15 + DeliveryFeePlanDetailVO getPlanDetail(Long cityId, Long planId);
  16 +
  17 + Long createPlan(Long cityId, DeliveryFeePlanSaveDTO dto);
  18 +
  19 + Long initializeDefaultPlan(Long cityId);
  20 +
  21 + void updatePlan(Long cityId, Long planId, DeliveryFeePlanSaveDTO dto);
  22 +
  23 + Long copyPlan(Long cityId, Long planId);
  24 +
  25 + void deletePlan(Long cityId, Long planId);
  26 +
  27 + void setDefaultPlan(Long cityId, Long planId);
  28 +
  29 + DeliveryFeeResultVO preview(Long cityId, DeliveryFeePlanPreviewDTO dto);
  30 +}
... ...
src/main/java/com/diligrp/rider/service/DeliveryFeeService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/DeliveryFeeService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
  4 +import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
  5 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  6 +
  7 +public interface DeliveryFeeService {
  8 + /**
  9 + * 计算配送费(对内中台核心接口)
  10 + * Helpsend.computed() + City.checkTime() + City.getLength()
  11 + */
  12 + DeliveryFeeResultVO calcFee(DeliveryFeeCalcDTO dto);
  13 +
  14 + /**
  15 + * 使用指定配置试算配送费
  16 + */
  17 + DeliveryFeeResultVO calcFeeByConfig(DeliveryPricingConfigDTO pricingConfig, DeliveryFeeCalcDTO dto);
  18 +
  19 + /**
  20 + * 检查指定城市是否开通某类型服务
  21 + */
  22 + boolean isServiceEnabled(Long cityId, int orderType);
  23 +}
... ...
src/main/java/com/diligrp/rider/service/DeliveryOrderService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/DeliveryOrderService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.DeliveryOrderCreateDTO;
  4 +import com.diligrp.rider.vo.DeliveryOrderCreateVO;
  5 +
  6 +public interface DeliveryOrderService {
  7 + /**
  8 + * 外部系统推单(核心接口)
  9 + * 1. 校验城市是否开通服务
  10 + * 2. 计算配送费
  11 + * 3. 创建配送订单(status=2待接单)
  12 + * 4. 返回订单信息供接入方展示
  13 + */
  14 + DeliveryOrderCreateVO create(String appKey, DeliveryOrderCreateDTO dto);
  15 +
  16 + /** 查询订单状态(供接入方轮询) */
  17 + DeliveryOrderCreateVO queryByOutOrderNo(String appKey, String outOrderNo);
  18 +
  19 + /** 取消订单(仅 status=2 可取消) */
  20 + void cancel(String appKey, String outOrderNo);
  21 +}
... ...
src/main/java/com/diligrp/rider/service/ExtStoreService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/ExtStoreService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.ExtStore;
  4 +
  5 +import java.util.List;
  6 +
  7 +public interface ExtStoreService {
  8 + /** 同步门店(新增或更新,以 appKey+outStoreId 为唯一键) */
  9 + ExtStore syncStore(String appKey, ExtStore store);
  10 + /** 查询某应用下的门店列表 */
  11 + List<ExtStore> listByApp(String appKey);
  12 + /** 获取单个门店(appKey为null时不校验归属) */
  13 + ExtStore getById(Long id, String appKey);
  14 + /** 设置门店状态 */
  15 + void setStatus(Long id, String appKey, int status);
  16 + /** 删除门店 */
  17 + void delete(Long id, String appKey);
  18 + /** 平台管理端:查看所有门店(可按 appKey/cityId 过滤) */
  19 + List<ExtStore> listAll(String appKey, Long cityId, int page);
  20 +}
... ...
src/main/java/com/diligrp/rider/service/MerchantService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/MerchantService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.MerchantStoreDTO;
  4 +import com.diligrp.rider.entity.MerchantStore;
  5 +
  6 +import java.util.List;
  7 +
  8 +public interface MerchantService {
  9 + // ---- 店铺管理 ----
  10 + /** 新增店铺(平台后台直接新建,无需审核) */
  11 + Long addStore(MerchantStoreDTO dto);
  12 + /** 编辑店铺 */
  13 + void editStore(MerchantStoreDTO dto);
  14 + /** 店铺列表 */
  15 + List<MerchantStore> storeList(Long cityId, String keyword, int page);
  16 + /** 获取店铺详情 */
  17 + MerchantStore getStore(Long storeId);
  18 + /** 设置营业/打烊 */
  19 + void setOperatingState(Long storeId, int state);
  20 + /** 设置自动接单 */
  21 + void setAutoOrder(Long storeId, int auto);
  22 + /** 更新免运费和起送金额 */
  23 + void updateFeeConfig(Long storeId, java.math.BigDecimal freeShipping, java.math.BigDecimal upToSend);
  24 + /** 删除店铺 */
  25 + void delStore(Long storeId);
  26 + /**
  27 + * 外部系统同步门店(新增或更新,以 appKey+outStoreId 为唯一键)
  28 + */
  29 + MerchantStore syncStore(String appKey, MerchantStoreDTO dto);
  30 + /**
  31 + * 根据 appKey + outStoreId 查询门店
  32 + */
  33 + MerchantStore getByOutStoreId(String appKey, String outStoreId);
  34 +}
... ...
src/main/java/com/diligrp/rider/service/OpenAppService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/OpenAppService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.OpenApp;
  4 +
  5 +import java.util.List;
  6 +
  7 +public interface OpenAppService {
  8 + /** 创建应用,自动生成 AppKey/AppSecret,cityId 为必填(租户隔离) */
  9 + OpenApp create(String appName, Long cityId, Long storeId, String webhookUrl, String webhookEvents, String remark);
  10 + /** 列表 */
  11 + List<OpenApp> list(int page);
  12 + /** 重置 AppSecret */
  13 + String resetSecret(Long appId);
  14 + /** 启用/禁用 */
  15 + void setStatus(Long appId, int status);
  16 + /** 更新 Webhook 配置 */
  17 + void updateWebhook(Long appId, String webhookUrl, String webhookEvents);
  18 + /** 根据 AppKey 获取应用(用于签名验证) */
  19 + OpenApp getByAppKey(String appKey);
  20 + /** 验证签名 */
  21 + boolean verifySign(String appKey, String timestamp, String nonce, String sign);
  22 +}
... ...
src/main/java/com/diligrp/rider/service/RefundService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/RefundService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.OrderRefundReason;
  4 +import com.diligrp.rider.entity.OrderRefundRecord;
  5 +
  6 +import java.util.List;
  7 +
  8 +public interface RefundService {
  9 + /** 退款原因列表 */
  10 + List<OrderRefundReason> getReasons(int role);
  11 + /** 用户/骑手申请退款 */
  12 + void applyRefund(Long orderId, Long uid, int role, Long reasonId, String reason);
  13 + /** 分站审核退款:status=1通过 2拒绝 */
  14 + void handleRefund(Long recordId, int status, String remark);
  15 + /** 查询订单退款记录 */
  16 + OrderRefundRecord getByOrderId(Long orderId);
  17 +}
... ...
src/main/java/com/diligrp/rider/service/RiderAuthService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/RiderAuthService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.ApplyDTO;
  4 +import com.diligrp.rider.dto.LoginDTO;
  5 +import com.diligrp.rider.vo.RiderVO;
  6 +
  7 +public interface RiderAuthService {
  8 + /** 骑手申请注册 */
  9 + void apply(ApplyDTO dto);
  10 + /** 密码登录 */
  11 + RiderVO loginByPass(LoginDTO dto);
  12 + /** 获取骑手信息 */
  13 + RiderVO getInfo(Long riderId);
  14 + /** 切换休息状态 */
  15 + void toggleRest(Long riderId);
  16 +}
... ...
src/main/java/com/diligrp/rider/service/RiderBalanceService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/RiderBalanceService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.vo.BalanceVO;
  4 +
  5 +import java.math.BigDecimal;
  6 +
  7 +public interface RiderBalanceService {
  8 + /** 查询余额和流水 */
  9 + BalanceVO getBalance(Long riderId, int page);
  10 + /** 今日收入统计(配送收入总和) */
  11 + BigDecimal getTodayIncome(Long riderId);
  12 +}
... ...
src/main/java/com/diligrp/rider/service/RiderEvaluateService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/RiderEvaluateService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.RiderEvaluate;
  4 +
  5 +import java.util.List;
  6 +
  7 +public interface RiderEvaluateService {
  8 + /** 用户对骑手评价(订单完成后) */
  9 + void evaluate(Long uid, Long orderId, int star, String content);
  10 + /** 骑手评价列表 */
  11 + List<RiderEvaluate> getRiderEvaluates(Long riderId, Integer type, int page);
  12 + /** 本月好评数 */
  13 + int getMonthGoodCount(Long riderId);
  14 +}
... ...
src/main/java/com/diligrp/rider/service/RiderLevelService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/RiderLevelService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.RiderLevel;
  4 +
  5 +import java.math.BigDecimal;
  6 +
  7 +public interface RiderLevelService {
  8 + /** 根据骑手获取等级配置 */
  9 + RiderLevel getLevelByRider(Long riderId);
  10 + /**
  11 + * 计算骑手收入
  12 + * @param riderId 骑手ID
  13 + * @param orderType 订单类型
  14 + * @param deliveryFee 配送费
  15 + * @param distance 距离(米)
  16 + */
  17 + BigDecimal calcIncome(Long riderId, int orderType, BigDecimal deliveryFee, long distance);
  18 +}
... ...
src/main/java/com/diligrp/rider/service/RiderLocationService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/RiderLocationService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.LocationDTO;
  4 +import com.diligrp.rider.vo.NearbyRiderVO;
  5 +
  6 +import java.util.List;
  7 +
  8 +public interface RiderLocationService {
  9 + /** 上报骑手位置 */
  10 + void updateLocation(Long riderId, LocationDTO dto);
  11 + /** 获取骑手位置 */
  12 + LocationDTO getLocation(Long riderId);
  13 + /**
  14 + * 获取附近在线骑手列表
  15 + * Location.getNearby()
  16 + * @param cityId 城市ID
  17 + * @param lng 查询点经度
  18 + * @param lat 查询点纬度
  19 + */
  20 + List<NearbyRiderVO> getNearby(Long cityId, String lng, String lat);
  21 +}
... ...
src/main/java/com/diligrp/rider/service/RiderOrderService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/RiderOrderService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.vo.OrderVO;
  4 +import com.diligrp.rider.vo.RiderMonthCountVO;
  5 +import com.diligrp.rider.vo.RiderTodayCountVO;
  6 +
  7 +import java.util.List;
  8 +
  9 +public interface RiderOrderService {
  10 + /** 订单列表:type=1待接单 2待取货 3待完成 */
  11 + List<OrderVO> getList(Long riderId, Long cityId, Integer type, int page);
  12 + /** 订单详情 */
  13 + OrderVO getDetail(Long riderId, Long orderId);
  14 + /** 拒单 */
  15 + void refuse(Long riderId, Long cityId, Long orderId);
  16 + /** 抢单 */
  17 + void grap(Long riderId, Long cityId, Long orderId);
  18 + /** 开始服务(取件),输入完成码 */
  19 + void start(Long riderId, Long orderId, String code);
  20 + /** 完成订单,上传照片 */
  21 + void complete(Long riderId, Long orderId, String thumbsJson);
  22 + /** 骑手申请转单 */
  23 + void applyTrans(Long riderId, Long orderId);
  24 + /** 今日统计 */
  25 + RiderTodayCountVO getTodayCount(Long riderId);
  26 + /** 月度统计 */
  27 + List<RiderMonthCountVO> getMonthCount(Long riderId, int year);
  28 + /**
  29 + * 骑手订单明细列表(历史)
  30 + * type=0全部 1已完成 2已转单
  31 + */
  32 + List<OrderVO> getCountList(Long riderId, int type, int page);
  33 +}
... ...
src/main/java/com/diligrp/rider/service/SubstationService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/SubstationService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.Substation;
  4 +
  5 +import java.util.List;
  6 +
  7 +public interface SubstationService {
  8 + /** 列表 */
  9 + List<Substation> list(String keyword);
  10 + /** 新增分站管理员 */
  11 + void add(Substation substation);
  12 + /** 编辑 */
  13 + void edit(Substation substation);
  14 + /** 禁用 */
  15 + void ban(Long id);
  16 + /** 启用 */
  17 + void cancelBan(Long id);
  18 + /** 删除 */
  19 + void del(Long id);
  20 + /** 根据城市ID获取分站管理员 */
  21 + Substation getByCityId(Long cityId);
  22 + /** 分站管理员修改自己的密码 */
  23 + void changePassword(Long substationId, String oldPassword, String newPassword);
  24 +}
... ...
src/main/java/com/diligrp/rider/service/WebhookService.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/WebhookService.java
  1 +package com.diligrp.rider.service;
  2 +
  3 +/**
  4 + * Webhook 推送服务
  5 + * 订单状态变更时调用此服务通知接入方
  6 + */
  7 +public interface WebhookService {
  8 + /**
  9 + * 发送 Webhook 通知
  10 + * @param event 事件名称,如 order.paid / order.completed / order.cancelled
  11 + * @param bizId 业务ID(订单ID等)
  12 + * @param payload 推送内容(JSON字符串)
  13 + */
  14 + void send(String event, Long bizId, String payload);
  15 +
  16 + /** 重试失败的 Webhook */
  17 + void retry(Long logId);
  18 +}
... ...
src/main/java/com/diligrp/rider/service/impl/AdminAuthServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/AdminAuthServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.exception.BizException;
  5 +import com.diligrp.rider.config.JwtUtil;
  6 +import com.diligrp.rider.dto.AdminLoginDTO;
  7 +import com.diligrp.rider.entity.AdminUser;
  8 +import com.diligrp.rider.entity.Substation;
  9 +import com.diligrp.rider.mapper.AdminUserMapper;
  10 +import com.diligrp.rider.mapper.SubstationMapper;
  11 +import com.diligrp.rider.vo.AdminLoginVO;
  12 +import lombok.RequiredArgsConstructor;
  13 +import org.springframework.stereotype.Service;
  14 +import org.springframework.util.DigestUtils;
  15 +
  16 +import java.nio.charset.StandardCharsets;
  17 +
  18 +@Service
  19 +@RequiredArgsConstructor
  20 +public class AdminAuthServiceImpl {
  21 +
  22 + private final AdminUserMapper adminUserMapper;
  23 + private final SubstationMapper substationMapper;
  24 + private final JwtUtil jwtUtil;
  25 +
  26 + /**
  27 + * 统一登录入口
  28 + * role=admin:超级管理员登录(admin_user表)
  29 + * role=substation:分站管理员登录(substation表)
  30 + */
  31 + public AdminLoginVO login(AdminLoginDTO dto) {
  32 + if ("admin".equals(dto.getRole())) {
  33 + return loginAdmin(dto.getAccount(), dto.getPass());
  34 + }
  35 + return loginSubstation(dto.getAccount(), dto.getPass());
  36 + }
  37 +
  38 + private AdminLoginVO loginAdmin(String account, String pass) {
  39 + AdminUser user = adminUserMapper.selectOne(new LambdaQueryWrapper<AdminUser>()
  40 + .eq(AdminUser::getUserLogin, account).last("LIMIT 1"));
  41 + if (user == null) throw new BizException("账号不存在");
  42 + if (!encryptPass(pass).equals(user.getUserPass())) throw new BizException("密码错误");
  43 + if (user.getUserStatus() == null || user.getUserStatus() == 0) throw new BizException("账号已被禁用");
  44 +
  45 + AdminLoginVO vo = new AdminLoginVO();
  46 + vo.setId(user.getId());
  47 + vo.setUserLogin(user.getUserLogin());
  48 + vo.setUserNickname(user.getUserNickname());
  49 + vo.setRole("admin");
  50 + vo.setToken(jwtUtil.generateAdminToken(user.getId(), "admin"));
  51 + return vo;
  52 + }
  53 +
  54 + private AdminLoginVO loginSubstation(String account, String pass) {
  55 + Substation sub = substationMapper.selectOne(new LambdaQueryWrapper<Substation>()
  56 + .eq(Substation::getUserLogin, account).last("LIMIT 1"));
  57 + if (sub == null) throw new BizException("账号不存在");
  58 + if (!encryptPass(pass).equals(sub.getUserPass())) throw new BizException("密码错误");
  59 + if (sub.getUserStatus() == null || sub.getUserStatus() == 0) throw new BizException("账号已被禁用");
  60 +
  61 + AdminLoginVO vo = new AdminLoginVO();
  62 + vo.setId(sub.getId());
  63 + vo.setUserLogin(sub.getUserLogin());
  64 + vo.setUserNickname(sub.getUserNickname());
  65 + vo.setRole("substation");
  66 + vo.setCityId(sub.getCityId());
  67 + vo.setToken(jwtUtil.generateAdminToken(sub.getId(), "substation"));
  68 + return vo;
  69 + }
  70 +
  71 + private String encryptPass(String pass) {
  72 + return DigestUtils.md5DigestAsHex(pass.getBytes(StandardCharsets.UTF_8));
  73 + }
  74 +}
... ...
src/main/java/com/diligrp/rider/service/impl/AdminRiderLevelServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/AdminRiderLevelServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.dto.AdminRiderLevelSaveDTO;
  7 +import com.diligrp.rider.entity.Rider;
  8 +import com.diligrp.rider.entity.RiderLevel;
  9 +import com.diligrp.rider.mapper.RiderLevelMapper;
  10 +import com.diligrp.rider.mapper.RiderMapper;
  11 +import com.diligrp.rider.service.AdminRiderLevelService;
  12 +import lombok.RequiredArgsConstructor;
  13 +import org.springframework.stereotype.Service;
  14 +
  15 +import java.math.BigDecimal;
  16 +import java.util.List;
  17 +
  18 +@Service
  19 +@RequiredArgsConstructor
  20 +public class AdminRiderLevelServiceImpl implements AdminRiderLevelService {
  21 +
  22 + private final RiderLevelMapper riderLevelMapper;
  23 + private final RiderMapper riderMapper;
  24 +
  25 + @Override
  26 + public List<RiderLevel> list(Long cityId) {
  27 + return riderLevelMapper.selectList(new LambdaQueryWrapper<RiderLevel>()
  28 + .eq(RiderLevel::getCityId, cityId)
  29 + .orderByAsc(RiderLevel::getLevelId)
  30 + .orderByDesc(RiderLevel::getIsDefault));
  31 + }
  32 +
  33 + @Override
  34 + public void add(AdminRiderLevelSaveDTO dto, Long cityId) {
  35 + validateCity(cityId);
  36 + ensureLevelIdUnique(cityId, dto.getLevelId(), null);
  37 +
  38 + RiderLevel level = new RiderLevel();
  39 + fillLevel(level, dto, cityId);
  40 + level.setIsDefault(hasDefault(cityId) ? 0 : 1);
  41 + riderLevelMapper.insert(level);
  42 + }
  43 +
  44 + @Override
  45 + public void edit(AdminRiderLevelSaveDTO dto, Long cityId) {
  46 + validateCity(cityId);
  47 + RiderLevel existing = getOwnedLevel(dto.getId(), cityId);
  48 + ensureLevelIdUnique(cityId, dto.getLevelId(), existing.getId());
  49 + fillLevel(existing, dto, cityId);
  50 + riderLevelMapper.updateById(existing);
  51 + }
  52 +
  53 + @Override
  54 + public void setDefault(Long id, Long cityId) {
  55 + validateCity(cityId);
  56 + RiderLevel existing = getOwnedLevel(id, cityId);
  57 + riderLevelMapper.update(null, new LambdaUpdateWrapper<RiderLevel>()
  58 + .eq(RiderLevel::getCityId, cityId)
  59 + .set(RiderLevel::getIsDefault, 0));
  60 + riderLevelMapper.update(null, new LambdaUpdateWrapper<RiderLevel>()
  61 + .eq(RiderLevel::getId, existing.getId())
  62 + .set(RiderLevel::getIsDefault, 1));
  63 + }
  64 +
  65 + @Override
  66 + public void delete(Long id, Long cityId) {
  67 + validateCity(cityId);
  68 + RiderLevel existing = getOwnedLevel(id, cityId);
  69 + if (existing.getIsDefault() != null && existing.getIsDefault() == 1) {
  70 + throw new BizException("默认等级不能删除");
  71 + }
  72 + Long riderCount = riderMapper.selectCount(new LambdaQueryWrapper<Rider>()
  73 + .eq(Rider::getLevelId, existing.getId()));
  74 + if (riderCount != null && riderCount > 0) {
  75 + throw new BizException("该等级已有骑手使用,不能删除");
  76 + }
  77 + riderLevelMapper.deleteById(existing.getId());
  78 + }
  79 +
  80 + private void fillLevel(RiderLevel level, AdminRiderLevelSaveDTO dto, Long cityId) {
  81 + level.setCityId(cityId);
  82 + level.setLevelId(dto.getLevelId());
  83 + level.setName(dto.getName());
  84 + level.setTransNums(dto.getTransNums());
  85 + level.setRunFeeMode(dto.getRunFeeMode());
  86 + level.setRunFixMoney(nvl(dto.getRunFixMoney()));
  87 + level.setRunRate(nvl(dto.getRunRate()));
  88 + level.setDistanceBasic(dto.getDistanceBasic() == null ? 0 : dto.getDistanceBasic());
  89 + level.setDistanceBasicMoney(nvl(dto.getDistanceBasicMoney()));
  90 + level.setDistanceMoreMoney(nvl(dto.getDistanceMoreMoney()));
  91 + level.setDistanceMaxMoney(nvl(dto.getDistanceMaxMoney()));
  92 + level.setWorkFeeMode(level.getWorkFeeMode() == null ? 1 : level.getWorkFeeMode());
  93 + level.setWorkFixMoney(level.getWorkFixMoney() == null ? BigDecimal.ZERO : level.getWorkFixMoney());
  94 + level.setWorkRate(level.getWorkRate() == null ? BigDecimal.ZERO : level.getWorkRate());
  95 + }
  96 +
  97 + private RiderLevel getOwnedLevel(Long id, Long cityId) {
  98 + RiderLevel level = riderLevelMapper.selectById(id);
  99 + if (level == null) throw new BizException("等级不存在");
  100 + if (!cityId.equals(level.getCityId())) {
  101 + throw new BizException("等级不属于当前城市");
  102 + }
  103 + return level;
  104 + }
  105 +
  106 + private void ensureLevelIdUnique(Long cityId, Integer levelId, Long excludeId) {
  107 + LambdaQueryWrapper<RiderLevel> wrapper = new LambdaQueryWrapper<RiderLevel>()
  108 + .eq(RiderLevel::getCityId, cityId)
  109 + .eq(RiderLevel::getLevelId, levelId);
  110 + if (excludeId != null) {
  111 + wrapper.ne(RiderLevel::getId, excludeId);
  112 + }
  113 + Long exists = riderLevelMapper.selectCount(wrapper);
  114 + if (exists != null && exists > 0) {
  115 + throw new BizException("同城市下等级编号不能重复");
  116 + }
  117 + }
  118 +
  119 + private boolean hasDefault(Long cityId) {
  120 + Long count = riderLevelMapper.selectCount(new LambdaQueryWrapper<RiderLevel>()
  121 + .eq(RiderLevel::getCityId, cityId)
  122 + .eq(RiderLevel::getIsDefault, 1));
  123 + return count != null && count > 0;
  124 + }
  125 +
  126 + private void validateCity(Long cityId) {
  127 + if (cityId == null || cityId < 1) {
  128 + throw new BizException("城市不能为空");
  129 + }
  130 + }
  131 +
  132 + private BigDecimal nvl(BigDecimal value) {
  133 + return value == null ? BigDecimal.ZERO : value;
  134 + }
  135 +}
... ...
src/main/java/com/diligrp/rider/service/impl/AdminRiderServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/AdminRiderServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.dto.AdminRiderAddDTO;
  7 +import com.diligrp.rider.entity.Orders;
  8 +import com.diligrp.rider.entity.Rider;
  9 +import com.diligrp.rider.entity.RiderLevel;
  10 +import com.diligrp.rider.mapper.OrdersMapper;
  11 +import com.diligrp.rider.mapper.RiderBalanceMapper;
  12 +import com.diligrp.rider.mapper.RiderLevelMapper;
  13 +import com.diligrp.rider.mapper.RiderMapper;
  14 +import com.diligrp.rider.entity.*;
  15 +import com.diligrp.rider.mapper.*;
  16 +import com.diligrp.rider.service.AdminRiderService;
  17 +import lombok.RequiredArgsConstructor;
  18 +import org.springframework.stereotype.Service;
  19 +import org.springframework.transaction.annotation.Transactional;
  20 +import org.springframework.util.DigestUtils;
  21 +
  22 +import java.math.BigDecimal;
  23 +import java.nio.charset.StandardCharsets;
  24 +import java.util.HashMap;
  25 +import java.util.List;
  26 +import java.util.Map;
  27 +import java.util.Set;
  28 +import java.util.stream.Collectors;
  29 +
  30 +@Service
  31 +@RequiredArgsConstructor
  32 +public class AdminRiderServiceImpl implements AdminRiderService {
  33 +
  34 + private final RiderMapper riderMapper;
  35 + private final RiderLevelMapper riderLevelMapper;
  36 + private final OrdersMapper ordersMapper;
  37 + private final RiderBalanceMapper balanceMapper;
  38 +
  39 + @Override
  40 + public void add(AdminRiderAddDTO dto, Long cityId) {
  41 + if (cityId == null || cityId < 1) {
  42 + throw new BizException("城市不能为空");
  43 + }
  44 + Long exists = riderMapper.selectCount(new LambdaQueryWrapper<Rider>()
  45 + .eq(Rider::getMobile, dto.getMobile()));
  46 + if (exists > 0) {
  47 + throw new BizException("该手机号已存在");
  48 + }
  49 +
  50 + Rider rider = new Rider();
  51 + rider.setUserNickname(dto.getUserNickname());
  52 + rider.setMobile(dto.getMobile());
  53 + rider.setUserPass(encryptPass(dto.getPassword()));
  54 + rider.setCityId(cityId);
  55 + rider.setUserStatus(1);
  56 + rider.setStatus(1);
  57 + rider.setType(1);
  58 + rider.setIsRest(0);
  59 + rider.setBalance(BigDecimal.ZERO);
  60 + rider.setUserLogin("phone_" + System.currentTimeMillis());
  61 + rider.setCreateTime(System.currentTimeMillis() / 1000);
  62 + riderMapper.insert(rider);
  63 + }
  64 +
  65 + @Override
  66 + public List<Rider> list(String keyword, Integer userStatus, Long cityId) {
  67 + LambdaQueryWrapper<Rider> wrapper = new LambdaQueryWrapper<Rider>()
  68 + .orderByDesc(Rider::getId);
  69 + if (cityId != null) {
  70 + wrapper.eq(Rider::getCityId, cityId);
  71 + }
  72 + if (userStatus != null) {
  73 + wrapper.eq(Rider::getUserStatus, userStatus);
  74 + }
  75 + if (keyword != null && !keyword.isBlank()) {
  76 + wrapper.and(w -> w.like(Rider::getUserNickname, keyword)
  77 + .or()
  78 + .like(Rider::getMobile, keyword));
  79 + }
  80 + List<Rider> riders = riderMapper.selectList(wrapper);
  81 + enrichLevelName(riders);
  82 + return riders;
  83 + }
  84 +
  85 + @Override
  86 + public List<Rider> designateCandidates(Long orderId, Long cityId) {
  87 + Orders order = ordersMapper.selectById(orderId);
  88 + if (order == null) throw new BizException("订单不存在");
  89 + if (cityId != null && !cityId.equals(order.getCityId())) {
  90 + throw new BizException("只能查看当前租户订单的骑手");
  91 + }
  92 +
  93 + LambdaQueryWrapper<Rider> wrapper = new LambdaQueryWrapper<Rider>()
  94 + .eq(Rider::getCityId, order.getCityId())
  95 + .eq(Rider::getUserStatus, 1)
  96 + .eq(Rider::getStatus, 1)
  97 + .orderByAsc(Rider::getIsRest)
  98 + .orderByDesc(Rider::getId);
  99 + if (order.getIsTrans() != null && order.getIsTrans() == 1
  100 + && order.getOldRiderId() != null && order.getOldRiderId() > 0) {
  101 + wrapper.ne(Rider::getId, order.getOldRiderId());
  102 + }
  103 +
  104 + List<Rider> riders = riderMapper.selectList(wrapper);
  105 + enrichLevelName(riders);
  106 + return riders;
  107 + }
  108 +
  109 + @Override
  110 + public void setStatus(Long riderId, int status) {
  111 + Rider rider = riderMapper.selectById(riderId);
  112 + if (rider == null) throw new BizException("骑手不存在");
  113 + riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
  114 + .eq(Rider::getId, riderId)
  115 + .set(Rider::getUserStatus, status));
  116 + }
  117 +
  118 + @Override
  119 + public void setLevel(Long riderId, Long levelId, Long cityId) {
  120 + Rider rider = riderMapper.selectById(riderId);
  121 + if (rider == null) throw new BizException("骑手不存在");
  122 + if (cityId != null && !cityId.equals(rider.getCityId())) {
  123 + throw new BizException("只能操作当前城市骑手");
  124 + }
  125 + if (levelId != null && levelId > 0) {
  126 + RiderLevel level = riderLevelMapper.selectById(levelId);
  127 + if (level == null) throw new BizException("等级不存在");
  128 + if (!rider.getCityId().equals(level.getCityId())) {
  129 + throw new BizException("等级与骑手城市不匹配");
  130 + }
  131 + }
  132 + riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
  133 + .eq(Rider::getId, riderId)
  134 + .set(Rider::getLevelId, levelId != null && levelId > 0 ? levelId : null));
  135 + }
  136 +
  137 + @Override
  138 + public void setEnableStatus(Long riderId, int status) {
  139 + Rider rider = riderMapper.selectById(riderId);
  140 + if (rider == null) throw new BizException("骑手不存在");
  141 + riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
  142 + .eq(Rider::getId, riderId)
  143 + .set(Rider::getStatus, status));
  144 + }
  145 +
  146 + @Override
  147 + @Transactional
  148 + public void setType(Long riderId, int type) {
  149 + Rider rider = riderMapper.selectById(riderId);
  150 + if (rider == null) throw new BizException("骑手不存在");
  151 + if (type == 2) {
  152 + if (rider.getBalance() != null && rider.getBalance().compareTo(BigDecimal.ZERO) > 0) {
  153 + throw new BizException("变更为全职前要保证余额为0");
  154 + }
  155 + }
  156 + LambdaUpdateWrapper<Rider> wrapper = new LambdaUpdateWrapper<Rider>()
  157 + .eq(Rider::getId, riderId)
  158 + .set(Rider::getType, type);
  159 + if (type == 1) {
  160 + wrapper.set(Rider::getBalance, BigDecimal.ZERO);
  161 + }
  162 + riderMapper.update(null, wrapper);
  163 + }
  164 +
  165 + @Override
  166 + @Transactional
  167 + public void designate(Long orderId, Long riderId) {
  168 + Orders order = ordersMapper.selectById(orderId);
  169 + if (order == null) throw new BizException("订单不存在");
  170 + if (order.getStatus() == 1) throw new BizException("订单未支付,无法指派");
  171 + if (order.getStatus() == 10) throw new BizException("订单已取消,无法指派");
  172 + if (order.getStatus() != 2) throw new BizException("订单已服务中,无法指派");
  173 + if (order.getIsTrans() == 1 && riderId.equals(order.getOldRiderId())) {
  174 + throw new BizException("此订单为该骑手转单订单,无法指派给该骑手");
  175 + }
  176 +
  177 + long now = System.currentTimeMillis() / 1000;
  178 + LambdaUpdateWrapper<Orders> wrapper = new LambdaUpdateWrapper<Orders>()
  179 + .eq(Orders::getId, orderId)
  180 + .eq(Orders::getStatus, 2)
  181 + .eq(Orders::getRiderId, 0)
  182 + .set(Orders::getRiderId, riderId)
  183 + .set(Orders::getStatus, 3)
  184 + .set(Orders::getGrapTime, now);
  185 + if (order.getOldRiderId() == null || order.getOldRiderId() == 0) {
  186 + wrapper.set(Orders::getOldRiderId, riderId);
  187 + }
  188 + int updated = ordersMapper.update(null, wrapper);
  189 + if (updated == 0) throw new BizException("指派失败,请重试");
  190 + }
  191 +
  192 + @Override
  193 + @Transactional
  194 + public void setTrans(Long orderId, int trans) {
  195 + Orders order = ordersMapper.selectById(orderId);
  196 + if (order == null) throw new BizException("订单不存在");
  197 + if (order.getStatus() != 4) throw new BizException("订单状态错误,无法操作");
  198 + if (order.getIsTrans() != 2) throw new BizException("订单未申请转单,无法操作");
  199 +
  200 + LambdaUpdateWrapper<Orders> wrapper = new LambdaUpdateWrapper<Orders>()
  201 + .eq(Orders::getId, orderId)
  202 + .eq(Orders::getIsTrans, 2)
  203 + .set(Orders::getIsTrans, trans);
  204 +
  205 + if (trans == 1) {
  206 + wrapper.set(Orders::getStatus, 2)
  207 + .set(Orders::getGrapTime, 0L)
  208 + .set(Orders::getPickTime, 0L)
  209 + .set(Orders::getRiderId, 0L)
  210 + .set(Orders::getIsIncome, 0)
  211 + .set(Orders::getRiderIncome, BigDecimal.ZERO)
  212 + .set(Orders::getSubstationIncome, BigDecimal.ZERO);
  213 + }
  214 + int updated = ordersMapper.update(null, wrapper);
  215 + if (updated == 0) throw new BizException("操作失败,请重试");
  216 + }
  217 +
  218 + private String encryptPass(String pass) {
  219 + return DigestUtils.md5DigestAsHex(pass.getBytes(StandardCharsets.UTF_8));
  220 + }
  221 +
  222 + private void enrichLevelName(List<Rider> riders) {
  223 + if (riders == null || riders.isEmpty()) return;
  224 +
  225 + Set<Long> cityIds = riders.stream()
  226 + .map(Rider::getCityId)
  227 + .filter(id -> id != null && id > 0)
  228 + .collect(Collectors.toSet());
  229 + if (cityIds.isEmpty()) return;
  230 +
  231 + List<RiderLevel> levels = riderLevelMapper.selectList(new LambdaQueryWrapper<RiderLevel>()
  232 + .in(RiderLevel::getCityId, cityIds));
  233 + Map<Long, String> levelNameMap = new HashMap<>();
  234 + Map<Long, String> defaultLevelNameMap = new HashMap<>();
  235 + for (RiderLevel level : levels) {
  236 + levelNameMap.put(level.getId(), level.getName());
  237 + if (level.getIsDefault() != null && level.getIsDefault() == 1) {
  238 + defaultLevelNameMap.put(level.getCityId(), level.getName());
  239 + }
  240 + }
  241 +
  242 + for (Rider rider : riders) {
  243 + if (rider.getLevelId() != null && rider.getLevelId() > 0) {
  244 + rider.setLevelName(levelNameMap.getOrDefault(rider.getLevelId(), "未知等级"));
  245 + } else {
  246 + rider.setLevelName(defaultLevelNameMap.getOrDefault(rider.getCityId(), "默认等级"));
  247 + }
  248 + }
  249 + }
  250 +}
... ...
src/main/java/com/diligrp/rider/service/impl/CityServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/CityServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
  7 +import com.diligrp.rider.dto.DeliveryPricingRuleDTO;
  8 +import com.diligrp.rider.entity.City;
  9 +import com.diligrp.rider.entity.DeliveryFeePlan;
  10 +import com.diligrp.rider.entity.DeliveryFeePlanDimension;
  11 +import com.diligrp.rider.entity.DeliveryFeePlanDistanceStep;
  12 +import com.diligrp.rider.entity.DeliveryFeePlanPieceRule;
  13 +import com.diligrp.rider.entity.DeliveryFeePlanTimeRule;
  14 +import com.diligrp.rider.mapper.CityMapper;
  15 +import com.diligrp.rider.mapper.DeliveryFeePlanDimensionMapper;
  16 +import com.diligrp.rider.mapper.DeliveryFeePlanDistanceStepMapper;
  17 +import com.diligrp.rider.mapper.DeliveryFeePlanMapper;
  18 +import com.diligrp.rider.mapper.DeliveryFeePlanPieceRuleMapper;
  19 +import com.diligrp.rider.mapper.DeliveryFeePlanTimeRuleMapper;
  20 +import com.diligrp.rider.service.CityService;
  21 +import com.diligrp.rider.vo.CityVO;
  22 +import lombok.RequiredArgsConstructor;
  23 +import org.springframework.stereotype.Service;
  24 +
  25 +import java.math.BigDecimal;
  26 +import java.util.Collections;
  27 +import java.util.HashMap;
  28 +import java.util.List;
  29 +import java.util.Map;
  30 +import java.util.stream.Collectors;
  31 +
  32 +@Service
  33 +@RequiredArgsConstructor
  34 +public class CityServiceImpl implements CityService {
  35 +
  36 + private final CityMapper cityMapper;
  37 + private final DeliveryFeePlanMapper deliveryFeePlanMapper;
  38 + private final DeliveryFeePlanDimensionMapper deliveryFeePlanDimensionMapper;
  39 + private final DeliveryFeePlanDistanceStepMapper deliveryFeePlanDistanceStepMapper;
  40 + private final DeliveryFeePlanPieceRuleMapper deliveryFeePlanPieceRuleMapper;
  41 + private final DeliveryFeePlanTimeRuleMapper deliveryFeePlanTimeRuleMapper;
  42 + @Override
  43 + public List<CityVO> getTree() {
  44 + List<City> all = cityMapper.selectList(new LambdaQueryWrapper<City>().orderByAsc(City::getListOrder));
  45 + return all.stream().map(this::toVO).collect(Collectors.toList());
  46 + }
  47 +
  48 + @Override
  49 + public List<CityVO> getOpenList() {
  50 + return cityMapper.selectList(new LambdaQueryWrapper<City>()
  51 + .eq(City::getStatus, 1)
  52 + .orderByAsc(City::getListOrder))
  53 + .stream().map(this::toVO).collect(Collectors.toList());
  54 + }
  55 +
  56 + @Override
  57 + public void add(City city) {
  58 + if (city.getAreaCode() != null && !city.getAreaCode().isBlank()) {
  59 + Long exists = cityMapper.selectCount(new LambdaQueryWrapper<City>()
  60 + .eq(City::getAreaCode, city.getAreaCode()));
  61 + if (exists > 0) {
  62 + throw new BizException("地区编号已存在");
  63 + }
  64 + }
  65 + cityMapper.insert(city);
  66 + initializeDefaultPlan(city.getId());
  67 + }
  68 +
  69 + @Override
  70 + public void edit(City city) {
  71 + City existing = cityMapper.selectById(city.getId());
  72 + if (existing == null) {
  73 + throw new BizException("城市不存在");
  74 + }
  75 + cityMapper.updateById(city);
  76 + }
  77 +
  78 + @Override
  79 + public void setStatus(Long cityId, int status) {
  80 + cityMapper.update(null, new LambdaUpdateWrapper<City>()
  81 + .eq(City::getId, cityId)
  82 + .set(City::getStatus, status));
  83 + }
  84 +
  85 + @Override
  86 + public DeliveryPricingConfigDTO getConfig(Long cityId) {
  87 + City city = cityMapper.selectById(cityId);
  88 + if (city == null) {
  89 + throw new BizException("城市不存在");
  90 + }
  91 +
  92 + DeliveryFeePlan plan = deliveryFeePlanMapper.selectOne(new LambdaQueryWrapper<DeliveryFeePlan>()
  93 + .eq(DeliveryFeePlan::getCityId, cityId)
  94 + .eq(DeliveryFeePlan::getIsDefault, 1)
  95 + .last("LIMIT 1"));
  96 + if (plan == null) {
  97 + plan = deliveryFeePlanMapper.selectOne(new LambdaQueryWrapper<DeliveryFeePlan>()
  98 + .eq(DeliveryFeePlan::getCityId, cityId)
  99 + .orderByAsc(DeliveryFeePlan::getListOrder)
  100 + .orderByAsc(DeliveryFeePlan::getId)
  101 + .last("LIMIT 1"));
  102 + }
  103 + if (plan == null) {
  104 + return createDefaultConfig();
  105 + }
  106 +
  107 + List<DeliveryFeePlanDimension> dimensions = deliveryFeePlanDimensionMapper.selectList(
  108 + new LambdaQueryWrapper<DeliveryFeePlanDimension>()
  109 + .eq(DeliveryFeePlanDimension::getPlanId, plan.getId()));
  110 + Map<String, DeliveryFeePlanDimension> dimensionMap = new HashMap<>();
  111 + for (DeliveryFeePlanDimension item : dimensions) {
  112 + dimensionMap.put(item.getDimensionType(), item);
  113 + }
  114 +
  115 + List<DeliveryFeePlanDistanceStep> distanceSteps = deliveryFeePlanDistanceStepMapper.selectList(
  116 + new LambdaQueryWrapper<DeliveryFeePlanDistanceStep>()
  117 + .eq(DeliveryFeePlanDistanceStep::getPlanId, plan.getId())
  118 + .orderByAsc(DeliveryFeePlanDistanceStep::getListOrder)
  119 + .orderByAsc(DeliveryFeePlanDistanceStep::getId));
  120 + List<DeliveryFeePlanPieceRule> pieceRules = deliveryFeePlanPieceRuleMapper.selectList(
  121 + new LambdaQueryWrapper<DeliveryFeePlanPieceRule>()
  122 + .eq(DeliveryFeePlanPieceRule::getPlanId, plan.getId())
  123 + .orderByAsc(DeliveryFeePlanPieceRule::getListOrder)
  124 + .orderByAsc(DeliveryFeePlanPieceRule::getId));
  125 + List<DeliveryFeePlanTimeRule> timeRules = deliveryFeePlanTimeRuleMapper.selectList(
  126 + new LambdaQueryWrapper<DeliveryFeePlanTimeRule>()
  127 + .eq(DeliveryFeePlanTimeRule::getPlanId, plan.getId())
  128 + .orderByAsc(DeliveryFeePlanTimeRule::getListOrder)
  129 + .orderByAsc(DeliveryFeePlanTimeRule::getId));
  130 +
  131 + DeliveryPricingRuleDTO type6 = new DeliveryPricingRuleDTO();
  132 + type6.setMinFee(nvl(plan.getMinFee()));
  133 + type6.setFeeMode(2);
  134 +
  135 + DeliveryFeePlanDimension base = dimensionMap.get("base");
  136 + if (base != null) {
  137 + type6.setBaseSwitch(on(base.getEnabled()));
  138 + type6.setBaseFee(nvl(base.getBaseFee()));
  139 + }
  140 +
  141 + DeliveryFeePlanDimension distance = dimensionMap.get("distance");
  142 + if (distance != null) {
  143 + type6.setDistanceSwitch(on(distance.getEnabled()));
  144 + type6.setDistanceBasic(nvl(distance.getStartDistance()));
  145 + type6.setDistanceBasicMoney(nvl(distance.getStartFee()));
  146 + type6.setDistanceType(2);
  147 + type6.setDistanceSteps(distanceSteps.stream().map(step -> {
  148 + DeliveryPricingRuleDTO.DistanceStepDTO dto = new DeliveryPricingRuleDTO.DistanceStepDTO();
  149 + dto.setEndDistance(nvl(step.getEndDistance()));
  150 + dto.setUnitDistance(nvl(step.getUnitDistance()));
  151 + dto.setUnitFee(nvl(step.getUnitFee()));
  152 + dto.setListOrder(step.getListOrder());
  153 + return dto;
  154 + }).collect(Collectors.toList()));
  155 + if (!distanceSteps.isEmpty()) {
  156 + type6.setDistanceMoreMoney(nvl(distanceSteps.get(distanceSteps.size() - 1).getUnitFee()));
  157 + }
  158 + }
  159 +
  160 + DeliveryFeePlanDimension weight = dimensionMap.get("weight");
  161 + if (weight != null) {
  162 + type6.setWeightSwitch(on(weight.getEnabled()));
  163 + type6.setWeightFirst(nvl(weight.getFirstWeight()));
  164 + type6.setWeightFirstFee(nvl(weight.getFirstFee()));
  165 + type6.setWeightUnitFee(nvl(weight.getUnitWeightFee()));
  166 + type6.setWeightCapFee(nvl(weight.getCapFee()));
  167 + type6.setWeightBasic(nvl(weight.getFirstWeight()));
  168 + type6.setWeightBasicMoney(nvl(weight.getFirstFee()));
  169 + type6.setWeightMoreMoney(nvl(weight.getUnitWeightFee()));
  170 + }
  171 +
  172 + DeliveryFeePlanDimension piece = dimensionMap.get("piece");
  173 + if (piece != null) {
  174 + type6.setPieceSwitch(on(piece.getEnabled()));
  175 + type6.setPieceRules(pieceRules.stream().map(rule -> {
  176 + DeliveryPricingRuleDTO.PieceRuleDTO dto = new DeliveryPricingRuleDTO.PieceRuleDTO();
  177 + dto.setStartPiece(rule.getStartPiece());
  178 + dto.setEndPiece(rule.getEndPiece());
  179 + dto.setFee(nvl(rule.getFee()));
  180 + dto.setListOrder(rule.getListOrder());
  181 + return dto;
  182 + }).collect(Collectors.toList()));
  183 + }
  184 +
  185 + DeliveryFeePlanDimension time = dimensionMap.get("time");
  186 + if (time != null) {
  187 + type6.setTimes(timeRules.stream().map(rule -> {
  188 + DeliveryPricingRuleDTO.TimePeriodDTO dto = new DeliveryPricingRuleDTO.TimePeriodDTO();
  189 + dto.setStart(rule.getStartMinute());
  190 + dto.setEnd(rule.getEndMinute());
  191 + dto.setMoney(nvl(rule.getFee()));
  192 + dto.setIsOpen(on(rule.getEnabled()));
  193 + return dto;
  194 + }).collect(Collectors.toList()));
  195 + }
  196 +
  197 + DeliveryPricingConfigDTO config = createDefaultConfig();
  198 + config.setType6(type6);
  199 + config.setDistanceBasic(nvl(plan.getDistanceBasic()));
  200 + config.setDistanceBasicTime(plan.getDistanceBasicTime() != null ? plan.getDistanceBasicTime() : 30);
  201 + config.setDistanceMoreTime(plan.getDistanceMoreTime() != null ? plan.getDistanceMoreTime() : 10);
  202 + config.setRiderDistance(nvl(plan.getRiderDistance()));
  203 + config.setRiderTime(plan.getRiderTime());
  204 + return config;
  205 + }
  206 +
  207 + @Override
  208 + public City getById(Long cityId) {
  209 + return cityMapper.selectById(cityId);
  210 + }
  211 +
  212 + private CityVO toVO(City c) {
  213 + CityVO vo = new CityVO();
  214 + vo.setId(c.getId());
  215 + vo.setPid(c.getPid());
  216 + vo.setName(c.getName());
  217 + vo.setAreaCode(c.getAreaCode());
  218 + vo.setStatus(c.getStatus());
  219 + vo.setStatusName(c.getStatus() != null && c.getStatus() == 1 ? "已开通" : "未开通");
  220 + vo.setRate(c.getRate());
  221 + vo.setListOrder(c.getListOrder());
  222 + return vo;
  223 + }
  224 +
  225 + private DeliveryPricingConfigDTO createDefaultConfig() {
  226 + DeliveryPricingConfigDTO config = new DeliveryPricingConfigDTO();
  227 + config.setType(Collections.emptyList());
  228 + config.setType6(new DeliveryPricingRuleDTO());
  229 + config.setDistanceBasic(BigDecimal.valueOf(3));
  230 + config.setDistanceBasicTime(30);
  231 + config.setDistanceMoreTime(10);
  232 + config.setRiderDistance(BigDecimal.valueOf(3));
  233 + return config;
  234 + }
  235 +
  236 + private void initializeDefaultPlan(Long cityId) {
  237 + long now = System.currentTimeMillis() / 1000;
  238 + DeliveryFeePlan plan = new DeliveryFeePlan();
  239 + plan.setCityId(cityId);
  240 + plan.setName("默认方案");
  241 + plan.setIsDefault(1);
  242 + plan.setStatus(1);
  243 + plan.setListOrder(0);
  244 + plan.setRemark("系统初始化");
  245 + plan.setMinFee(BigDecimal.ZERO);
  246 + plan.setDistanceBasic(BigDecimal.valueOf(3));
  247 + plan.setDistanceBasicTime(30);
  248 + plan.setDistanceMoreTime(10);
  249 + plan.setRiderDistance(BigDecimal.valueOf(3));
  250 + plan.setRiderTime(0);
  251 + plan.setCreateTime(now);
  252 + plan.setUpdateTime(now);
  253 + deliveryFeePlanMapper.insert(plan);
  254 +
  255 + upsertDimension(plan.getId(), "base", 0, BigDecimal.ZERO, null, null, null, null, null, null, now);
  256 + upsertDimension(plan.getId(), "distance", 1, null, BigDecimal.valueOf(3), BigDecimal.valueOf(4), null, null, null, null, now);
  257 + upsertDimension(plan.getId(), "weight", 1, null, null, null, BigDecimal.valueOf(5), BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.valueOf(30), now);
  258 + upsertDimension(plan.getId(), "piece", 0, null, null, null, null, null, null, null, now);
  259 + upsertDimension(plan.getId(), "time", 0, null, null, null, null, null, null, null, now);
  260 + }
  261 +
  262 + private void upsertDimension(Long planId, String type, Integer enabled, BigDecimal baseFee,
  263 + BigDecimal startDistance, BigDecimal startFee, BigDecimal firstWeight,
  264 + BigDecimal firstFee, BigDecimal unitWeightFee, BigDecimal capFee, long now) {
  265 + DeliveryFeePlanDimension dimension = new DeliveryFeePlanDimension();
  266 + dimension.setPlanId(planId);
  267 + dimension.setDimensionType(type);
  268 + dimension.setEnabled(on(enabled));
  269 + dimension.setBaseFee(nvl(baseFee));
  270 + dimension.setStartDistance(nvl(startDistance));
  271 + dimension.setStartFee(nvl(startFee));
  272 + dimension.setFirstWeight(nvl(firstWeight));
  273 + dimension.setFirstFee(nvl(firstFee));
  274 + dimension.setUnitWeightFee(nvl(unitWeightFee));
  275 + dimension.setCapFee(nvl(capFee));
  276 + dimension.setCreateTime(now);
  277 + dimension.setUpdateTime(now);
  278 + deliveryFeePlanDimensionMapper.insert(dimension);
  279 + }
  280 +
  281 + private BigDecimal nvl(BigDecimal value) {
  282 + return value != null ? value : BigDecimal.ZERO;
  283 + }
  284 +
  285 + private Integer on(Integer value) {
  286 + return value != null && value == 1 ? 1 : 0;
  287 + }
  288 +}
... ...
src/main/java/com/diligrp/rider/service/impl/DeliveryFeePlanServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/DeliveryFeePlanServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.exception.BizException;
  5 +import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
  6 +import com.diligrp.rider.dto.DeliveryPricingRuleDTO;
  7 +import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
  8 +import com.diligrp.rider.dto.DeliveryFeePlanPreviewDTO;
  9 +import com.diligrp.rider.dto.DeliveryFeePlanSaveDTO;
  10 +import com.diligrp.rider.entity.City;
  11 +import com.diligrp.rider.entity.DeliveryFeePlan;
  12 +import com.diligrp.rider.entity.DeliveryFeePlanDimension;
  13 +import com.diligrp.rider.entity.DeliveryFeePlanDistanceStep;
  14 +import com.diligrp.rider.entity.DeliveryFeePlanPieceRule;
  15 +import com.diligrp.rider.entity.DeliveryFeePlanTimeRule;
  16 +import com.diligrp.rider.mapper.CityMapper;
  17 +import com.diligrp.rider.mapper.DeliveryFeePlanDimensionMapper;
  18 +import com.diligrp.rider.mapper.DeliveryFeePlanDistanceStepMapper;
  19 +import com.diligrp.rider.mapper.DeliveryFeePlanMapper;
  20 +import com.diligrp.rider.mapper.DeliveryFeePlanPieceRuleMapper;
  21 +import com.diligrp.rider.mapper.DeliveryFeePlanTimeRuleMapper;
  22 +import com.diligrp.rider.service.DeliveryFeePlanService;
  23 +import com.diligrp.rider.service.DeliveryFeeService;
  24 +import com.diligrp.rider.vo.DeliveryFeePlanDetailVO;
  25 +import com.diligrp.rider.vo.DeliveryFeePlanVO;
  26 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  27 +import lombok.RequiredArgsConstructor;
  28 +import lombok.extern.slf4j.Slf4j;
  29 +import org.springframework.stereotype.Service;
  30 +import org.springframework.transaction.annotation.Transactional;
  31 +
  32 +import java.math.BigDecimal;
  33 +import java.util.ArrayList;
  34 +import java.util.Arrays;
  35 +import java.util.HashMap;
  36 +import java.util.List;
  37 +import java.util.Map;
  38 +import java.util.stream.Collectors;
  39 +
  40 +@Slf4j
  41 +@Service
  42 +@RequiredArgsConstructor
  43 +public class DeliveryFeePlanServiceImpl implements DeliveryFeePlanService {
  44 +
  45 + private final CityMapper cityMapper;
  46 + private final DeliveryFeeService deliveryFeeService;
  47 + private final DeliveryFeePlanMapper deliveryFeePlanMapper;
  48 + private final DeliveryFeePlanDimensionMapper deliveryFeePlanDimensionMapper;
  49 + private final DeliveryFeePlanDistanceStepMapper deliveryFeePlanDistanceStepMapper;
  50 + private final DeliveryFeePlanPieceRuleMapper deliveryFeePlanPieceRuleMapper;
  51 + private final DeliveryFeePlanTimeRuleMapper deliveryFeePlanTimeRuleMapper;
  52 +
  53 + @Override
  54 + public List<DeliveryFeePlanVO> listPlans(Long cityId) {
  55 + ensurePlanTablesReady();
  56 + requireCity(cityId);
  57 + return deliveryFeePlanMapper.selectList(new LambdaQueryWrapper<DeliveryFeePlan>()
  58 + .eq(DeliveryFeePlan::getCityId, cityId)
  59 + .orderByDesc(DeliveryFeePlan::getIsDefault)
  60 + .orderByAsc(DeliveryFeePlan::getListOrder)
  61 + .orderByAsc(DeliveryFeePlan::getId))
  62 + .stream()
  63 + .map(this::toPlanVO)
  64 + .collect(Collectors.toList());
  65 + }
  66 +
  67 + @Override
  68 + public DeliveryFeePlanDetailVO getPlanDetail(Long cityId, Long planId) {
  69 + ensurePlanTablesReady();
  70 + requireCity(cityId);
  71 + DeliveryFeePlan plan = requirePlan(cityId, planId);
  72 + DeliveryFeePlanDetailVO vo = new DeliveryFeePlanDetailVO();
  73 + copyPlanMeta(plan, vo);
  74 + vo.setConfig(buildPlanConfig(plan));
  75 + return vo;
  76 + }
  77 +
  78 + @Override
  79 + @Transactional
  80 + public Long createPlan(Long cityId, DeliveryFeePlanSaveDTO dto) {
  81 + ensurePlanTablesReady();
  82 + requireCity(cityId);
  83 + long now = now();
  84 +
  85 + DeliveryFeePlan plan = new DeliveryFeePlan();
  86 + plan.setCityId(cityId);
  87 + plan.setName(safeName(dto != null ? dto.getName() : null, "新方案"));
  88 + plan.setIsDefault(0);
  89 + plan.setStatus(dto != null && dto.getStatus() != null ? dto.getStatus() : 1);
  90 + plan.setListOrder(dto != null && dto.getListOrder() != null ? dto.getListOrder() : nextListOrder(cityId));
  91 + plan.setRemark(dto != null && dto.getRemark() != null ? dto.getRemark() : "");
  92 + plan.setMinFee(BigDecimal.ZERO);
  93 + plan.setCreateTime(now);
  94 + plan.setUpdateTime(now);
  95 + deliveryFeePlanMapper.insert(plan);
  96 +
  97 + DeliveryPricingConfigDTO config = normalizeConfig(dto != null ? dto.getConfig() : null);
  98 + savePlanConfig(plan, config, now);
  99 + return plan.getId();
  100 + }
  101 +
  102 + @Override
  103 + @Transactional
  104 + public Long initializeDefaultPlan(Long cityId) {
  105 + ensurePlanTablesReady();
  106 + requireCity(cityId);
  107 + DeliveryFeePlan existed = deliveryFeePlanMapper.selectOne(new LambdaQueryWrapper<DeliveryFeePlan>()
  108 + .eq(DeliveryFeePlan::getCityId, cityId)
  109 + .eq(DeliveryFeePlan::getIsDefault, 1)
  110 + .last("LIMIT 1"));
  111 + if (existed != null) {
  112 + return existed.getId();
  113 + }
  114 + long now = now();
  115 + DeliveryFeePlan plan = new DeliveryFeePlan();
  116 + plan.setCityId(cityId);
  117 + plan.setName("默认方案");
  118 + plan.setIsDefault(1);
  119 + plan.setStatus(1);
  120 + plan.setListOrder(0);
  121 + plan.setRemark("系统初始化");
  122 + plan.setMinFee(BigDecimal.ZERO);
  123 + plan.setCreateTime(now);
  124 + plan.setUpdateTime(now);
  125 + deliveryFeePlanMapper.insert(plan);
  126 + savePlanConfig(plan, createDefaultConfig(), now);
  127 + return plan.getId();
  128 + }
  129 +
  130 + @Override
  131 + @Transactional
  132 + public void updatePlan(Long cityId, Long planId, DeliveryFeePlanSaveDTO dto) {
  133 + ensurePlanTablesReady();
  134 + requireCity(cityId);
  135 + DeliveryFeePlan plan = requirePlan(cityId, planId);
  136 + long now = now();
  137 +
  138 + plan.setName(safeName(dto != null ? dto.getName() : null, plan.getName()));
  139 + plan.setStatus(dto != null && dto.getStatus() != null ? dto.getStatus() : plan.getStatus());
  140 + plan.setListOrder(dto != null && dto.getListOrder() != null ? dto.getListOrder() : plan.getListOrder());
  141 + plan.setRemark(dto != null && dto.getRemark() != null ? dto.getRemark() : "");
  142 + plan.setUpdateTime(now);
  143 + deliveryFeePlanMapper.updateById(plan);
  144 +
  145 + DeliveryPricingConfigDTO config = normalizeConfig(dto != null ? dto.getConfig() : null);
  146 + savePlanConfig(plan, config, now);
  147 + }
  148 +
  149 + @Override
  150 + @Transactional
  151 + public Long copyPlan(Long cityId, Long planId) {
  152 + ensurePlanTablesReady();
  153 + requireCity(cityId);
  154 + DeliveryFeePlan source = requirePlan(cityId, planId);
  155 + DeliveryPricingConfigDTO sourceConfig = buildPlanConfig(source);
  156 + long now = now();
  157 +
  158 + DeliveryFeePlan target = new DeliveryFeePlan();
  159 + target.setCityId(cityId);
  160 + target.setName(safeName(source.getName(), "方案") + "副本");
  161 + target.setIsDefault(0);
  162 + target.setStatus(source.getStatus() != null ? source.getStatus() : 1);
  163 + target.setListOrder(nextListOrder(cityId));
  164 + target.setRemark(source.getRemark() != null ? source.getRemark() : "");
  165 + target.setMinFee(source.getMinFee() != null ? source.getMinFee() : BigDecimal.ZERO);
  166 + target.setCreateTime(now);
  167 + target.setUpdateTime(now);
  168 + deliveryFeePlanMapper.insert(target);
  169 +
  170 + savePlanConfig(target, sourceConfig, now);
  171 + return target.getId();
  172 + }
  173 +
  174 + @Override
  175 + @Transactional
  176 + public void deletePlan(Long cityId, Long planId) {
  177 + ensurePlanTablesReady();
  178 + requireCity(cityId);
  179 + DeliveryFeePlan plan = requirePlan(cityId, planId);
  180 + if (plan.getIsDefault() != null && plan.getIsDefault() == 1) {
  181 + throw new BizException("默认方案不允许删除");
  182 + }
  183 + deletePlanChildren(planId);
  184 + deliveryFeePlanMapper.deleteById(planId);
  185 + }
  186 +
  187 + @Override
  188 + @Transactional
  189 + public void setDefaultPlan(Long cityId, Long planId) {
  190 + ensurePlanTablesReady();
  191 + requireCity(cityId);
  192 + DeliveryFeePlan target = requirePlan(cityId, planId);
  193 + if (target.getStatus() == null || target.getStatus() != 1) {
  194 + throw new BizException("禁用方案不能设为默认");
  195 + }
  196 + long now = now();
  197 + List<DeliveryFeePlan> plans = deliveryFeePlanMapper.selectList(new LambdaQueryWrapper<DeliveryFeePlan>()
  198 + .eq(DeliveryFeePlan::getCityId, cityId));
  199 + for (DeliveryFeePlan item : plans) {
  200 + item.setIsDefault(item.getId().equals(planId) ? 1 : 0);
  201 + item.setUpdateTime(now);
  202 + deliveryFeePlanMapper.updateById(item);
  203 + }
  204 + }
  205 +
  206 + @Override
  207 + public DeliveryFeeResultVO preview(Long cityId, DeliveryFeePlanPreviewDTO dto) {
  208 + ensurePlanTablesReady();
  209 + requireCity(cityId);
  210 + DeliveryFeeCalcDTO calc = dto != null ? dto.getCalc() : null;
  211 + if (calc == null) {
  212 + throw new BizException("请填写试算参数");
  213 + }
  214 + calc.setCityId(cityId);
  215 + calc.setOrderType(6);
  216 + DeliveryPricingConfigDTO config = normalizeConfig(dto != null ? dto.getConfig() : null);
  217 + return deliveryFeeService.calcFeeByConfig(config, calc);
  218 + }
  219 +
  220 + private void ensurePlanTablesReady() {
  221 + try {
  222 + deliveryFeePlanMapper.selectCount(new LambdaQueryWrapper<DeliveryFeePlan>().last("LIMIT 1"));
  223 + } catch (Exception e) {
  224 + log.warn("配送费方案表不可用", e);
  225 + throw new BizException("配送费方案表未初始化,请先执行数据库建表脚本");
  226 + }
  227 + }
  228 +
  229 + private City requireCity(Long cityId) {
  230 + City city = cityMapper.selectById(cityId);
  231 + if (city == null) {
  232 + throw new BizException("租户不存在");
  233 + }
  234 + return city;
  235 + }
  236 +
  237 + private DeliveryFeePlan requirePlan(Long cityId, Long planId) {
  238 + DeliveryFeePlan plan = deliveryFeePlanMapper.selectOne(new LambdaQueryWrapper<DeliveryFeePlan>()
  239 + .eq(DeliveryFeePlan::getId, planId)
  240 + .eq(DeliveryFeePlan::getCityId, cityId)
  241 + .last("LIMIT 1"));
  242 + if (plan == null) {
  243 + throw new BizException("配送费方案不存在");
  244 + }
  245 + return plan;
  246 + }
  247 +
  248 + private int nextListOrder(Long cityId) {
  249 + List<DeliveryFeePlan> plans = deliveryFeePlanMapper.selectList(new LambdaQueryWrapper<DeliveryFeePlan>()
  250 + .eq(DeliveryFeePlan::getCityId, cityId)
  251 + .orderByDesc(DeliveryFeePlan::getListOrder)
  252 + .last("LIMIT 1"));
  253 + if (plans.isEmpty() || plans.get(0).getListOrder() == null) {
  254 + return plans.size();
  255 + }
  256 + return plans.get(0).getListOrder() + 1;
  257 + }
  258 +
  259 + private DeliveryFeePlanVO toPlanVO(DeliveryFeePlan plan) {
  260 + DeliveryFeePlanVO vo = new DeliveryFeePlanVO();
  261 + copyPlanMeta(plan, vo);
  262 + return vo;
  263 + }
  264 +
  265 + private void copyPlanMeta(DeliveryFeePlan plan, DeliveryFeePlanVO vo) {
  266 + vo.setId(plan.getId());
  267 + vo.setCityId(plan.getCityId());
  268 + vo.setName(plan.getName());
  269 + vo.setIsDefault(plan.getIsDefault());
  270 + vo.setStatus(plan.getStatus());
  271 + vo.setListOrder(plan.getListOrder());
  272 + vo.setRemark(plan.getRemark());
  273 + vo.setCreateTime(plan.getCreateTime());
  274 + vo.setUpdateTime(plan.getUpdateTime());
  275 + }
  276 +
  277 + private DeliveryPricingConfigDTO buildPlanConfig(DeliveryFeePlan plan) {
  278 + List<DeliveryFeePlanDimension> dimensions = deliveryFeePlanDimensionMapper.selectList(
  279 + new LambdaQueryWrapper<DeliveryFeePlanDimension>()
  280 + .eq(DeliveryFeePlanDimension::getPlanId, plan.getId()));
  281 + Map<String, DeliveryFeePlanDimension> dimensionMap = new HashMap<>();
  282 + for (DeliveryFeePlanDimension item : dimensions) {
  283 + dimensionMap.put(item.getDimensionType(), item);
  284 + }
  285 +
  286 + List<DeliveryFeePlanDistanceStep> distanceSteps = deliveryFeePlanDistanceStepMapper.selectList(
  287 + new LambdaQueryWrapper<DeliveryFeePlanDistanceStep>()
  288 + .eq(DeliveryFeePlanDistanceStep::getPlanId, plan.getId())
  289 + .orderByAsc(DeliveryFeePlanDistanceStep::getListOrder)
  290 + .orderByAsc(DeliveryFeePlanDistanceStep::getId));
  291 + List<DeliveryFeePlanPieceRule> pieceRules = deliveryFeePlanPieceRuleMapper.selectList(
  292 + new LambdaQueryWrapper<DeliveryFeePlanPieceRule>()
  293 + .eq(DeliveryFeePlanPieceRule::getPlanId, plan.getId())
  294 + .orderByAsc(DeliveryFeePlanPieceRule::getListOrder)
  295 + .orderByAsc(DeliveryFeePlanPieceRule::getId));
  296 + List<DeliveryFeePlanTimeRule> timeRules = deliveryFeePlanTimeRuleMapper.selectList(
  297 + new LambdaQueryWrapper<DeliveryFeePlanTimeRule>()
  298 + .eq(DeliveryFeePlanTimeRule::getPlanId, plan.getId())
  299 + .orderByAsc(DeliveryFeePlanTimeRule::getListOrder)
  300 + .orderByAsc(DeliveryFeePlanTimeRule::getId));
  301 +
  302 + DeliveryPricingRuleDTO type6 = createDefaultType6();
  303 + type6.setMinFee(nvl(plan.getMinFee()));
  304 + type6.setFeeMode(2);
  305 +
  306 + DeliveryFeePlanDimension base = dimensionMap.get("base");
  307 + if (base != null) {
  308 + type6.setBaseSwitch(on(base.getEnabled()));
  309 + type6.setBaseFee(nvl(base.getBaseFee()));
  310 + }
  311 +
  312 + DeliveryFeePlanDimension distance = dimensionMap.get("distance");
  313 + if (distance != null) {
  314 + type6.setDistanceSwitch(on(distance.getEnabled()));
  315 + type6.setDistanceBasic(nvl(distance.getStartDistance()));
  316 + type6.setDistanceBasicMoney(nvl(distance.getStartFee()));
  317 + type6.setDistanceType(2);
  318 + type6.setDistanceSteps(distanceSteps.stream().map(step -> {
  319 + DeliveryPricingRuleDTO.DistanceStepDTO item = new DeliveryPricingRuleDTO.DistanceStepDTO();
  320 + item.setEndDistance(nvl(step.getEndDistance()));
  321 + item.setUnitDistance(nvl(step.getUnitDistance()));
  322 + item.setUnitFee(nvl(step.getUnitFee()));
  323 + item.setListOrder(step.getListOrder());
  324 + return item;
  325 + }).collect(Collectors.toList()));
  326 + if (!distanceSteps.isEmpty()) {
  327 + type6.setDistanceMoreMoney(nvl(distanceSteps.get(distanceSteps.size() - 1).getUnitFee()));
  328 + }
  329 + }
  330 +
  331 + DeliveryFeePlanDimension weight = dimensionMap.get("weight");
  332 + if (weight != null) {
  333 + type6.setWeightSwitch(on(weight.getEnabled()));
  334 + type6.setWeightFirst(nvl(weight.getFirstWeight()));
  335 + type6.setWeightFirstFee(nvl(weight.getFirstFee()));
  336 + type6.setWeightUnitFee(nvl(weight.getUnitWeightFee()));
  337 + type6.setWeightCapFee(nvl(weight.getCapFee()));
  338 + type6.setWeightBasic(nvl(weight.getFirstWeight()));
  339 + type6.setWeightBasicMoney(nvl(weight.getFirstFee()));
  340 + type6.setWeightMoreMoney(nvl(weight.getUnitWeightFee()));
  341 + }
  342 +
  343 + DeliveryFeePlanDimension piece = dimensionMap.get("piece");
  344 + if (piece != null) {
  345 + type6.setPieceSwitch(on(piece.getEnabled()));
  346 + type6.setPieceRules(pieceRules.stream().map(rule -> {
  347 + DeliveryPricingRuleDTO.PieceRuleDTO item = new DeliveryPricingRuleDTO.PieceRuleDTO();
  348 + item.setStartPiece(rule.getStartPiece());
  349 + item.setEndPiece(rule.getEndPiece());
  350 + item.setFee(nvl(rule.getFee()));
  351 + item.setListOrder(rule.getListOrder());
  352 + return item;
  353 + }).collect(Collectors.toList()));
  354 + }
  355 +
  356 + DeliveryFeePlanDimension time = dimensionMap.get("time");
  357 + if (time != null) {
  358 + type6.setTimes(timeRules.stream().map(rule -> {
  359 + DeliveryPricingRuleDTO.TimePeriodDTO item = new DeliveryPricingRuleDTO.TimePeriodDTO();
  360 + item.setStart(rule.getStartMinute());
  361 + item.setEnd(rule.getEndMinute());
  362 + item.setMoney(nvl(rule.getFee()));
  363 + item.setIsOpen(on(rule.getEnabled()));
  364 + return item;
  365 + }).collect(Collectors.toList()));
  366 + }
  367 +
  368 + DeliveryPricingConfigDTO config = createDefaultConfig();
  369 + config.setType6(type6);
  370 + config.setDistanceBasic(nvl(plan.getDistanceBasic()));
  371 + config.setDistanceBasicTime(plan.getDistanceBasicTime() != null ? plan.getDistanceBasicTime() : 30);
  372 + config.setDistanceMoreTime(plan.getDistanceMoreTime() != null ? plan.getDistanceMoreTime() : 10);
  373 + config.setRiderDistance(nvl(plan.getRiderDistance()));
  374 + config.setRiderTime(plan.getRiderTime());
  375 + return config;
  376 + }
  377 +
  378 + private void savePlanConfig(DeliveryFeePlan plan, DeliveryPricingConfigDTO config, long now) {
  379 + DeliveryPricingConfigDTO normalized = normalizeConfig(config);
  380 + DeliveryPricingRuleDTO type6 = normalized.getType6();
  381 +
  382 + plan.setMinFee(nvl(type6.getMinFee()));
  383 + plan.setDistanceBasic(nvl(normalized.getDistanceBasic()));
  384 + plan.setDistanceBasicTime(normalized.getDistanceBasicTime() != null ? normalized.getDistanceBasicTime() : 30);
  385 + plan.setDistanceMoreTime(normalized.getDistanceMoreTime() != null ? normalized.getDistanceMoreTime() : 10);
  386 + plan.setRiderDistance(nvl(normalized.getRiderDistance()));
  387 + plan.setRiderTime(normalized.getRiderTime() != null ? normalized.getRiderTime() : 0);
  388 + plan.setUpdateTime(now);
  389 + deliveryFeePlanMapper.updateById(plan);
  390 +
  391 + upsertDimension(plan.getId(), "base", on(type6.getBaseSwitch()), type6.getBaseFee(),
  392 + null, null, null, null, null, null, now);
  393 + upsertDimension(plan.getId(), "distance", on(type6.getDistanceSwitch()), null,
  394 + type6.getDistanceBasic(), type6.getDistanceBasicMoney(), null, null, null, null, now);
  395 + upsertDimension(plan.getId(), "weight", on(type6.getWeightSwitch()), null,
  396 + null, null, type6.getWeightFirst(), type6.getWeightFirstFee(),
  397 + type6.getWeightUnitFee(), type6.getWeightCapFee(), now);
  398 + upsertDimension(plan.getId(), "piece", on(type6.getPieceSwitch()), null,
  399 + null, null, null, null, null, null, now);
  400 + upsertDimension(plan.getId(), "time", type6.getTimes() != null && !type6.getTimes().isEmpty() ? 1 : 0,
  401 + null, null, null, null, null, null, null, now);
  402 +
  403 + deliveryFeePlanDistanceStepMapper.delete(new LambdaQueryWrapper<DeliveryFeePlanDistanceStep>()
  404 + .eq(DeliveryFeePlanDistanceStep::getPlanId, plan.getId()));
  405 + int stepOrder = 0;
  406 + for (DeliveryPricingRuleDTO.DistanceStepDTO item : safeList(type6.getDistanceSteps())) {
  407 + DeliveryFeePlanDistanceStep step = new DeliveryFeePlanDistanceStep();
  408 + step.setPlanId(plan.getId());
  409 + step.setEndDistance(nvl(item.getEndDistance()));
  410 + step.setUnitDistance(nvl(item.getUnitDistance()));
  411 + step.setUnitFee(nvl(item.getUnitFee()));
  412 + step.setListOrder(item.getListOrder() != null ? item.getListOrder() : stepOrder++);
  413 + step.setCreateTime(now);
  414 + step.setUpdateTime(now);
  415 + deliveryFeePlanDistanceStepMapper.insert(step);
  416 + }
  417 +
  418 + deliveryFeePlanPieceRuleMapper.delete(new LambdaQueryWrapper<DeliveryFeePlanPieceRule>()
  419 + .eq(DeliveryFeePlanPieceRule::getPlanId, plan.getId()));
  420 + int pieceOrder = 0;
  421 + for (DeliveryPricingRuleDTO.PieceRuleDTO item : safeList(type6.getPieceRules())) {
  422 + DeliveryFeePlanPieceRule rule = new DeliveryFeePlanPieceRule();
  423 + rule.setPlanId(plan.getId());
  424 + rule.setStartPiece(item.getStartPiece() != null ? item.getStartPiece() : 0);
  425 + rule.setEndPiece(item.getEndPiece() != null ? item.getEndPiece() : 0);
  426 + rule.setFee(nvl(item.getFee()));
  427 + rule.setListOrder(item.getListOrder() != null ? item.getListOrder() : pieceOrder++);
  428 + rule.setCreateTime(now);
  429 + rule.setUpdateTime(now);
  430 + deliveryFeePlanPieceRuleMapper.insert(rule);
  431 + }
  432 +
  433 + deliveryFeePlanTimeRuleMapper.delete(new LambdaQueryWrapper<DeliveryFeePlanTimeRule>()
  434 + .eq(DeliveryFeePlanTimeRule::getPlanId, plan.getId()));
  435 + int timeOrder = 0;
  436 + for (DeliveryPricingRuleDTO.TimePeriodDTO item : safeList(type6.getTimes())) {
  437 + DeliveryFeePlanTimeRule rule = new DeliveryFeePlanTimeRule();
  438 + rule.setPlanId(plan.getId());
  439 + rule.setStartMinute(item.getStart() != null ? item.getStart() : 0);
  440 + rule.setEndMinute(item.getEnd() != null ? item.getEnd() : 0);
  441 + rule.setFee(nvl(item.getMoney()));
  442 + rule.setEnabled(on(item.getIsOpen()));
  443 + rule.setListOrder(timeOrder++);
  444 + rule.setCreateTime(now);
  445 + rule.setUpdateTime(now);
  446 + deliveryFeePlanTimeRuleMapper.insert(rule);
  447 + }
  448 + }
  449 +
  450 + private void deletePlanChildren(Long planId) {
  451 + deliveryFeePlanDimensionMapper.delete(new LambdaQueryWrapper<DeliveryFeePlanDimension>()
  452 + .eq(DeliveryFeePlanDimension::getPlanId, planId));
  453 + deliveryFeePlanDistanceStepMapper.delete(new LambdaQueryWrapper<DeliveryFeePlanDistanceStep>()
  454 + .eq(DeliveryFeePlanDistanceStep::getPlanId, planId));
  455 + deliveryFeePlanPieceRuleMapper.delete(new LambdaQueryWrapper<DeliveryFeePlanPieceRule>()
  456 + .eq(DeliveryFeePlanPieceRule::getPlanId, planId));
  457 + deliveryFeePlanTimeRuleMapper.delete(new LambdaQueryWrapper<DeliveryFeePlanTimeRule>()
  458 + .eq(DeliveryFeePlanTimeRule::getPlanId, planId));
  459 + }
  460 +
  461 + private void upsertDimension(Long planId, String type, Integer enabled, BigDecimal baseFee,
  462 + BigDecimal startDistance, BigDecimal startFee, BigDecimal firstWeight,
  463 + BigDecimal firstFee, BigDecimal unitWeightFee, BigDecimal capFee, long now) {
  464 + DeliveryFeePlanDimension dimension = deliveryFeePlanDimensionMapper.selectOne(
  465 + new LambdaQueryWrapper<DeliveryFeePlanDimension>()
  466 + .eq(DeliveryFeePlanDimension::getPlanId, planId)
  467 + .eq(DeliveryFeePlanDimension::getDimensionType, type)
  468 + .last("LIMIT 1"));
  469 + if (dimension == null) {
  470 + dimension = new DeliveryFeePlanDimension();
  471 + dimension.setPlanId(planId);
  472 + dimension.setDimensionType(type);
  473 + dimension.setCreateTime(now);
  474 + }
  475 + dimension.setEnabled(on(enabled));
  476 + dimension.setBaseFee(nvl(baseFee));
  477 + dimension.setStartDistance(nvl(startDistance));
  478 + dimension.setStartFee(nvl(startFee));
  479 + dimension.setFirstWeight(nvl(firstWeight));
  480 + dimension.setFirstFee(nvl(firstFee));
  481 + dimension.setUnitWeightFee(nvl(unitWeightFee));
  482 + dimension.setCapFee(nvl(capFee));
  483 + dimension.setUpdateTime(now);
  484 + if (dimension.getId() == null) {
  485 + deliveryFeePlanDimensionMapper.insert(dimension);
  486 + } else {
  487 + deliveryFeePlanDimensionMapper.updateById(dimension);
  488 + }
  489 + }
  490 +
  491 + private DeliveryPricingConfigDTO normalizeConfig(DeliveryPricingConfigDTO input) {
  492 + DeliveryPricingConfigDTO config = input != null ? input : createDefaultConfig();
  493 + DeliveryPricingRuleDTO type6 = config.getType6() != null ? config.getType6() : createDefaultType6();
  494 +
  495 + if (type6.getDistanceSteps() == null) {
  496 + type6.setDistanceSteps(new ArrayList<>());
  497 + }
  498 + if (type6.getPieceRules() == null) {
  499 + type6.setPieceRules(new ArrayList<>());
  500 + }
  501 + if (type6.getTimes() == null) {
  502 + type6.setTimes(new ArrayList<>());
  503 + }
  504 +
  505 + config.setType(Arrays.asList(6));
  506 + config.setType6(type6);
  507 + config.setDistanceBasic(config.getDistanceBasic() != null ? config.getDistanceBasic() : BigDecimal.valueOf(3));
  508 + config.setDistanceBasicTime(config.getDistanceBasicTime() != null ? config.getDistanceBasicTime() : 30);
  509 + config.setDistanceMoreTime(config.getDistanceMoreTime() != null ? config.getDistanceMoreTime() : 10);
  510 + config.setRiderDistance(config.getRiderDistance() != null ? config.getRiderDistance() : BigDecimal.valueOf(3));
  511 + config.setRiderTime(config.getRiderTime() != null ? config.getRiderTime() : 0);
  512 + return config;
  513 + }
  514 +
  515 + private DeliveryPricingConfigDTO createDefaultConfig() {
  516 + DeliveryPricingConfigDTO config = new DeliveryPricingConfigDTO();
  517 + config.setType(Arrays.asList(6));
  518 + config.setType6(createDefaultType6());
  519 + config.setDistanceBasic(BigDecimal.valueOf(3));
  520 + config.setDistanceBasicTime(30);
  521 + config.setDistanceMoreTime(10);
  522 + config.setRiderDistance(BigDecimal.valueOf(3));
  523 + return config;
  524 + }
  525 +
  526 + private DeliveryPricingRuleDTO createDefaultType6() {
  527 + DeliveryPricingRuleDTO type6 = new DeliveryPricingRuleDTO();
  528 + type6.setMinFee(BigDecimal.ZERO);
  529 + type6.setBaseSwitch(0);
  530 + type6.setBaseFee(BigDecimal.ZERO);
  531 + type6.setFeeMode(2);
  532 + type6.setFixMoney(BigDecimal.ZERO);
  533 + type6.setDistanceSwitch(1);
  534 + type6.setDistanceBasic(BigDecimal.valueOf(3));
  535 + type6.setDistanceBasicMoney(BigDecimal.valueOf(4));
  536 + type6.setDistanceMoreMoney(BigDecimal.valueOf(1.5));
  537 + type6.setDistanceMode(1);
  538 + type6.setDistanceType(1);
  539 + type6.setDistanceSteps(new ArrayList<>());
  540 + type6.setWeightSwitch(1);
  541 + type6.setWeightFirst(BigDecimal.valueOf(5));
  542 + type6.setWeightFirstFee(BigDecimal.ZERO);
  543 + type6.setWeightUnitFee(BigDecimal.ONE);
  544 + type6.setWeightCapFee(BigDecimal.valueOf(30));
  545 + type6.setWeightBasic(BigDecimal.ZERO);
  546 + type6.setWeightBasicMoney(BigDecimal.ZERO);
  547 + type6.setWeightMoreMoney(BigDecimal.ZERO);
  548 + type6.setWeightType(1);
  549 + type6.setPieceSwitch(0);
  550 + type6.setPieceRules(new ArrayList<>());
  551 + type6.setTimes(new ArrayList<>());
  552 + return type6;
  553 + }
  554 +
  555 + private String safeName(String value, String fallback) {
  556 + if (value == null || value.isBlank()) {
  557 + return fallback;
  558 + }
  559 + return value.trim();
  560 + }
  561 +
  562 + private long now() {
  563 + return System.currentTimeMillis() / 1000;
  564 + }
  565 +
  566 + private BigDecimal nvl(BigDecimal value) {
  567 + return value != null ? value : BigDecimal.ZERO;
  568 + }
  569 +
  570 + private Integer on(Integer value) {
  571 + return value != null && value == 1 ? 1 : 0;
  572 + }
  573 +
  574 + private <T> List<T> safeList(List<T> list) {
  575 + return list != null ? list : new ArrayList<>();
  576 + }
  577 +}
... ...
src/main/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.diligrp.rider.common.exception.BizException;
  4 +import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
  5 +import com.diligrp.rider.dto.DeliveryPricingRuleDTO;
  6 +import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
  7 +import com.diligrp.rider.service.CityService;
  8 +import com.diligrp.rider.service.DeliveryFeeService;
  9 +import com.diligrp.rider.util.GeoUtil;
  10 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  11 +import lombok.RequiredArgsConstructor;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.springframework.stereotype.Service;
  14 +
  15 +import java.math.BigDecimal;
  16 +import java.math.RoundingMode;
  17 +import java.time.Instant;
  18 +import java.time.ZoneId;
  19 +import java.time.ZonedDateTime;
  20 +import java.util.ArrayList;
  21 +import java.util.Comparator;
  22 +import java.util.List;
  23 +
  24 +/**
  25 + * 配送费计算引擎
  26 + * Helpsend.computed() + City.checkTime() + City.getLength()
  27 + */
  28 +@Slf4j
  29 +@Service
  30 +@RequiredArgsConstructor
  31 +public class DeliveryFeeServiceImpl implements DeliveryFeeService {
  32 +
  33 + private final CityService cityService;
  34 +
  35 + @Override
  36 + public DeliveryFeeResultVO calcFee(DeliveryFeeCalcDTO dto) {
  37 + DeliveryPricingConfigDTO pricingConfig = cityService.getConfig(dto.getCityId());
  38 + return calcFeeByConfig(pricingConfig, dto);
  39 + }
  40 +
  41 + @Override
  42 + public DeliveryFeeResultVO calcFeeByConfig(DeliveryPricingConfigDTO pricingConfig, DeliveryFeeCalcDTO dto) {
  43 + if (pricingConfig == null || pricingConfig.getType() == null) {
  44 + throw new BizException("当前城市未开通服务");
  45 + }
  46 +
  47 + int orderType = dto.getOrderType();
  48 + if (!pricingConfig.getType().contains(orderType)) {
  49 + throw new BizException("当前城市未开通该类型服务");
  50 + }
  51 +
  52 + // 获取该订单类型的配送费配置
  53 + DeliveryPricingRuleDTO typeConfig = getTypeConfig(pricingConfig, orderType);
  54 + if (typeConfig == null) {
  55 + throw new BizException("该服务类型未配置收费规则");
  56 + }
  57 +
  58 + DeliveryFeeResultVO result = new DeliveryFeeResultVO();
  59 + result.setMoneyBasic(BigDecimal.ZERO);
  60 + result.setMoneyBasicTxt("");
  61 + result.setMoneyDistance(BigDecimal.ZERO);
  62 + result.setMoneyDistanceTxt("");
  63 + result.setMoneyWeight(BigDecimal.ZERO);
  64 + result.setMoneyWeightTxt("");
  65 + result.setMoneyPiece(BigDecimal.ZERO);
  66 + result.setMoneyPieceTxt("");
  67 + result.setMoneyTime(BigDecimal.ZERO);
  68 + result.setDistance(BigDecimal.ZERO);
  69 + result.setWeight(dto.getWeight());
  70 + result.setPieces(dto.getPieces());
  71 + result.setMinFee(nvl(typeConfig.getMinFee()));
  72 + result.setMinFeeApplied(0);
  73 +
  74 + // --- fee_mode=1:固定费用 ---
  75 + if (typeConfig.getFeeMode() != null && typeConfig.getFeeMode() == 1) {
  76 + BigDecimal fix = typeConfig.getFixMoney() != null ? typeConfig.getFixMoney() : BigDecimal.ZERO;
  77 + result.setMoneyBasic(fix);
  78 + result.setTotalFee(applyMinFee(fix, typeConfig, result));
  79 + result.setEstimatedMinutes(calcEstimatedMinutes(pricingConfig, BigDecimal.ZERO));
  80 + return result;
  81 + }
  82 +
  83 + // --- fee_mode=2:按距离/重量计费 ---
  84 + BigDecimal moneyBasic = BigDecimal.ZERO;
  85 + BigDecimal moneyDistance = BigDecimal.ZERO;
  86 + BigDecimal moneyWeight = BigDecimal.ZERO;
  87 + BigDecimal moneyPiece = BigDecimal.ZERO;
  88 + String moneyBasicTxt = "";
  89 + String moneyDistanceTxt = "";
  90 + String moneyWeightTxt = "";
  91 + String moneyPieceTxt = "";
  92 + BigDecimal distanceKm = BigDecimal.ZERO;
  93 +
  94 + // 基础费
  95 + if (typeConfig.getBaseSwitch() != null && typeConfig.getBaseSwitch() == 1) {
  96 + moneyBasic = moneyBasic.add(nvl(typeConfig.getBaseFee()));
  97 + }
  98 +
  99 + // 距离计费
  100 + if (typeConfig.getDistanceSwitch() != null && typeConfig.getDistanceSwitch() == 1) {
  101 + BigDecimal distanceBasicMoney = nvl(typeConfig.getDistanceBasicMoney());
  102 + moneyBasic = moneyBasic.add(distanceBasicMoney);
  103 + BigDecimal basicKm = nvl(typeConfig.getDistanceBasic());
  104 + moneyBasicTxt = "(" + basicKm.stripTrailingZeros().toPlainString() + "km)";
  105 +
  106 + // 计算实际距离
  107 + distanceKm = calcDistance(typeConfig, dto);
  108 +
  109 + // 超出距离费
  110 + if (distanceKm.compareTo(basicKm) > 0) {
  111 + BigDecimal moreKm = distanceKm.subtract(basicKm);
  112 + if (typeConfig.getDistanceSteps() != null && !typeConfig.getDistanceSteps().isEmpty()) {
  113 + moneyDistance = calcDistanceStepFee(basicKm, distanceKm, typeConfig.getDistanceSteps());
  114 + } else {
  115 + BigDecimal moreMoneyPerKm = nvl(typeConfig.getDistanceMoreMoney());
  116 + BigDecimal moreKmCeil = moreKm.setScale(0, RoundingMode.CEILING);
  117 + moneyDistance = moreKmCeil.multiply(moreMoneyPerKm).setScale(2, RoundingMode.HALF_UP);
  118 + }
  119 + moneyDistanceTxt = "(" + moreKm.setScale(1, RoundingMode.HALF_UP).toPlainString() + "km)";
  120 + }
  121 + }
  122 +
  123 + // 重量计费
  124 + if (typeConfig.getWeightSwitch() != null && typeConfig.getWeightSwitch() == 1) {
  125 + BigDecimal weightBasicMoney = nvl(typeConfig.getWeightFirstFee().compareTo(BigDecimal.ZERO) > 0
  126 + ? typeConfig.getWeightFirstFee() : typeConfig.getWeightBasicMoney());
  127 + moneyBasic = moneyBasic.add(weightBasicMoney);
  128 +
  129 + BigDecimal weight = dto.getWeight() != null ? dto.getWeight() : BigDecimal.ZERO;
  130 + BigDecimal weightAdj = adjustValue(weight, typeConfig.getWeightType());
  131 + BigDecimal basicWeight = nvl(typeConfig.getWeightFirst().compareTo(BigDecimal.ZERO) > 0
  132 + ? typeConfig.getWeightFirst() : typeConfig.getWeightBasic());
  133 + BigDecimal moreMoneyPerKg = nvl(typeConfig.getWeightUnitFee().compareTo(BigDecimal.ZERO) > 0
  134 + ? typeConfig.getWeightUnitFee() : typeConfig.getWeightMoreMoney());
  135 + BigDecimal capFee = nvl(typeConfig.getWeightCapFee());
  136 +
  137 + if (weightAdj.compareTo(basicWeight) > 0) {
  138 + BigDecimal moreWeight = weightAdj.subtract(basicWeight);
  139 + moneyWeight = moreWeight.multiply(moreMoneyPerKg).setScale(2, RoundingMode.HALF_UP);
  140 + if (capFee.compareTo(BigDecimal.ZERO) > 0) {
  141 + BigDecimal weightTotal = weightBasicMoney.add(moneyWeight);
  142 + if (weightTotal.compareTo(capFee) > 0) {
  143 + moneyWeight = capFee.subtract(weightBasicMoney).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
  144 + }
  145 + }
  146 + moneyWeightTxt = "(" + moreWeight.toPlainString() + "kg)";
  147 + }
  148 + }
  149 +
  150 + // 件数计费
  151 + if (typeConfig.getPieceSwitch() != null && typeConfig.getPieceSwitch() == 1
  152 + && typeConfig.getPieceRules() != null && !typeConfig.getPieceRules().isEmpty()) {
  153 + int pieces = dto.getPieces() != null ? dto.getPieces() : 0;
  154 + List<DeliveryPricingRuleDTO.PieceRuleDTO> rules = new ArrayList<>(typeConfig.getPieceRules());
  155 + rules.sort(Comparator.comparing(rule -> rule.getListOrder() == null ? 0 : rule.getListOrder()));
  156 + for (DeliveryPricingRuleDTO.PieceRuleDTO rule : rules) {
  157 + int start = rule.getStartPiece() != null ? rule.getStartPiece() : 0;
  158 + int end = rule.getEndPiece() != null ? rule.getEndPiece() : Integer.MAX_VALUE;
  159 + if (pieces >= start && pieces <= end) {
  160 + moneyPiece = nvl(rule.getFee()).setScale(2, RoundingMode.HALF_UP);
  161 + moneyPieceTxt = "(" + start + "-" + end + "件)";
  162 + break;
  163 + }
  164 + }
  165 + }
  166 +
  167 + // 时段附加费
  168 + long serviceTime = dto.getServiceTime() != null && dto.getServiceTime() > 0
  169 + ? dto.getServiceTime() : System.currentTimeMillis() / 1000;
  170 + BigDecimal moneyTime = calcTimeMoney(typeConfig, serviceTime);
  171 +
  172 + result.setMoneyBasic(moneyBasic);
  173 + result.setMoneyBasicTxt(moneyBasicTxt);
  174 + result.setMoneyDistance(moneyDistance);
  175 + result.setMoneyDistanceTxt(moneyDistanceTxt);
  176 + result.setMoneyWeight(moneyWeight);
  177 + result.setMoneyWeightTxt(moneyWeightTxt);
  178 + result.setMoneyPiece(moneyPiece);
  179 + result.setMoneyPieceTxt(moneyPieceTxt);
  180 + result.setMoneyTime(moneyTime);
  181 + result.setDistance(distanceKm);
  182 +
  183 + BigDecimal total = moneyBasic.add(moneyDistance).add(moneyWeight).add(moneyPiece).add(moneyTime);
  184 + result.setTotalFee(applyMinFee(total, typeConfig, result));
  185 + result.setEstimatedMinutes(calcEstimatedMinutes(pricingConfig, distanceKm));
  186 +
  187 + return result;
  188 + }
  189 +
  190 + @Override
  191 + public boolean isServiceEnabled(Long cityId, int orderType) {
  192 + DeliveryPricingConfigDTO config = cityService.getConfig(cityId);
  193 + return config != null && config.getType() != null && config.getType().contains(orderType);
  194 + }
  195 +
  196 + // ---- 私有方法 ----
  197 +
  198 + /**
  199 + * 计算实际距离(km), getDistance() + distance_type取整
  200 + */
  201 + private BigDecimal calcDistance(DeliveryPricingRuleDTO typeConfig, DeliveryFeeCalcDTO dto) {
  202 + double disKm;
  203 + if (typeConfig.getDistanceMode() != null && typeConfig.getDistanceMode() == 2) {
  204 + // 直接使用传入距离(米转km)
  205 + disKm = 0; // 传入距离模式下 dto 中应有 distance 字段,此处简化为0
  206 + } else {
  207 + // 经纬度计算
  208 + try {
  209 + disKm = GeoUtil.calcDistanceKm(
  210 + Double.parseDouble(dto.getStartLat()),
  211 + Double.parseDouble(dto.getStartLng()),
  212 + Double.parseDouble(dto.getEndLat()),
  213 + Double.parseDouble(dto.getEndLng()));
  214 + } catch (Exception e) {
  215 + throw new BizException("地点经纬度信息错误");
  216 + }
  217 + }
  218 + return adjustValue(BigDecimal.valueOf(disKm), typeConfig.getDistanceType());
  219 + }
  220 +
  221 + /**
  222 + * 根据取整方式调整数值
  223 + * distanceType: 1=四舍五入(保留1位) 2=向上取整 3=向下取整
  224 + */
  225 + private BigDecimal adjustValue(BigDecimal value, Integer type) {
  226 + if (type == null || type == 1) {
  227 + return value.setScale(1, RoundingMode.HALF_UP);
  228 + } else if (type == 2) {
  229 + return value.setScale(0, RoundingMode.CEILING).setScale(1);
  230 + } else {
  231 + return value.setScale(0, RoundingMode.FLOOR).setScale(1);
  232 + }
  233 + }
  234 +
  235 + /**
  236 + * 计算时段附加费
  237 + * City.checkTime()
  238 + */
  239 + private BigDecimal calcTimeMoney(DeliveryPricingRuleDTO typeConfig, long serviceTime) {
  240 + if (typeConfig.getTimes() == null || typeConfig.getTimes().isEmpty()) {
  241 + return BigDecimal.ZERO;
  242 + }
  243 + ZonedDateTime zdt = Instant.ofEpochSecond(serviceTime).atZone(ZoneId.of("Asia/Shanghai"));
  244 + int minuteOfDay = zdt.getHour() * 60 + zdt.getMinute();
  245 +
  246 + BigDecimal total = BigDecimal.ZERO;
  247 + for (DeliveryPricingRuleDTO.TimePeriodDTO period : typeConfig.getTimes()) {
  248 + if (period.getIsOpen() == null || period.getIsOpen() != 1) continue;
  249 + if (period.getStart() == null || period.getEnd() == null) continue;
  250 + if (period.getStart() <= period.getEnd()) {
  251 + if (minuteOfDay >= period.getStart() && minuteOfDay < period.getEnd()) {
  252 + total = total.add(period.getMoney() != null ? period.getMoney() : BigDecimal.ZERO);
  253 + }
  254 + } else {
  255 + if (minuteOfDay >= period.getStart() || minuteOfDay < period.getEnd()) {
  256 + total = total.add(period.getMoney() != null ? period.getMoney() : BigDecimal.ZERO);
  257 + }
  258 + }
  259 + }
  260 + return total;
  261 + }
  262 +
  263 + /**
  264 + * 计算预计送达分钟数
  265 + * City.getLength()
  266 + */
  267 + private int calcEstimatedMinutes(DeliveryPricingConfigDTO pricingConfig, BigDecimal distanceKm) {
  268 + if (pricingConfig.getDistanceBasicTime() == null) return 0;
  269 + int basicTime = pricingConfig.getDistanceBasicTime();
  270 + BigDecimal basicKm = pricingConfig.getDistanceBasic() != null ? pricingConfig.getDistanceBasic() : BigDecimal.ZERO;
  271 + int moreTime = pricingConfig.getDistanceMoreTime() != null ? pricingConfig.getDistanceMoreTime() : 0;
  272 +
  273 + if (distanceKm.compareTo(basicKm) <= 0) return basicTime;
  274 + BigDecimal moreKm = distanceKm.subtract(basicKm);
  275 + return (int) (basicTime + moreKm.doubleValue() * moreTime);
  276 + }
  277 +
  278 + private DeliveryPricingRuleDTO getTypeConfig(DeliveryPricingConfigDTO config, int orderType) {
  279 + return switch (orderType) {
  280 + case 1 -> config.getType1();
  281 + case 2 -> config.getType2();
  282 + case 6 -> config.getType6();
  283 + default -> null;
  284 + };
  285 + }
  286 +
  287 + private BigDecimal nvl(BigDecimal v) {
  288 + return v != null ? v : BigDecimal.ZERO;
  289 + }
  290 +
  291 + private BigDecimal calcDistanceStepFee(BigDecimal basicKm, BigDecimal distanceKm,
  292 + List<DeliveryPricingRuleDTO.DistanceStepDTO> steps) {
  293 + List<DeliveryPricingRuleDTO.DistanceStepDTO> sorted = new ArrayList<>(steps);
  294 + sorted.sort(Comparator.comparing(step -> step.getListOrder() == null ? 0 : step.getListOrder()));
  295 +
  296 + BigDecimal fee = BigDecimal.ZERO;
  297 + BigDecimal prevThreshold = basicKm;
  298 + for (DeliveryPricingRuleDTO.DistanceStepDTO step : sorted) {
  299 + BigDecimal endDistance = nvl(step.getEndDistance());
  300 + BigDecimal unitDistance = nvl(step.getUnitDistance());
  301 + BigDecimal unitFee = nvl(step.getUnitFee());
  302 + if (unitDistance.compareTo(BigDecimal.ZERO) <= 0 || unitFee.compareTo(BigDecimal.ZERO) < 0
  303 + || endDistance.compareTo(prevThreshold) <= 0) {
  304 + continue;
  305 + }
  306 +
  307 + BigDecimal capped = distanceKm.min(endDistance);
  308 + if (capped.compareTo(prevThreshold) > 0) {
  309 + BigDecimal segments = capped.subtract(prevThreshold)
  310 + .divide(unitDistance, 0, RoundingMode.CEILING);
  311 + fee = fee.add(segments.multiply(unitFee));
  312 + prevThreshold = endDistance;
  313 + }
  314 + if (distanceKm.compareTo(endDistance) <= 0) {
  315 + return fee.setScale(2, RoundingMode.HALF_UP);
  316 + }
  317 + }
  318 +
  319 + if (!sorted.isEmpty() && distanceKm.compareTo(prevThreshold) > 0) {
  320 + DeliveryPricingRuleDTO.DistanceStepDTO last = sorted.get(sorted.size() - 1);
  321 + BigDecimal unitDistance = nvl(last.getUnitDistance());
  322 + BigDecimal unitFee = nvl(last.getUnitFee());
  323 + if (unitDistance.compareTo(BigDecimal.ZERO) > 0) {
  324 + BigDecimal segments = distanceKm.subtract(prevThreshold)
  325 + .divide(unitDistance, 0, RoundingMode.CEILING);
  326 + fee = fee.add(segments.multiply(unitFee));
  327 + }
  328 + }
  329 + return fee.setScale(2, RoundingMode.HALF_UP);
  330 + }
  331 +
  332 + private BigDecimal applyMinFee(BigDecimal total, DeliveryPricingRuleDTO typeConfig, DeliveryFeeResultVO result) {
  333 + BigDecimal finalTotal = total.setScale(2, RoundingMode.HALF_UP);
  334 + BigDecimal minFee = nvl(typeConfig.getMinFee()).setScale(2, RoundingMode.HALF_UP);
  335 + result.setMinFee(minFee);
  336 + if (minFee.compareTo(BigDecimal.ZERO) > 0 && finalTotal.compareTo(minFee) < 0) {
  337 + result.setMinFeeApplied(1);
  338 + return minFee;
  339 + }
  340 + result.setMinFeeApplied(0);
  341 + return finalTotal;
  342 + }
  343 +}
... ...
src/main/java/com/diligrp/rider/service/impl/DeliveryOrderServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/DeliveryOrderServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.entity.MerchantStore;
  6 +import com.fasterxml.jackson.databind.ObjectMapper;
  7 +import com.diligrp.rider.common.exception.BizException;
  8 +import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
  9 +import com.diligrp.rider.dto.DeliveryOrderCreateDTO;
  10 +import com.diligrp.rider.entity.OpenApp;
  11 +import com.diligrp.rider.entity.Orders;
  12 +import com.diligrp.rider.mapper.OpenAppMapper;
  13 +import com.diligrp.rider.mapper.OrdersMapper;
  14 +import com.diligrp.rider.service.DeliveryFeeService;
  15 +import com.diligrp.rider.service.DeliveryOrderService;
  16 +import com.diligrp.rider.service.MerchantService;
  17 +import com.diligrp.rider.service.WebhookService;
  18 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  19 +import com.diligrp.rider.vo.DeliveryOrderCreateVO;
  20 +import lombok.RequiredArgsConstructor;
  21 +import lombok.extern.slf4j.Slf4j;
  22 +import org.springframework.stereotype.Service;
  23 +import org.springframework.transaction.annotation.Transactional;
  24 +
  25 +import java.math.BigDecimal;
  26 +import java.time.LocalDate;
  27 +import java.time.format.DateTimeFormatter;
  28 +import java.util.List;
  29 +import java.util.HashMap;
  30 +import java.util.Map;
  31 +import java.util.UUID;
  32 +
  33 +@Slf4j
  34 +@Service
  35 +@RequiredArgsConstructor
  36 +public class DeliveryOrderServiceImpl implements DeliveryOrderService {
  37 +
  38 + private final OrdersMapper ordersMapper;
  39 + private final OpenAppMapper openAppMapper;
  40 + private final MerchantService merchantService;
  41 + private final DeliveryFeeService deliveryFeeService;
  42 + private final WebhookService webhookService;
  43 + private final ObjectMapper objectMapper;
  44 +
  45 + @Override
  46 + @Transactional
  47 + public DeliveryOrderCreateVO create(String appKey, DeliveryOrderCreateDTO dto) {
  48 + // 1. 校验应用,从 AppKey 获取租户 cityId(不信任调用方传入的 cityId)
  49 + OpenApp app = getApp(appKey);
  50 + if (app.getCityId() == null || app.getCityId() < 1) {
  51 + throw new BizException("该应用未绑定城市/租户,请联系平台管理员");
  52 + }
  53 + // 强制使用 AppKey 绑定的 cityId,覆盖调用方传入的值
  54 + dto.setCityId(app.getCityId());
  55 +
  56 + // 2. 检查外部订单号唯一性
  57 + Long exists = ordersMapper.selectCount(new LambdaQueryWrapper<Orders>()
  58 + .eq(Orders::getAppKey, appKey)
  59 + .eq(Orders::getOutOrderNo, dto.getOutOrderNo()));
  60 + if (exists > 0) throw new BizException("外部订单号已存在:" + dto.getOutOrderNo());
  61 +
  62 + // 3. 若传了 outStoreId,用 merchant_store 里的门店信息补全发货方
  63 + if (dto.getOutStoreId() != null && !dto.getOutStoreId().isBlank()) {
  64 + MerchantStore ms = merchantService.getByOutStoreId(appKey, dto.getOutStoreId());
  65 + if (ms != null) {
  66 + if (dto.getStoreName() == null || dto.getStoreName().isBlank())
  67 + dto.setStoreName(ms.getName());
  68 + if (dto.getStoreAddr() == null || dto.getStoreAddr().isBlank())
  69 + dto.setStoreAddr(ms.getAddress());
  70 + if (dto.getStoreLng() == null || dto.getStoreLng().isBlank())
  71 + dto.setStoreLng(ms.getLng());
  72 + if (dto.getStoreLat() == null || dto.getStoreLat().isBlank())
  73 + dto.setStoreLat(ms.getLat());
  74 + }
  75 + }
  76 + // 校验发货方坐标
  77 + if (dto.getStoreLng() == null || dto.getStoreLng().isBlank()
  78 + || dto.getStoreLat() == null || dto.getStoreLat().isBlank()) {
  79 + throw new BizException("发货方经纬度不能为空(请传 storeLng/storeLat 或有效的 outStoreId)");
  80 + }
  81 +
  82 + // 4. 计算配送费
  83 + DeliveryFeeCalcDTO feeDTO = new DeliveryFeeCalcDTO();
  84 + feeDTO.setCityId(dto.getCityId());
  85 + feeDTO.setOrderType(6); // 外卖配送
  86 + feeDTO.setStartLng(dto.getStoreLng());
  87 + feeDTO.setStartLat(dto.getStoreLat());
  88 + feeDTO.setEndLng(dto.getRecipLng());
  89 + feeDTO.setEndLat(dto.getRecipLat());
  90 + feeDTO.setWeight(dto.getWeight());
  91 + feeDTO.setPieces(calcPieces(dto.getItems()));
  92 + feeDTO.setServiceTime(dto.getServiceTime());
  93 + DeliveryFeeResultVO fee = deliveryFeeService.calcFee(feeDTO);
  94 +
  95 + // 4. 生成订单号
  96 + String orderNo = generateOrderNo();
  97 + // 5. 生成完成码(6位数字)
  98 + String code = String.valueOf((int) (Math.random() * 900000 + 100000));
  99 +
  100 + // 6. 构建 extra 信息
  101 + Map<String, Object> extra = new HashMap<>();
  102 + extra.put("distance", fee.getDistance());
  103 + extra.put("weight", dto.getWeight());
  104 + extra.put("pieces", feeDTO.getPieces());
  105 + extra.put("remark", dto.getRemark());
  106 + extra.put("computed", Map.of(
  107 + "money_basic", fee.getMoneyBasic(),
  108 + "money_distance", fee.getMoneyDistance(),
  109 + "money_piece", fee.getMoneyPiece(),
  110 + "money_time", fee.getMoneyTime()
  111 + ));
  112 + String extraJson;
  113 + try {
  114 + extraJson = objectMapper.writeValueAsString(extra);
  115 + } catch (Exception e) {
  116 + extraJson = "{}";
  117 + }
  118 +
  119 + // 7. 回调地址优先用订单级,其次用应用级
  120 + String callbackUrl = dto.getCallbackUrl();
  121 + if (callbackUrl == null || callbackUrl.isBlank()) {
  122 + callbackUrl = app.getWebhookUrl();
  123 + }
  124 +
  125 + // 8. 创建订单
  126 + Orders order = new Orders();
  127 + order.setOrderNo(orderNo);
  128 + order.setOutOrderNo(dto.getOutOrderNo());
  129 + order.setAppKey(appKey);
  130 + order.setCityId(dto.getCityId());
  131 + order.setType(6);
  132 + order.setStatus(2); // 直接到待接单(外部系统已完成支付)
  133 + order.setMoneyDelivery(fee.getTotalFee());
  134 + order.setMoney(fee.getTotalFee());
  135 + order.setMoneyTotal(fee.getTotalFee());
  136 + order.setFName(dto.getStoreName());
  137 + order.setFAddr(dto.getStoreAddr() != null ? dto.getStoreAddr() : "");
  138 + order.setFLng(dto.getStoreLng());
  139 + order.setFLat(dto.getStoreLat());
  140 + order.setTName(dto.getRecipAddr());
  141 + order.setTAddr(dto.getRecipAddr());
  142 + order.setTLng(dto.getRecipLng());
  143 + order.setTLat(dto.getRecipLat());
  144 + order.setRecipName(dto.getRecipName());
  145 + order.setRecipPhone(dto.getRecipPhone());
  146 + order.setCode(code);
  147 + order.setExtra(extraJson);
  148 + order.setCallbackUrl(callbackUrl);
  149 + order.setItemsJson(null);
  150 + // 货物快照
  151 + if (dto.getItems() != null && !dto.getItems().isEmpty()) {
  152 + try {
  153 + order.setItemsJson(objectMapper.writeValueAsString(dto.getItems()));
  154 + } catch (Exception ignored) {}
  155 + }
  156 + order.setItemRemark(dto.getItemRemark());
  157 + order.setExtStoreId(null); // 改为通过 outStoreId 关联,不存具体ID
  158 + order.setRiderId(0L);
  159 + order.setOldRiderId(0L);
  160 + order.setIsTrans(0);
  161 + order.setIsIncome(0);
  162 + order.setIsDel(0);
  163 + order.setAddTime(System.currentTimeMillis() / 1000);
  164 + order.setPayTime(System.currentTimeMillis() / 1000);
  165 + ordersMapper.insert(order);
  166 +
  167 + // 9. 回调接入方:订单已创建
  168 + notifyCallback(order, "order.created");
  169 +
  170 + return toCreateVO(order, fee);
  171 + }
  172 +
  173 + @Override
  174 + public DeliveryOrderCreateVO queryByOutOrderNo(String appKey, String outOrderNo) {
  175 + getApp(appKey);
  176 + Orders order = getOrderByOutNo(appKey, outOrderNo);
  177 + DeliveryFeeResultVO feeVO = new DeliveryFeeResultVO();
  178 + feeVO.setMoneyBasic(order.getMoneyDelivery());
  179 + feeVO.setMoneyDistance(BigDecimal.ZERO);
  180 + feeVO.setMoneyTime(BigDecimal.ZERO);
  181 + feeVO.setTotalFee(order.getMoneyDelivery());
  182 + return toCreateVO(order, feeVO);
  183 + }
  184 +
  185 + @Override
  186 + @Transactional
  187 + public void cancel(String appKey, String outOrderNo) {
  188 + getApp(appKey);
  189 + Orders order = getOrderByOutNo(appKey, outOrderNo);
  190 + if (order.getStatus() != 2) {
  191 + throw new BizException("订单已接单或已完成,无法取消");
  192 + }
  193 + ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  194 + .eq(Orders::getId, order.getId())
  195 + .set(Orders::getStatus, 10));
  196 + order.setStatus(10);
  197 + notifyCallback(order, "order.cancelled");
  198 + }
  199 +
  200 + // ---- 私有方法 ----
  201 +
  202 + private OpenApp getApp(String appKey) {
  203 + OpenApp app = openAppMapper.selectOne(new LambdaQueryWrapper<OpenApp>()
  204 + .eq(OpenApp::getAppKey, appKey).last("LIMIT 1"));
  205 + if (app == null || app.getStatus() != 1) throw new BizException("应用不存在或已禁用");
  206 + return app;
  207 + }
  208 +
  209 + private Orders getOrderByOutNo(String appKey, String outOrderNo) {
  210 + Orders order = ordersMapper.selectOne(new LambdaQueryWrapper<Orders>()
  211 + .eq(Orders::getAppKey, appKey)
  212 + .eq(Orders::getOutOrderNo, outOrderNo)
  213 + .last("LIMIT 1"));
  214 + if (order == null) throw new BizException("订单不存在");
  215 + return order;
  216 + }
  217 +
  218 + private void notifyCallback(Orders order, String event) {
  219 + try {
  220 + Map<String, Object> payload = new HashMap<>();
  221 + payload.put("event", event);
  222 + payload.put("outOrderNo", order.getOutOrderNo());
  223 + payload.put("deliveryOrderId", order.getId());
  224 + payload.put("orderNo", order.getOrderNo());
  225 + payload.put("status", order.getStatus());
  226 + payload.put("timestamp", System.currentTimeMillis() / 1000);
  227 + String json = objectMapper.writeValueAsString(payload);
  228 + webhookService.send(event, order.getId(), json);
  229 + } catch (Exception e) {
  230 + log.warn("通知回调失败 orderId={}", order.getId(), e);
  231 + }
  232 + }
  233 +
  234 + private String generateOrderNo() {
  235 + String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
  236 + return "DL" + date + UUID.randomUUID().toString().replace("-", "").substring(0, 10).toUpperCase();
  237 + }
  238 +
  239 + private Integer calcPieces(List<DeliveryOrderCreateDTO.DeliveryItemDTO> items) {
  240 + if (items == null || items.isEmpty()) {
  241 + return 0;
  242 + }
  243 + int pieces = 0;
  244 + for (DeliveryOrderCreateDTO.DeliveryItemDTO item : items) {
  245 + pieces += item.getQuantity() != null && item.getQuantity() > 0 ? item.getQuantity() : 1;
  246 + }
  247 + return pieces;
  248 + }
  249 +
  250 + private DeliveryOrderCreateVO toCreateVO(Orders order, DeliveryFeeResultVO fee) {
  251 + DeliveryOrderCreateVO vo = new DeliveryOrderCreateVO();
  252 + vo.setDeliveryOrderId(order.getId());
  253 + vo.setOrderNo(order.getOrderNo());
  254 + vo.setOutOrderNo(order.getOutOrderNo());
  255 + vo.setMoneyBasic(fee.getMoneyBasic());
  256 + vo.setMoneyDistance(fee.getMoneyDistance());
  257 + vo.setMoneyTime(fee.getMoneyTime());
  258 + vo.setTotalFee(fee.getTotalFee());
  259 + vo.setDistance(fee.getDistance());
  260 + vo.setEstimatedMinutes(fee.getEstimatedMinutes());
  261 + vo.setStatus(order.getStatus());
  262 + return vo;
  263 + }
  264 +}
... ...
src/main/java/com/diligrp/rider/service/impl/ExtStoreServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/ExtStoreServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.entity.ExtStore;
  7 +import com.diligrp.rider.mapper.ExtStoreMapper;
  8 +import com.diligrp.rider.service.ExtStoreService;
  9 +import lombok.RequiredArgsConstructor;
  10 +import org.springframework.stereotype.Service;
  11 +
  12 +import java.util.List;
  13 +
  14 +@Service
  15 +@RequiredArgsConstructor
  16 +public class ExtStoreServiceImpl implements ExtStoreService {
  17 +
  18 + private final ExtStoreMapper extStoreMapper;
  19 +
  20 + @Override
  21 + public ExtStore syncStore(String appKey, ExtStore store) {
  22 + store.setAppKey(appKey);
  23 + long now = System.currentTimeMillis() / 1000;
  24 +
  25 + // 根据 appKey + outStoreId 判断是新增还是更新
  26 + ExtStore existing = extStoreMapper.selectOne(new LambdaQueryWrapper<ExtStore>()
  27 + .eq(ExtStore::getAppKey, appKey)
  28 + .eq(ExtStore::getOutStoreId, store.getOutStoreId())
  29 + .last("LIMIT 1"));
  30 +
  31 + if (existing == null) {
  32 + store.setStatus(store.getStatus() != null ? store.getStatus() : 1);
  33 + store.setCreateTime(now);
  34 + store.setUpdateTime(now);
  35 + extStoreMapper.insert(store);
  36 + } else {
  37 + store.setId(existing.getId());
  38 + store.setUpdateTime(now);
  39 + extStoreMapper.updateById(store);
  40 + }
  41 + return extStoreMapper.selectById(store.getId());
  42 + }
  43 +
  44 + @Override
  45 + public List<ExtStore> listByApp(String appKey) {
  46 + return extStoreMapper.selectList(new LambdaQueryWrapper<ExtStore>()
  47 + .eq(ExtStore::getAppKey, appKey)
  48 + .eq(ExtStore::getStatus, 1)
  49 + .orderByDesc(ExtStore::getId));
  50 + }
  51 +
  52 + @Override
  53 + public ExtStore getById(Long id, String appKey) {
  54 + ExtStore store = extStoreMapper.selectById(id);
  55 + if (store == null) throw new BizException("门店不存在");
  56 + if (appKey != null && !appKey.equals(store.getAppKey())) throw new BizException("无权访问该门店");
  57 + return store;
  58 + }
  59 +
  60 + @Override
  61 + public void setStatus(Long id, String appKey, int status) {
  62 + ExtStore store = getById(id, appKey);
  63 + extStoreMapper.update(null, new LambdaUpdateWrapper<ExtStore>()
  64 + .eq(ExtStore::getId, id)
  65 + .set(ExtStore::getStatus, status)
  66 + .set(ExtStore::getUpdateTime, System.currentTimeMillis() / 1000));
  67 + }
  68 +
  69 + @Override
  70 + public void delete(Long id, String appKey) {
  71 + getById(id, appKey);
  72 + extStoreMapper.deleteById(id);
  73 + }
  74 +
  75 + // 平台管理端:不限 appKey 查看所有门店
  76 + @Override
  77 + public List<ExtStore> listAll(String appKey, Long cityId, int page) {
  78 + LambdaQueryWrapper<ExtStore> wrapper = new LambdaQueryWrapper<ExtStore>()
  79 + .orderByDesc(ExtStore::getId);
  80 + if (appKey != null && !appKey.isBlank()) wrapper.eq(ExtStore::getAppKey, appKey);
  81 + if (cityId != null && cityId > 0) wrapper.eq(ExtStore::getCityId, cityId);
  82 + int offset = (page - 1) * 20;
  83 + wrapper.last("LIMIT " + offset + ",20");
  84 + return extStoreMapper.selectList(wrapper);
  85 + }
  86 +}
... ...
src/main/java/com/diligrp/rider/service/impl/MerchantServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/MerchantServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.dto.MerchantStoreDTO;
  7 +import com.diligrp.rider.entity.MerchantStore;
  8 +import com.diligrp.rider.entity.MerchantUsers;
  9 +import com.diligrp.rider.mapper.MerchantStoreMapper;
  10 +import com.diligrp.rider.mapper.MerchantUsersMapper;
  11 +import com.diligrp.rider.service.CityService;
  12 +import com.diligrp.rider.service.MerchantService;
  13 +import lombok.RequiredArgsConstructor;
  14 +import org.springframework.stereotype.Service;
  15 +import org.springframework.transaction.annotation.Transactional;
  16 +
  17 +import java.math.BigDecimal;
  18 +import java.util.List;
  19 +
  20 +@Service
  21 +@RequiredArgsConstructor
  22 +public class MerchantServiceImpl implements MerchantService {
  23 +
  24 + private final MerchantStoreMapper storeMapper;
  25 + private final MerchantUsersMapper usersMapper;
  26 + private final CityService cityService;
  27 +
  28 + private static final int PAGE_SIZE = 20;
  29 +
  30 + @Override
  31 + @Transactional
  32 + public Long addStore(MerchantStoreDTO dto) {
  33 + var city = cityService.getById(dto.getCityId());
  34 + if (city == null) throw new BizException("城市不存在");
  35 +
  36 + MerchantStore store = toEntity(dto);
  37 + store.setAddTime(System.currentTimeMillis() / 1000);
  38 + store.setIsDel(0);
  39 + storeMapper.insert(store);
  40 +
  41 + if (dto.getAccountMobile() != null && !dto.getAccountMobile().isBlank()) {
  42 + Long exists = usersMapper.selectCount(new LambdaQueryWrapper<MerchantUsers>()
  43 + .eq(MerchantUsers::getMobile, dto.getAccountMobile()));
  44 + if (exists > 0) throw new BizException("该手机号已有账号");
  45 + MerchantUsers user = new MerchantUsers();
  46 + user.setStoreId(store.getId());
  47 + user.setMobile(dto.getAccountMobile());
  48 + user.setUserNickname(dto.getName());
  49 + user.setUserStatus(1);
  50 + user.setType(1);
  51 + user.setCreateTime(System.currentTimeMillis() / 1000);
  52 + usersMapper.insert(user);
  53 + }
  54 + return store.getId();
  55 + }
  56 +
  57 + @Override
  58 + public void editStore(MerchantStoreDTO dto) {
  59 + MerchantStore existing = storeMapper.selectById(dto.getId());
  60 + if (existing == null) throw new BizException("店铺不存在");
  61 + MerchantStore store = toEntity(dto);
  62 + store.setId(dto.getId());
  63 + storeMapper.updateById(store);
  64 + }
  65 +
  66 + @Override
  67 + public List<MerchantStore> storeList(Long cityId, String keyword, int page) {
  68 + LambdaQueryWrapper<MerchantStore> wrapper = new LambdaQueryWrapper<MerchantStore>()
  69 + .eq(MerchantStore::getIsDel, 0)
  70 + .orderByAsc(MerchantStore::getListOrder)
  71 + .orderByDesc(MerchantStore::getId);
  72 + if (cityId != null && cityId > 0) wrapper.eq(MerchantStore::getCityId, cityId);
  73 + if (keyword != null && !keyword.isBlank()) wrapper.like(MerchantStore::getName, keyword);
  74 + int offset = (page - 1) * PAGE_SIZE;
  75 + wrapper.last("LIMIT " + offset + "," + PAGE_SIZE);
  76 + return storeMapper.selectList(wrapper);
  77 + }
  78 +
  79 + @Override
  80 + public MerchantStore getStore(Long storeId) {
  81 + return storeMapper.selectById(storeId);
  82 + }
  83 +
  84 + @Override
  85 + public void setOperatingState(Long storeId, int state) {
  86 + storeMapper.update(null, new LambdaUpdateWrapper<MerchantStore>()
  87 + .eq(MerchantStore::getId, storeId)
  88 + .set(MerchantStore::getOperatingState, state));
  89 + }
  90 +
  91 + @Override
  92 + public void setAutoOrder(Long storeId, int auto) {
  93 + storeMapper.update(null, new LambdaUpdateWrapper<MerchantStore>()
  94 + .eq(MerchantStore::getId, storeId)
  95 + .set(MerchantStore::getAutomaticOrder, auto));
  96 + }
  97 +
  98 + @Override
  99 + public void updateFeeConfig(Long storeId, BigDecimal freeShipping, BigDecimal upToSend) {
  100 + storeMapper.update(null, new LambdaUpdateWrapper<MerchantStore>()
  101 + .eq(MerchantStore::getId, storeId)
  102 + .set(MerchantStore::getFreeShipping, freeShipping)
  103 + .set(MerchantStore::getUpToSend, upToSend));
  104 + }
  105 +
  106 + @Override
  107 + public void delStore(Long storeId) {
  108 + storeMapper.update(null, new LambdaUpdateWrapper<MerchantStore>()
  109 + .eq(MerchantStore::getId, storeId)
  110 + .set(MerchantStore::getIsDel, 1));
  111 + }
  112 +
  113 + @Override
  114 + @Transactional
  115 + public MerchantStore syncStore(String appKey, MerchantStoreDTO dto) {
  116 + if (appKey == null || appKey.isBlank()) throw new BizException("AppKey不能为空");
  117 + if (dto.getOutStoreId() == null || dto.getOutStoreId().isBlank()) throw new BizException("外部门店编号不能为空");
  118 +
  119 + long now = System.currentTimeMillis() / 1000;
  120 + MerchantStore existing = getByOutStoreId(appKey, dto.getOutStoreId());
  121 +
  122 + if (existing == null) {
  123 + MerchantStore store = toEntity(dto);
  124 + store.setAppKey(appKey);
  125 + store.setAddTime(now);
  126 + store.setIsDel(0);
  127 + storeMapper.insert(store);
  128 + return store;
  129 + } else {
  130 + MerchantStore store = toEntity(dto);
  131 + store.setId(existing.getId());
  132 + store.setAppKey(appKey);
  133 + storeMapper.updateById(store);
  134 + return storeMapper.selectById(existing.getId());
  135 + }
  136 + }
  137 +
  138 + @Override
  139 + public MerchantStore getByOutStoreId(String appKey, String outStoreId) {
  140 + return storeMapper.selectOne(new LambdaQueryWrapper<MerchantStore>()
  141 + .eq(MerchantStore::getAppKey, appKey)
  142 + .eq(MerchantStore::getOutStoreId, outStoreId)
  143 + .eq(MerchantStore::getIsDel, 0)
  144 + .last("LIMIT 1"));
  145 + }
  146 +
  147 + private MerchantStore toEntity(MerchantStoreDTO dto) {
  148 + MerchantStore store = new MerchantStore();
  149 + store.setName(dto.getName());
  150 + store.setCityId(dto.getCityId());
  151 + store.setThumb(dto.getThumb());
  152 + store.setAddress(dto.getAddress());
  153 + store.setLng(dto.getLng());
  154 + store.setLat(dto.getLat());
  155 + store.setOperatingState(dto.getOperatingState() != null ? dto.getOperatingState() : 1);
  156 + store.setAutomaticOrder(dto.getAutomaticOrder() != null ? dto.getAutomaticOrder() : 0);
  157 + store.setShippingType(dto.getShippingType() != null ? dto.getShippingType() : 1);
  158 + store.setFreeShipping(dto.getFreeShipping() != null ? dto.getFreeShipping() : BigDecimal.ZERO);
  159 + store.setUpToSend(dto.getUpToSend() != null ? dto.getUpToSend() : BigDecimal.ZERO);
  160 + store.setOpenDate(dto.getOpenDate());
  161 + store.setOpenTime(dto.getOpenTime());
  162 + store.setAbout(dto.getAbout());
  163 + store.setOutStoreId(dto.getOutStoreId());
  164 + return store;
  165 + }
  166 +}
... ...
src/main/java/com/diligrp/rider/service/impl/OpenAppServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/OpenAppServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.entity.OpenApp;
  7 +import com.diligrp.rider.mapper.OpenAppMapper;
  8 +import com.diligrp.rider.service.OpenAppService;
  9 +import com.diligrp.rider.util.SignUtil;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.stereotype.Service;
  12 +
  13 +import java.util.List;
  14 +
  15 +@Service
  16 +@RequiredArgsConstructor
  17 +public class OpenAppServiceImpl implements OpenAppService {
  18 +
  19 + private final OpenAppMapper openAppMapper;
  20 +
  21 + @Override
  22 + public OpenApp create(String appName, Long cityId, Long storeId, String webhookUrl, String webhookEvents, String remark) {
  23 + if (cityId == null || cityId < 1) throw new BizException("请选择关联城市/租户");
  24 + OpenApp app = new OpenApp();
  25 + app.setAppName(appName);
  26 + app.setAppKey(SignUtil.generateAppKey());
  27 + app.setAppSecret(SignUtil.generateAppSecret());
  28 + app.setCityId(cityId);
  29 + app.setStoreId(storeId != null ? storeId : 0L);
  30 + app.setStatus(1);
  31 + app.setWebhookUrl(webhookUrl);
  32 + app.setWebhookEvents(webhookEvents);
  33 + app.setRemark(remark);
  34 + app.setCreateTime(System.currentTimeMillis() / 1000);
  35 + openAppMapper.insert(app);
  36 + return app;
  37 + }
  38 +
  39 + @Override
  40 + public List<OpenApp> list(int page) {
  41 + int offset = (page - 1) * 20;
  42 + return openAppMapper.selectList(new LambdaQueryWrapper<OpenApp>()
  43 + .orderByDesc(OpenApp::getId)
  44 + .last("LIMIT " + offset + ",20"));
  45 + }
  46 +
  47 + @Override
  48 + public String resetSecret(Long appId) {
  49 + OpenApp app = openAppMapper.selectById(appId);
  50 + if (app == null) throw new BizException("应用不存在");
  51 + String newSecret = SignUtil.generateAppSecret();
  52 + openAppMapper.update(null, new LambdaUpdateWrapper<OpenApp>()
  53 + .eq(OpenApp::getId, appId)
  54 + .set(OpenApp::getAppSecret, newSecret));
  55 + return newSecret;
  56 + }
  57 +
  58 + @Override
  59 + public void setStatus(Long appId, int status) {
  60 + openAppMapper.update(null, new LambdaUpdateWrapper<OpenApp>()
  61 + .eq(OpenApp::getId, appId)
  62 + .set(OpenApp::getStatus, status));
  63 + }
  64 +
  65 + @Override
  66 + public void updateWebhook(Long appId, String webhookUrl, String webhookEvents) {
  67 + openAppMapper.update(null, new LambdaUpdateWrapper<OpenApp>()
  68 + .eq(OpenApp::getId, appId)
  69 + .set(OpenApp::getWebhookUrl, webhookUrl)
  70 + .set(OpenApp::getWebhookEvents, webhookEvents));
  71 + }
  72 +
  73 + @Override
  74 + public OpenApp getByAppKey(String appKey) {
  75 + return openAppMapper.selectOne(new LambdaQueryWrapper<OpenApp>()
  76 + .eq(OpenApp::getAppKey, appKey)
  77 + .last("LIMIT 1"));
  78 + }
  79 +
  80 + @Override
  81 + public boolean verifySign(String appKey, String timestamp, String nonce, String sign) {
  82 + OpenApp app = getByAppKey(appKey);
  83 + if (app == null || app.getStatus() != 1) return false;
  84 + return SignUtil.verify(appKey, timestamp, nonce, sign, app.getAppSecret());
  85 + }
  86 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RefundServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/RefundServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.entity.OrderRefundReason;
  7 +import com.diligrp.rider.entity.OrderRefundRecord;
  8 +import com.diligrp.rider.entity.Orders;
  9 +import com.diligrp.rider.mapper.OrderRefundReasonMapper;
  10 +import com.diligrp.rider.mapper.OrderRefundRecordMapper;
  11 +import com.diligrp.rider.mapper.OrdersMapper;
  12 +import com.diligrp.rider.entity.*;
  13 +import com.diligrp.rider.mapper.*;
  14 +import com.diligrp.rider.service.RefundService;
  15 +import com.diligrp.rider.service.WebhookService;
  16 +import com.fasterxml.jackson.databind.ObjectMapper;
  17 +import lombok.RequiredArgsConstructor;
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.stereotype.Service;
  20 +import org.springframework.transaction.annotation.Transactional;
  21 +
  22 +import java.util.HashMap;
  23 +import java.util.List;
  24 +import java.util.Map;
  25 +
  26 +@Slf4j
  27 +@Service
  28 +@RequiredArgsConstructor
  29 +public class RefundServiceImpl implements RefundService {
  30 +
  31 + private final OrderRefundReasonMapper reasonMapper;
  32 + private final OrderRefundRecordMapper recordMapper;
  33 + private final OrdersMapper ordersMapper;
  34 + private final WebhookService webhookService;
  35 + private final ObjectMapper objectMapper;
  36 +
  37 + @Override
  38 + public List<OrderRefundReason> getReasons(int role) {
  39 + return reasonMapper.selectList(new LambdaQueryWrapper<OrderRefundReason>()
  40 + .eq(role > 0, OrderRefundReason::getRole, role)
  41 + .orderByAsc(OrderRefundReason::getListOrder));
  42 + }
  43 +
  44 + @Override
  45 + @Transactional
  46 + public void applyRefund(Long orderId, Long uid, int role, Long reasonId, String reason) {
  47 + Orders order = ordersMapper.selectById(orderId);
  48 + if (order == null) throw new BizException("订单不存在");
  49 +
  50 + // 用户申请:必须是自己的订单且已完成或服务中
  51 + if (role == 1) {
  52 + if (!uid.equals(order.getUid())) throw new BizException("订单信息错误");
  53 + if (order.getStatus() < 3 || order.getStatus() == 10) throw new BizException("当前订单状态不可申请退款");
  54 + }
  55 + // 骑手申请:必须是自己接的单
  56 + if (role == 2) {
  57 + if (!uid.equals(order.getRiderId())) throw new BizException("订单信息错误");
  58 + if (order.getStatus() != 3 && order.getStatus() != 4) throw new BizException("当前订单状态不可申请退款");
  59 + }
  60 +
  61 + // 检查是否已有退款申请
  62 + Long exists = recordMapper.selectCount(new LambdaQueryWrapper<OrderRefundRecord>()
  63 + .eq(OrderRefundRecord::getOid, orderId)
  64 + .ne(OrderRefundRecord::getStatus, 2));
  65 + if (exists > 0) throw new BizException("该订单已有退款申请,请勿重复提交");
  66 +
  67 + // 更新订单状态为退款申请中
  68 + ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  69 + .eq(Orders::getId, orderId)
  70 + .set(Orders::getStatus, 7));
  71 +
  72 + // 写退款记录
  73 + OrderRefundRecord record = new OrderRefundRecord();
  74 + record.setOid(orderId);
  75 + record.setOrderNo(order.getOrderNo());
  76 + record.setUid(uid);
  77 + record.setRole(role);
  78 + record.setReasonId(reasonId);
  79 + record.setReason(reason);
  80 + record.setMoney(order.getMoneyDelivery());
  81 + record.setStatus(0);
  82 + record.setAddTime(System.currentTimeMillis() / 1000);
  83 + recordMapper.insert(record);
  84 +
  85 + // 通知接入方
  86 + notifyRefundEvent(order, "order.refund_apply");
  87 + }
  88 +
  89 + @Override
  90 + @Transactional
  91 + public void handleRefund(Long recordId, int status, String remark) {
  92 + OrderRefundRecord record = recordMapper.selectById(recordId);
  93 + if (record == null) throw new BizException("退款记录不存在");
  94 + if (record.getStatus() != 0) throw new BizException("该退款申请已处理");
  95 +
  96 + long now = System.currentTimeMillis() / 1000;
  97 + recordMapper.update(null, new LambdaUpdateWrapper<OrderRefundRecord>()
  98 + .eq(OrderRefundRecord::getId, recordId)
  99 + .set(OrderRefundRecord::getStatus, status)
  100 + .set(OrderRefundRecord::getRemark, remark)
  101 + .set(OrderRefundRecord::getHandleTime, now));
  102 +
  103 + Orders order = ordersMapper.selectById(record.getOid());
  104 + if (order == null) return;
  105 +
  106 + if (status == 1) {
  107 + // 退款通过 → 订单状态改为退款成功
  108 + ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  109 + .eq(Orders::getId, record.getOid())
  110 + .set(Orders::getStatus, 8));
  111 + notifyRefundEvent(order, "order.refund_success");
  112 + } else {
  113 + // 退款拒绝 → 订单状态改为退款拒绝
  114 + ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  115 + .eq(Orders::getId, record.getOid())
  116 + .set(Orders::getStatus, 9));
  117 + notifyRefundEvent(order, "order.refund_reject");
  118 + }
  119 + }
  120 +
  121 + @Override
  122 + public OrderRefundRecord getByOrderId(Long orderId) {
  123 + return recordMapper.selectOne(new LambdaQueryWrapper<OrderRefundRecord>()
  124 + .eq(OrderRefundRecord::getOid, orderId)
  125 + .orderByDesc(OrderRefundRecord::getId)
  126 + .last("LIMIT 1"));
  127 + }
  128 +
  129 + private void notifyRefundEvent(Orders order, String event) {
  130 + try {
  131 + if (order.getAppKey() == null || order.getAppKey().isBlank()) return;
  132 + Map<String, Object> payload = new HashMap<>();
  133 + payload.put("event", event);
  134 + payload.put("outOrderNo", order.getOutOrderNo());
  135 + payload.put("deliveryOrderId", order.getId());
  136 + payload.put("orderNo", order.getOrderNo());
  137 + payload.put("status", order.getStatus());
  138 + payload.put("timestamp", System.currentTimeMillis() / 1000);
  139 + webhookService.send(event, order.getId(), objectMapper.writeValueAsString(payload));
  140 + } catch (Exception e) {
  141 + log.warn("退款事件通知失败 orderId={}", order.getId(), e);
  142 + }
  143 + }
  144 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RiderAuthServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/RiderAuthServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.dto.ApplyDTO;
  7 +import com.diligrp.rider.dto.LoginDTO;
  8 +import com.diligrp.rider.entity.Rider;
  9 +import com.diligrp.rider.mapper.RiderMapper;
  10 +import com.diligrp.rider.service.RiderAuthService;
  11 +import com.diligrp.rider.config.JwtUtil;
  12 +import com.diligrp.rider.vo.RiderVO;
  13 +import lombok.RequiredArgsConstructor;
  14 +import org.springframework.data.redis.core.StringRedisTemplate;
  15 +import org.springframework.stereotype.Service;
  16 +import org.springframework.util.DigestUtils;
  17 +
  18 +import java.nio.charset.StandardCharsets;
  19 +
  20 +@Service
  21 +@RequiredArgsConstructor
  22 +public class RiderAuthServiceImpl implements RiderAuthService {
  23 +
  24 + private final RiderMapper riderMapper;
  25 + private final JwtUtil jwtUtil;
  26 + private final StringRedisTemplate redisTemplate;
  27 +
  28 + private static final String SMS_KEY_PREFIX = "rider_sms_apply_";
  29 +
  30 + @Override
  31 + public void apply(ApplyDTO dto) {
  32 + // 校验验证码
  33 + String cacheCode = redisTemplate.opsForValue().get(SMS_KEY_PREFIX + dto.getMobile());
  34 + if (cacheCode == null || !cacheCode.equals(dto.getCode())) {
  35 + throw new BizException("验证码错误或已过期");
  36 + }
  37 +
  38 + // 手机号唯一校验
  39 + Long exists = riderMapper.selectCount(new LambdaQueryWrapper<Rider>()
  40 + .eq(Rider::getMobile, dto.getMobile()));
  41 + if (exists > 0) {
  42 + throw new BizException("该手机号已申请,请更换");
  43 + }
  44 +
  45 + Rider rider = new Rider();
  46 + rider.setMobile(dto.getMobile());
  47 + rider.setUserNickname(dto.getName());
  48 + rider.setUserPass(encryptPass(dto.getPass()));
  49 + rider.setIdNo(dto.getIdNo());
  50 + rider.setThumb(dto.getThumb());
  51 + rider.setCityId(dto.getCityId());
  52 + rider.setUserStatus(2); // 待审核
  53 + rider.setType(1); // 默认兼职
  54 + rider.setIsRest(0);
  55 + rider.setUserLogin("phone_" + System.currentTimeMillis());
  56 + rider.setCreateTime(System.currentTimeMillis() / 1000);
  57 + riderMapper.insert(rider);
  58 +
  59 + // 删除验证码
  60 + redisTemplate.delete(SMS_KEY_PREFIX + dto.getMobile());
  61 + }
  62 +
  63 + @Override
  64 + public RiderVO loginByPass(LoginDTO dto) {
  65 + Rider rider = riderMapper.selectOne(new LambdaQueryWrapper<Rider>()
  66 + .eq(Rider::getMobile, dto.getUsername()));
  67 + if (rider == null) {
  68 + throw new BizException("手机号或密码错误");
  69 + }
  70 + if (!encryptPass(dto.getPass()).equals(rider.getUserPass())) {
  71 + throw new BizException("手机号或密码错误");
  72 + }
  73 + if (rider.getUserStatus() == 2) {
  74 + throw new BizException("账号审核中,请耐心等待");
  75 + }
  76 + if (rider.getUserStatus() == 0) {
  77 + throw new BizException("账号审核未通过,请联系客服");
  78 + }
  79 + if (rider.getStatus() != null && rider.getStatus() == 0) {
  80 + throw new BizException("账号已被禁用");
  81 + }
  82 + return buildVO(rider);
  83 + }
  84 +
  85 + @Override
  86 + public RiderVO getInfo(Long riderId) {
  87 + Rider rider = riderMapper.selectById(riderId);
  88 + if (rider == null) throw new BizException("骑手信息不存在");
  89 + return buildVO(rider);
  90 + }
  91 +
  92 + @Override
  93 + public void toggleRest(Long riderId) {
  94 + Rider rider = riderMapper.selectById(riderId);
  95 + if (rider == null) throw new BizException("骑手信息不存在");
  96 + int newRest = rider.getIsRest() == 1 ? 0 : 1;
  97 + riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
  98 + .eq(Rider::getId, riderId)
  99 + .set(Rider::getIsRest, newRest));
  100 + }
  101 +
  102 + private RiderVO buildVO(Rider rider) {
  103 + RiderVO vo = new RiderVO();
  104 + vo.setId(rider.getId());
  105 + vo.setMobile(rider.getMobile());
  106 + vo.setUserNickname(rider.getUserNickname());
  107 + vo.setAvatar(rider.getAvatar());
  108 + vo.setType(rider.getType());
  109 + vo.setTypeName(rider.getType() == 1 ? "兼职" : "全职");
  110 + vo.setUserStatus(rider.getUserStatus());
  111 + vo.setStatus(rider.getStatus() == null ? 1 : rider.getStatus());
  112 + vo.setStatusName((rider.getStatus() == null || rider.getStatus() == 1) ? "正常" : "禁用");
  113 + vo.setBalance(rider.getBalance());
  114 + vo.setIsRest(rider.getIsRest());
  115 + vo.setCityId(rider.getCityId());
  116 + vo.setToken(jwtUtil.generateRiderToken(rider.getId(), rider.getCityId()));
  117 + return vo;
  118 + }
  119 +
  120 + private String encryptPass(String pass) {
  121 + return DigestUtils.md5DigestAsHex(pass.getBytes(StandardCharsets.UTF_8));
  122 + }
  123 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RiderBalanceServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/RiderBalanceServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.exception.BizException;
  5 +import com.diligrp.rider.entity.Rider;
  6 +import com.diligrp.rider.entity.RiderBalance;
  7 +import com.diligrp.rider.mapper.RiderBalanceMapper;
  8 +import com.diligrp.rider.mapper.RiderMapper;
  9 +import com.diligrp.rider.service.RiderBalanceService;
  10 +import com.diligrp.rider.vo.BalanceVO;
  11 +import lombok.RequiredArgsConstructor;
  12 +import org.springframework.stereotype.Service;
  13 +
  14 +import java.time.Instant;
  15 +import java.time.ZoneId;
  16 +import java.time.format.DateTimeFormatter;
  17 +import java.util.ArrayList;
  18 +import java.util.List;
  19 +
  20 +@Service
  21 +@RequiredArgsConstructor
  22 +public class RiderBalanceServiceImpl implements RiderBalanceService {
  23 +
  24 + private final RiderMapper riderMapper;
  25 + private final RiderBalanceMapper balanceMapper;
  26 +
  27 + private static final int PAGE_SIZE = 20;
  28 + private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
  29 + .withZone(ZoneId.of("Asia/Shanghai"));
  30 +
  31 + @Override
  32 + public BalanceVO getBalance(Long riderId, int page) {
  33 + Rider rider = riderMapper.selectById(riderId);
  34 + if (rider == null) throw new BizException("骑手信息不存在");
  35 +
  36 + int offset = (page - 1) * PAGE_SIZE;
  37 + List<RiderBalance> records = balanceMapper.selectList(
  38 + new LambdaQueryWrapper<RiderBalance>()
  39 + .eq(RiderBalance::getUid, riderId)
  40 + .orderByDesc(RiderBalance::getId)
  41 + .last("LIMIT " + offset + "," + PAGE_SIZE));
  42 +
  43 + List<BalanceVO.BalanceRecordVO> recordVOs = new ArrayList<>();
  44 + for (RiderBalance r : records) {
  45 + BalanceVO.BalanceRecordVO vo = new BalanceVO.BalanceRecordVO();
  46 + vo.setId(r.getId());
  47 + vo.setType(r.getType());
  48 + vo.setTypeName(r.getType() == 1 ? "收入" : "提现");
  49 + vo.setAction(r.getAction());
  50 + vo.setOrderNo(r.getOrderNo());
  51 + vo.setNums(r.getNums());
  52 + vo.setTotal(r.getTotal());
  53 + vo.setAddTime(r.getAddTime() != null ? FMT.format(Instant.ofEpochSecond(r.getAddTime())) : "");
  54 + recordVOs.add(vo);
  55 + }
  56 +
  57 + BalanceVO vo = new BalanceVO();
  58 + vo.setBalance(rider.getBalance());
  59 + vo.setRecords(recordVOs);
  60 + return vo;
  61 + }
  62 +
  63 + @Override
  64 + public java.math.BigDecimal getTodayIncome(Long riderId) {
  65 + // Balance.getToday():type=1(收入) action=order_complete 当日流水总和
  66 + java.time.LocalDate today = java.time.LocalDate.now();
  67 + long todayStart = today.atStartOfDay(ZoneId.of("Asia/Shanghai")).toEpochSecond();
  68 + long todayEnd = todayStart + 86400;
  69 +
  70 + java.util.List<RiderBalance> records = balanceMapper.selectList(
  71 + new LambdaQueryWrapper<RiderBalance>()
  72 + .eq(RiderBalance::getUid, riderId)
  73 + .eq(RiderBalance::getType, 1) // 收入
  74 + .ge(RiderBalance::getAddTime, todayStart)
  75 + .lt(RiderBalance::getAddTime, todayEnd));
  76 +
  77 + return records.stream()
  78 + .map(RiderBalance::getNums)
  79 + .filter(n -> n != null)
  80 + .reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
  81 + }
  82 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RiderEvaluateServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/RiderEvaluateServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.entity.Orders;
  7 +import com.diligrp.rider.entity.RiderEvaluate;
  8 +import com.diligrp.rider.entity.Rider;
  9 +import com.diligrp.rider.mapper.OrdersMapper;
  10 +import com.diligrp.rider.mapper.RiderEvaluateMapper;
  11 +import com.diligrp.rider.mapper.RiderMapper;
  12 +import com.diligrp.rider.service.RiderEvaluateService;
  13 +import lombok.RequiredArgsConstructor;
  14 +import org.springframework.stereotype.Service;
  15 +import org.springframework.transaction.annotation.Transactional;
  16 +
  17 +import java.time.YearMonth;
  18 +import java.time.ZoneId;
  19 +import java.util.List;
  20 +
  21 +@Service
  22 +@RequiredArgsConstructor
  23 +public class RiderEvaluateServiceImpl implements RiderEvaluateService {
  24 +
  25 + private final RiderEvaluateMapper evaluateMapper;
  26 + private final OrdersMapper ordersMapper;
  27 + private final RiderMapper riderMapper;
  28 +
  29 + private static final int PAGE_SIZE = 20;
  30 +
  31 + @Override
  32 + @Transactional
  33 + public void evaluate(Long uid, Long orderId, int star, String content) {
  34 + if (star < 1 || star > 5) throw new BizException("请选择1-5星评分");
  35 +
  36 + Orders order = ordersMapper.selectById(orderId);
  37 + if (order == null) throw new BizException("订单不存在");
  38 + if (!uid.equals(order.getUid())) throw new BizException("订单信息错误");
  39 + if (order.getStatus() != 6) throw new BizException("订单未完成,无法评价");
  40 +
  41 + // 检查是否已评价
  42 + Long exists = evaluateMapper.selectCount(new LambdaQueryWrapper<RiderEvaluate>()
  43 + .eq(RiderEvaluate::getUid, uid)
  44 + .eq(RiderEvaluate::getOid, orderId));
  45 + if (exists > 0) throw new BizException("订单已评价,无法再次评价");
  46 +
  47 + // 写评价
  48 + RiderEvaluate eval = new RiderEvaluate();
  49 + eval.setUid(uid);
  50 + eval.setOid(orderId);
  51 + eval.setRid(order.getRiderId());
  52 + eval.setContent(content);
  53 + eval.setStar(star);
  54 + eval.setCityId(order.getCityId());
  55 + eval.setAddTime(System.currentTimeMillis() / 1000);
  56 + evaluateMapper.insert(eval);
  57 +
  58 + // 更新骑手星级(累加平均分)
  59 + Rider rider = riderMapper.selectById(order.getRiderId());
  60 + if (rider != null) {
  61 + // 简单累加到 extra 统计,实际可扩展 star_total/star_count 字段
  62 + // 此处直接用 SQL 累加到骑手备注字段,生产建议加 star_total/count 字段
  63 + riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
  64 + .eq(Rider::getId, order.getRiderId())
  65 + .setSql("star_total = IFNULL(star_total,0) + " + star
  66 + + ", star_count = IFNULL(star_count,0) + 1"));
  67 + }
  68 + }
  69 +
  70 + @Override
  71 + public List<RiderEvaluate> getRiderEvaluates(Long riderId, Integer type, int page) {
  72 + LambdaQueryWrapper<RiderEvaluate> wrapper = new LambdaQueryWrapper<RiderEvaluate>()
  73 + .eq(RiderEvaluate::getRid, riderId)
  74 + .orderByDesc(RiderEvaluate::getId);
  75 + if (type == 1) wrapper.ge(RiderEvaluate::getStar, 4); // 好评
  76 + if (type == 2) wrapper.gt(RiderEvaluate::getStar, 2).lt(RiderEvaluate::getStar, 4); // 中评
  77 + if (type == 3) wrapper.le(RiderEvaluate::getStar, 2); // 差评
  78 + int offset = (page - 1) * PAGE_SIZE;
  79 + wrapper.last("LIMIT " + offset + "," + PAGE_SIZE);
  80 + return evaluateMapper.selectList(wrapper);
  81 + }
  82 +
  83 + @Override
  84 + public int getMonthGoodCount(Long riderId) {
  85 + YearMonth ym = YearMonth.now();
  86 + long start = ym.atDay(1).atStartOfDay(ZoneId.of("Asia/Shanghai")).toEpochSecond();
  87 + long end = ym.plusMonths(1).atDay(1).atStartOfDay(ZoneId.of("Asia/Shanghai")).toEpochSecond();
  88 + return evaluateMapper.selectCount(new LambdaQueryWrapper<RiderEvaluate>()
  89 + .eq(RiderEvaluate::getRid, riderId)
  90 + .ge(RiderEvaluate::getStar, 4)
  91 + .ge(RiderEvaluate::getAddTime, start)
  92 + .lt(RiderEvaluate::getAddTime, end)).intValue();
  93 + }
  94 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RiderLevelServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/RiderLevelServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.exception.BizException;
  5 +import com.diligrp.rider.entity.RiderLevel;
  6 +import com.diligrp.rider.entity.Rider;
  7 +import com.diligrp.rider.mapper.RiderLevelMapper;
  8 +import com.diligrp.rider.mapper.RiderMapper;
  9 +import com.diligrp.rider.service.RiderLevelService;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.stereotype.Service;
  12 +
  13 +import java.math.BigDecimal;
  14 +import java.math.RoundingMode;
  15 +
  16 +@Service
  17 +@RequiredArgsConstructor
  18 +public class RiderLevelServiceImpl implements RiderLevelService {
  19 +
  20 + private final RiderMapper riderMapper;
  21 + private final RiderLevelMapper riderLevelMapper;
  22 +
  23 + @Override
  24 + public RiderLevel getLevelByRider(Long riderId) {
  25 + Rider rider = riderMapper.selectById(riderId);
  26 + if (rider == null) throw new BizException("骑手信息不存在");
  27 +
  28 + RiderLevel level = null;
  29 + if (rider.getLevelId() != null && rider.getLevelId() > 0) {
  30 + level = riderLevelMapper.selectById(rider.getLevelId());
  31 + }
  32 + if (level == null) {
  33 + // 取该城市默认等级
  34 + level = riderLevelMapper.selectOne(new LambdaQueryWrapper<RiderLevel>()
  35 + .eq(RiderLevel::getCityId, rider.getCityId())
  36 + .eq(RiderLevel::getIsDefault, 1)
  37 + .last("LIMIT 1"));
  38 + }
  39 + return level;
  40 + }
  41 +
  42 + @Override
  43 + public BigDecimal calcIncome(Long riderId, int orderType, BigDecimal deliveryFee, long distance) {
  44 + RiderLevel level = getLevelByRider(riderId);
  45 + if (level == null) return BigDecimal.ZERO;
  46 +
  47 + // 外卖配送(type=6)按跑腿类规则计算
  48 + // 办事类(type=5)按办事类规则计算,其余统一按跑腿类
  49 + if (orderType == 5) {
  50 + return calcWorkIncome(level, deliveryFee);
  51 + }
  52 + return calcRunIncome(level, deliveryFee, distance);
  53 + }
  54 +
  55 + private BigDecimal calcRunIncome(RiderLevel level, BigDecimal deliveryFee, long distance) {
  56 + int mode = level.getRunFeeMode() == null ? 1 : level.getRunFeeMode();
  57 + switch (mode) {
  58 + case 1: // 固定金额
  59 + return level.getRunFixMoney() == null ? BigDecimal.ZERO : level.getRunFixMoney();
  60 + case 2: // 按比例
  61 + if (level.getRunRate() == null || deliveryFee == null) return BigDecimal.ZERO;
  62 + return deliveryFee.multiply(level.getRunRate())
  63 + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
  64 + case 3: // 按距离
  65 + return calcDistanceIncome(level, distance);
  66 + default:
  67 + return BigDecimal.ZERO;
  68 + }
  69 + }
  70 +
  71 + private BigDecimal calcDistanceIncome(RiderLevel level, long distanceMeters) {
  72 + if (level.getDistanceBasic() == null || level.getDistanceBasicMoney() == null) {
  73 + return BigDecimal.ZERO;
  74 + }
  75 + BigDecimal income = level.getDistanceBasicMoney();
  76 + long basicMeters = level.getDistanceBasic();
  77 + if (distanceMeters > basicMeters && level.getDistanceMoreMoney() != null) {
  78 + // 超出部分按每公里计费
  79 + long extraMeters = distanceMeters - basicMeters;
  80 + BigDecimal extraKm = BigDecimal.valueOf(extraMeters).divide(BigDecimal.valueOf(1000), 2, RoundingMode.HALF_UP);
  81 + income = income.add(extraKm.multiply(level.getDistanceMoreMoney()));
  82 + }
  83 + // 上限
  84 + if (level.getDistanceMaxMoney() != null && level.getDistanceMaxMoney().compareTo(BigDecimal.ZERO) > 0) {
  85 + if (income.compareTo(level.getDistanceMaxMoney()) > 0) {
  86 + income = level.getDistanceMaxMoney();
  87 + }
  88 + }
  89 + return income.setScale(2, RoundingMode.HALF_UP);
  90 + }
  91 +
  92 + private BigDecimal calcWorkIncome(RiderLevel level, BigDecimal deliveryFee) {
  93 + int mode = level.getWorkFeeMode() == null ? 1 : level.getWorkFeeMode();
  94 + switch (mode) {
  95 + case 1:
  96 + return level.getWorkFixMoney() == null ? BigDecimal.ZERO : level.getWorkFixMoney();
  97 + case 2:
  98 + if (level.getWorkRate() == null || deliveryFee == null) return BigDecimal.ZERO;
  99 + return deliveryFee.multiply(level.getWorkRate())
  100 + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
  101 + default:
  102 + return BigDecimal.ZERO;
  103 + }
  104 + }
  105 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RiderLocationServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/RiderLocationServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
  6 +import com.diligrp.rider.dto.LocationDTO;
  7 +import com.diligrp.rider.entity.Rider;
  8 +import com.diligrp.rider.entity.RiderLocation;
  9 +import com.diligrp.rider.mapper.RiderLocationMapper;
  10 +import com.diligrp.rider.mapper.RiderMapper;
  11 +import com.diligrp.rider.service.CityService;
  12 +import com.diligrp.rider.service.RiderLocationService;
  13 +import com.diligrp.rider.util.GeoUtil;
  14 +import com.diligrp.rider.vo.NearbyRiderVO;
  15 +import lombok.RequiredArgsConstructor;
  16 +import org.springframework.stereotype.Service;
  17 +
  18 +import java.math.BigDecimal;
  19 +import java.util.ArrayList;
  20 +import java.util.List;
  21 +
  22 +@Service
  23 +@RequiredArgsConstructor
  24 +public class RiderLocationServiceImpl implements RiderLocationService {
  25 +
  26 + private final RiderLocationMapper locationMapper;
  27 + private final RiderMapper riderMapper;
  28 + private final CityService cityService;
  29 +
  30 + @Override
  31 + public void updateLocation(Long riderId, LocationDTO dto) {
  32 + RiderLocation existing = locationMapper.selectOne(new LambdaQueryWrapper<RiderLocation>()
  33 + .eq(RiderLocation::getUid, riderId));
  34 + long now = System.currentTimeMillis() / 1000;
  35 + if (existing == null) {
  36 + RiderLocation loc = new RiderLocation();
  37 + loc.setUid(riderId);
  38 + loc.setLng(dto.getLng());
  39 + loc.setLat(dto.getLat());
  40 + loc.setUpdateTime(now);
  41 + locationMapper.insert(loc);
  42 + } else {
  43 + locationMapper.update(null, new LambdaUpdateWrapper<RiderLocation>()
  44 + .eq(RiderLocation::getUid, riderId)
  45 + .set(RiderLocation::getLng, dto.getLng())
  46 + .set(RiderLocation::getLat, dto.getLat())
  47 + .set(RiderLocation::getUpdateTime, now));
  48 + }
  49 + }
  50 +
  51 + @Override
  52 + public LocationDTO getLocation(Long riderId) {
  53 + RiderLocation loc = locationMapper.selectOne(new LambdaQueryWrapper<RiderLocation>()
  54 + .eq(RiderLocation::getUid, riderId));
  55 + if (loc == null) return null;
  56 + LocationDTO dto = new LocationDTO();
  57 + dto.setLng(loc.getLng());
  58 + dto.setLat(loc.getLat());
  59 + return dto;
  60 + }
  61 +
  62 + @Override
  63 + public List<NearbyRiderVO> getNearby(Long cityId, String lng, String lat) {
  64 + List<NearbyRiderVO> result = new ArrayList<>();
  65 + if (cityId == null || cityId < 1 || lng == null || lat == null) return result;
  66 +
  67 + // 获取城市配置的附近距离范围(km)
  68 + DeliveryPricingConfigDTO config = cityService.getConfig(cityId);
  69 + if (config == null) return result;
  70 + BigDecimal limitKm = config.getRiderDistance();
  71 + if (limitKm == null || limitKm.compareTo(BigDecimal.ZERO) <= 0) {
  72 + limitKm = BigDecimal.valueOf(3); // 默认3km
  73 + }
  74 +
  75 + // 查该城市所有在线(未休息)骑手
  76 + List<Rider> riders = riderMapper.selectList(new LambdaQueryWrapper<Rider>()
  77 + .eq(Rider::getCityId, cityId)
  78 + .eq(Rider::getIsRest, 0)
  79 + .eq(Rider::getUserStatus, 1));
  80 + if (riders.isEmpty()) return result;
  81 +
  82 + List<Long> riderIds = riders.stream().map(Rider::getId).toList();
  83 +
  84 + // 查骑手位置
  85 + List<RiderLocation> locations = locationMapper.selectList(
  86 + new LambdaQueryWrapper<RiderLocation>().in(RiderLocation::getUid, riderIds));
  87 +
  88 + double queryLat = Double.parseDouble(lat);
  89 + double queryLng = Double.parseDouble(lng);
  90 + double limitD = limitKm.doubleValue();
  91 +
  92 + for (RiderLocation loc : locations) {
  93 + try {
  94 + double dist = GeoUtil.calcDistanceKm(
  95 + queryLat, queryLng,
  96 + Double.parseDouble(loc.getLat()),
  97 + Double.parseDouble(loc.getLng()));
  98 + if (dist <= limitD) {
  99 + NearbyRiderVO vo = new NearbyRiderVO();
  100 + vo.setLng(loc.getLng());
  101 + vo.setLat(loc.getLat());
  102 + vo.setDistance(dist);
  103 + result.add(vo);
  104 + }
  105 + } catch (Exception ignored) {}
  106 + }
  107 + return result;
  108 + }
  109 +}
... ...
src/main/java/com/diligrp/rider/service/impl/RiderOrderServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/RiderOrderServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.entity.*;
  6 +import com.diligrp.rider.mapper.*;
  7 +import com.fasterxml.jackson.core.type.TypeReference;
  8 +import com.fasterxml.jackson.databind.ObjectMapper;
  9 +import com.diligrp.rider.common.exception.BizException;
  10 +import com.diligrp.rider.entity.*;
  11 +import com.diligrp.rider.mapper.*;
  12 +import com.diligrp.rider.service.RiderLevelService;
  13 +import com.diligrp.rider.service.RiderOrderService;
  14 +import com.diligrp.rider.service.WebhookService;
  15 +import com.diligrp.rider.vo.OrderVO;
  16 +import com.diligrp.rider.vo.RiderMonthCountVO;
  17 +import com.diligrp.rider.vo.RiderTodayCountVO;
  18 +import lombok.RequiredArgsConstructor;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.stereotype.Service;
  21 +import org.springframework.transaction.annotation.Transactional;
  22 +
  23 +import java.math.BigDecimal;
  24 +import java.time.LocalDate;
  25 +import java.time.ZoneId;
  26 +import java.time.format.DateTimeFormatter;
  27 +import java.util.ArrayList;
  28 +import java.util.List;
  29 +import java.util.Map;
  30 +
  31 +@Slf4j
  32 +@Service
  33 +@RequiredArgsConstructor
  34 +public class RiderOrderServiceImpl implements RiderOrderService {
  35 +
  36 + private final OrdersMapper ordersMapper;
  37 + private final RiderOrderRefuseMapper refuseMapper;
  38 + private final RiderOrderCountMapper countMapper;
  39 + private final RiderBalanceMapper balanceMapper;
  40 + private final RiderMapper riderMapper;
  41 + private final RiderLevelService riderLevelService;
  42 + private final ObjectMapper objectMapper;
  43 + private final WebhookService webhookService;
  44 +
  45 + private static final int PAGE_SIZE = 20;
  46 +
  47 + @Override
  48 + public List<OrderVO> getList(Long riderId, Long cityId, Integer type, int page) {
  49 + if (type == null || !List.of(1, 2, 3).contains(type)) return List.of();
  50 +
  51 + LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<Orders>()
  52 + .eq(Orders::getCityId, cityId)
  53 + .eq(Orders::getIsDel, 0);
  54 +
  55 + if (type == 1) {
  56 + // 待接单:已支付、无骑手、非自己转出的单、未拒绝的单
  57 + wrapper.eq(Orders::getStatus, 2)
  58 + .eq(Orders::getRiderId, 0)
  59 + .ne(Orders::getOldRiderId, riderId);
  60 + List<Long> refusedIds = refuseMapper.selectRefuseOrderIds(riderId);
  61 + if (!refusedIds.isEmpty()) {
  62 + wrapper.notIn(Orders::getId, refusedIds);
  63 + }
  64 + } else if (type == 2) {
  65 + // 待取货:已接单
  66 + wrapper.eq(Orders::getRiderId, riderId).eq(Orders::getStatus, 3);
  67 + } else {
  68 + // 待完成:服务中
  69 + wrapper.eq(Orders::getRiderId, riderId).eq(Orders::getStatus, 4);
  70 + }
  71 +
  72 + int offset = (page - 1) * PAGE_SIZE;
  73 + wrapper.orderByDesc(Orders::getId).last("LIMIT " + offset + "," + PAGE_SIZE);
  74 +
  75 + List<Orders> orders = ordersMapper.selectList(wrapper);
  76 + List<OrderVO> result = new ArrayList<>();
  77 + for (Orders o : orders) {
  78 + result.add(toVO(o, riderId));
  79 + }
  80 + return result;
  81 + }
  82 +
  83 + @Override
  84 + public OrderVO getDetail(Long riderId, Long orderId) {
  85 + Orders order = ordersMapper.selectById(orderId);
  86 + if (order == null) throw new BizException(1001, "订单信息错误");
  87 + // @TODO 注释
  88 +// if (!riderId.equals(order.getRiderId())) throw new BizException(1002, "订单信息错误");
  89 + if (order.getIsTrans() == 1 && riderId.equals(order.getOldRiderId())) {
  90 + throw new BizException(1003, "转单的订单无法查看详情");
  91 + }
  92 + return toVO(order, riderId);
  93 + }
  94 +
  95 + @Override
  96 + @Transactional
  97 + public void refuse(Long riderId, Long cityId, Long orderId) {
  98 + Orders order = ordersMapper.selectById(orderId);
  99 + if (order == null) throw new BizException(1001, "订单信息错误");
  100 + if (!cityId.equals(order.getCityId())) throw new BizException(1002, "订单信息错误");
  101 + if (order.getRiderId() != null && order.getRiderId() != 0) {
  102 + throw new BizException(1003, "订单已被接");
  103 + }
  104 + RiderOrderRefuse refuse = new RiderOrderRefuse();
  105 + refuse.setRiderId(riderId);
  106 + refuse.setOid(orderId);
  107 + refuse.setAddTime(System.currentTimeMillis() / 1000);
  108 + refuseMapper.insert(refuse);
  109 + }
  110 +
  111 + @Override
  112 + @Transactional
  113 + public void grap(Long riderId, Long cityId, Long orderId) {
  114 + // 检查今日转单次数
  115 + checkTransLimit(riderId, cityId);
  116 +
  117 + Rider rider = riderMapper.selectById(riderId);
  118 + if (rider == null) throw new BizException(1000, "骑手信息不存在");
  119 + if (rider.getIsRest() != null && rider.getIsRest() == 1) {
  120 + throw new BizException(1000, "休息中无法接单");
  121 + }
  122 +
  123 + Orders order = ordersMapper.selectById(orderId);
  124 + if (order == null) throw new BizException(1001, "订单信息错误");
  125 + if (!cityId.equals(order.getCityId())) throw new BizException(1002, "订单城市不匹配");
  126 + if (order.getStatus() < 2) throw new BizException(1003, "订单状态错误");
  127 + if (order.getRiderId() != null && order.getRiderId() != 0) {
  128 + throw new BizException(980, "抢单失败,订单已被接");
  129 + }
  130 + if (order.getIsTrans() == 1 && riderId.equals(order.getOldRiderId())) {
  131 + throw new BizException(980, "抢单失败,不能抢自己转出的单");
  132 + }
  133 +
  134 + long now = System.currentTimeMillis() / 1000;
  135 + LambdaUpdateWrapper<Orders> wrapper = new LambdaUpdateWrapper<Orders>()
  136 + .eq(Orders::getId, orderId)
  137 + .eq(Orders::getRiderId, 0)
  138 + .eq(Orders::getStatus, 2)
  139 + .set(Orders::getRiderId, riderId)
  140 + .set(Orders::getStatus, 3)
  141 + .set(Orders::getGrapTime, now);
  142 + if (order.getOldRiderId() == null || order.getOldRiderId() == 0) {
  143 + wrapper.set(Orders::getOldRiderId, riderId);
  144 + }
  145 + int updated = ordersMapper.update(null, wrapper);
  146 + if (updated == 0) throw new BizException(980, "抢单失败,请重试");
  147 +
  148 + // 预置骑手收入
  149 + presetIncome(orderId, riderId, order);
  150 +
  151 + // 通知接入方:骑手已接单
  152 + notifyOrderEvent(orderId, "order.accepted");
  153 +
  154 + // 删除该骑手的拒单记录(抢到了就清掉)
  155 + refuseMapper.delete(new LambdaQueryWrapper<RiderOrderRefuse>()
  156 + .eq(RiderOrderRefuse::getOid, orderId));
  157 + }
  158 +
  159 + @Override
  160 + @Transactional
  161 + public void start(Long riderId, Long orderId, String code) {
  162 + if (code == null || code.isBlank()) throw new BizException(1001, "请输入完成码");
  163 +
  164 + Orders order = ordersMapper.selectById(orderId);
  165 + if (order == null) throw new BizException(1004, "订单信息错误");
  166 + if (!riderId.equals(order.getRiderId())) throw new BizException(1005, "订单信息错误");
  167 + if (order.getStatus() != 3) throw new BizException(1006, "订单状态错误");
  168 + // @TODO 注释
  169 +// if (!code.equals(order.getCode())) throw new BizException(1007, "完成码错误");
  170 +
  171 + long now = System.currentTimeMillis() / 1000;
  172 + int updated = ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  173 + .eq(Orders::getId, orderId)
  174 + .eq(Orders::getRiderId, riderId)
  175 + .eq(Orders::getStatus, 3)
  176 + .set(Orders::getStatus, 4)
  177 + .set(Orders::getPickTime, now));
  178 + if (updated == 0) throw new BizException(980, "操作失败");
  179 + // 通知接入方:骑手取件中
  180 + notifyOrderEvent(orderId, "order.picking");
  181 + }
  182 +
  183 + @Override
  184 + @Transactional
  185 + public void complete(Long riderId, Long orderId, String thumbsJson) {
  186 + if (thumbsJson == null || thumbsJson.isBlank()) throw new BizException(1001, "请上传照片");
  187 +
  188 + List<String> thumbs;
  189 + try {
  190 + thumbs = objectMapper.readValue(thumbsJson, new TypeReference<>() {});
  191 + } catch (Exception e) {
  192 + throw new BizException(1002, "照片格式错误");
  193 + }
  194 + if (thumbs.isEmpty()) throw new BizException(1002, "请上传照片");
  195 + if (thumbs.size() > 3) throw new BizException(1003, "最多上传3张照片");
  196 +
  197 + Orders order = ordersMapper.selectById(orderId);
  198 + if (order == null) throw new BizException(1004, "订单信息错误");
  199 + if (!riderId.equals(order.getRiderId())) throw new BizException(1005, "订单信息错误");
  200 + if (order.getStatus() != 4) throw new BizException(1006, "订单状态错误");
  201 +
  202 + long now = System.currentTimeMillis() / 1000;
  203 + int updated = ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  204 + .eq(Orders::getId, orderId)
  205 + .eq(Orders::getRiderId, riderId)
  206 + .eq(Orders::getStatus, 4)
  207 + .set(Orders::getThumbs, thumbsJson)
  208 + .set(Orders::getStatus, 6)
  209 + .set(Orders::getCompleteTime, now));
  210 + if (updated == 0) throw new BizException(1008, "操作失败");
  211 +
  212 + // 结算骑手收入
  213 + settleIncome(orderId, riderId);
  214 +
  215 + // 通知接入方:订单已完成
  216 + notifyOrderEvent(orderId, "order.completed");
  217 +
  218 + // 累计骑手统计
  219 + long distance = getDistance(order);
  220 + int countDate = Integer.parseInt(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
  221 + countMapper.upsertCount(riderId, countDate, 1, distance, 0);
  222 + }
  223 +
  224 + // ---- 私有辅助方法 ----
  225 +
  226 + /** 通知接入方订单状态变更 */
  227 + private void notifyOrderEvent(Long orderId, String event) {
  228 + try {
  229 + Orders order = ordersMapper.selectById(orderId);
  230 + if (order == null || order.getAppKey() == null) return;
  231 + java.util.Map<String, Object> payload = new java.util.HashMap<>();
  232 + payload.put("event", event);
  233 + payload.put("outOrderNo", order.getOutOrderNo());
  234 + payload.put("deliveryOrderId", order.getId());
  235 + payload.put("orderNo", order.getOrderNo());
  236 + payload.put("status", order.getStatus());
  237 + payload.put("riderId", order.getRiderId());
  238 + payload.put("timestamp", System.currentTimeMillis() / 1000);
  239 + webhookService.send(event, orderId, objectMapper.writeValueAsString(payload));
  240 + } catch (Exception e) {
  241 + log.warn("订单事件通知失败 orderId={} event={}", orderId, event, e);
  242 + }
  243 + }
  244 +
  245 + private void checkTransLimit(Long riderId, Long cityId) {
  246 + RiderLevel level = riderLevelService.getLevelByRider(riderId);
  247 + if (level == null || level.getTransNums() == null) return;
  248 + long todayStart = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toEpochSecond();
  249 + int todayTrans = ordersMapper.countTodayTrans(riderId, todayStart);
  250 + if (todayTrans >= level.getTransNums()) {
  251 + throw new BizException(1000, "今日转单已超上限,无法接单");
  252 + }
  253 + }
  254 +
  255 + private void presetIncome(Long orderId, Long riderId, Orders order) {
  256 + try {
  257 + long distance = getDistance(order);
  258 + BigDecimal income = riderLevelService.calcIncome(riderId, order.getType(),
  259 + order.getMoneyDelivery(), distance);
  260 + ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  261 + .eq(Orders::getId, orderId)
  262 + .set(Orders::getRiderIncome, income)
  263 + .set(Orders::getIsIncome, 1));
  264 + } catch (Exception e) {
  265 + log.warn("预置收入失败 orderId={}", orderId, e);
  266 + }
  267 + }
  268 +
  269 + private void settleIncome(Long orderId, Long riderId) {
  270 + Orders order = ordersMapper.selectById(orderId);
  271 + BigDecimal income = order.getRiderIncome();
  272 + if (income == null) income = BigDecimal.ZERO;
  273 +
  274 + // 更新结算状态
  275 + ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  276 + .eq(Orders::getId, orderId)
  277 + .set(Orders::getIsIncome, 2));
  278 +
  279 + // 兼职骑手累加余额
  280 + Rider rider = riderMapper.selectById(riderId);
  281 + if (rider != null && rider.getType() == 1 && income.compareTo(BigDecimal.ZERO) > 0) {
  282 + riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
  283 + .eq(Rider::getId, riderId)
  284 + .setSql("balance = balance + " + income));
  285 + // 记录流水
  286 + RiderBalance balance = new RiderBalance();
  287 + balance.setUid(riderId);
  288 + balance.setType(1);
  289 + balance.setAction("order_complete");
  290 + balance.setActionId(orderId);
  291 + balance.setOrderNo(order.getOrderNo());
  292 + balance.setNums(income);
  293 + balance.setAddTime(System.currentTimeMillis() / 1000);
  294 + balanceMapper.insert(balance);
  295 + }
  296 + }
  297 +
  298 + private long getDistance(Orders order) {
  299 + try {
  300 + Map<String, Object> extra = objectMapper.readValue(order.getExtra(), new TypeReference<>() {});
  301 + Object dist = extra.get("distance");
  302 + if (dist != null) return Long.parseLong(dist.toString());
  303 + } catch (Exception ignored) {}
  304 + return 0;
  305 + }
  306 +
  307 + private OrderVO toVO(Orders o, Long riderId) {
  308 + OrderVO vo = new OrderVO();
  309 + vo.setId(o.getId());
  310 + vo.setOrderNo(o.getOrderNo());
  311 + vo.setType(o.getType());
  312 + vo.setTypeName(getTypeName(o.getType()));
  313 + vo.setStatus(o.getStatus());
  314 + vo.setFName(o.getFName());
  315 + vo.setFAddr(o.getFAddr());
  316 + vo.setFLng(o.getFLng());
  317 + vo.setFLat(o.getFLat());
  318 + vo.setTName(o.getTName());
  319 + vo.setTAddr(o.getTAddr());
  320 + vo.setTLng(o.getTLng());
  321 + vo.setTLat(o.getTLat());
  322 + vo.setRecipName(o.getRecipName());
  323 + vo.setRecipPhone(o.getRecipPhone());
  324 + vo.setIsTrans(o.getIsTrans());
  325 + vo.setAddTime(o.getAddTime() != null ? formatTime(o.getAddTime()) : null);
  326 + vo.setGrapTime(o.getGrapTime() != null && o.getGrapTime() > 0 ? formatTime(o.getGrapTime()) : null);
  327 + vo.setPickTime(o.getPickTime() != null && o.getPickTime() > 0 ? formatTime(o.getPickTime()) : null);
  328 + vo.setCompleteTime(o.getCompleteTime() != null && o.getCompleteTime() > 0 ? formatTime(o.getCompleteTime()) : null);
  329 + vo.setTransTime(o.getTransTime() != null && o.getTransTime() > 0 ? formatTime(o.getTransTime()) : null);
  330 + // 收入
  331 + BigDecimal income = o.getRiderIncome() != null ? o.getRiderIncome() : BigDecimal.ZERO;
  332 + if (o.getStatus() != 6 && (o.getIsIncome() == null || o.getIsIncome() == 0)) {
  333 + income = riderLevelService.calcIncome(riderId, o.getType(), o.getMoneyDelivery(), getDistance(o));
  334 + }
  335 + vo.setIncome(income);
  336 + vo.setOutOrderNo(o.getOutOrderNo());
  337 + vo.setItemRemark(o.getItemRemark());
  338 + try {
  339 + if (o.getExtra() != null) {
  340 + Map<String, Object> extraMap = objectMapper.readValue(o.getExtra(), new TypeReference<Map<String, Object>>() {});
  341 + vo.setExtra(extraMap);
  342 + // 从extra取备注
  343 + Object remark = extraMap.get("remark");
  344 + if (remark != null) vo.setDes(remark.toString());
  345 + // 从extra取预约标识
  346 + Object isPre = extraMap.get("ispre");
  347 + if (isPre != null) vo.setIsPre(Integer.parseInt(isPre.toString()));
  348 + // 从extra取服务时间
  349 + Object serviceTime = extraMap.get("service_time");
  350 + if (serviceTime != null) vo.setService_time(serviceTime.toString());
  351 + }
  352 + if (o.getItemsJson() != null && !o.getItemsJson().isBlank()) {
  353 + vo.setItems(objectMapper.readValue(o.getItemsJson(), new TypeReference<List<Object>>() {}));
  354 + }
  355 + } catch (Exception ignored) {}
  356 + return vo;
  357 + }
  358 +
  359 + private String formatTime(long epochSecond) {
  360 + return java.time.Instant.ofEpochSecond(epochSecond)
  361 + .atZone(ZoneId.of("Asia/Shanghai"))
  362 + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
  363 + }
  364 +
  365 + private String getTypeName(Integer type) {
  366 + if (type == null) return "";
  367 + return switch (type) {
  368 + case 1 -> "帮我送";
  369 + case 2 -> "帮我取";
  370 + case 3 -> "帮我买";
  371 + case 4 -> "帮我排队";
  372 + case 5 -> "帮我办";
  373 + case 6 -> "外卖配送";
  374 + default -> "";
  375 + };
  376 + }
  377 +
  378 + @Override
  379 + @Transactional
  380 + public void applyTrans(Long riderId, Long orderId) {
  381 + Orders order = ordersMapper.selectById(orderId);
  382 + if (order == null) throw new BizException(1001, "订单不存在");
  383 + if (!riderId.equals(order.getRiderId())) throw new BizException(1002, "订单信息错误");
  384 + if (order.getStatus() != 3 && order.getStatus() != 4) {
  385 + throw new BizException(1003, "只有已接单或配送中的订单可以申请转单");
  386 + }
  387 + if (order.getIsTrans() != 0) throw new BizException(1004, "转单申请已提交,请勿重复操作");
  388 +
  389 + // 检查今日转单次数
  390 + checkTransLimit(riderId, order.getCityId());
  391 +
  392 + long now = System.currentTimeMillis() / 1000;
  393 + int countDate = Integer.parseInt(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
  394 +
  395 + if (order.getStatus() == 3) {
  396 + // status=3(已接单,未取货):直接回抢单池,无需审批
  397 + ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  398 + .eq(Orders::getId, orderId)
  399 + .eq(Orders::getRiderId, riderId)
  400 + .eq(Orders::getStatus, 3)
  401 + .set(Orders::getIsTrans, 1) // 直接转单成功
  402 + .set(Orders::getStatus, 2) // 回到待接单
  403 + .set(Orders::getRiderId, 0L) // 清空骑手
  404 + .set(Orders::getGrapTime, 0L)
  405 + .set(Orders::getIsIncome, 0)
  406 + .set(Orders::getRiderIncome, BigDecimal.ZERO)
  407 + .set(Orders::getSubstationIncome, BigDecimal.ZERO)
  408 + .set(Orders::getTransTime, now));
  409 + } else {
  410 + // status=4(取货后配送中):需分站审批,设为申请中
  411 + ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  412 + .eq(Orders::getId, orderId)
  413 + .eq(Orders::getRiderId, riderId)
  414 + .eq(Orders::getStatus, 4)
  415 + .set(Orders::getIsTrans, 2) // 申请中,等待审批
  416 + .set(Orders::getTransTime, now));
  417 + }
  418 +
  419 + // 累加骑手转单统计
  420 + countMapper.upsertCount(riderId, countDate, 0, 0, 1);
  421 + }
  422 +
  423 + @Override
  424 + public RiderTodayCountVO getTodayCount(Long riderId) {
  425 + long todayStart = LocalDate.now().atStartOfDay(ZoneId.of("Asia/Shanghai")).toEpochSecond();
  426 + int countDate = Integer.parseInt(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
  427 +
  428 + RiderOrderCount count = countMapper.selectOne(
  429 + new LambdaQueryWrapper<RiderOrderCount>()
  430 + .eq(RiderOrderCount::getUid, riderId)
  431 + .eq(RiderOrderCount::getCountDate, countDate)
  432 + .last("LIMIT 1"));
  433 +
  434 + RiderTodayCountVO vo = new RiderTodayCountVO();
  435 + if (count != null) {
  436 + vo.setOrders(count.getOrders());
  437 + vo.setTransfers(count.getTransfers());
  438 + vo.setDistance(count.getDistance());
  439 + } else {
  440 + vo.setOrders(0);
  441 + vo.setTransfers(0);
  442 + vo.setDistance(0L);
  443 + }
  444 + // 今日抢单数(今日接单总数, getGrapNums)
  445 + long todayStartTs = LocalDate.now().atStartOfDay(ZoneId.of("Asia/Shanghai")).toEpochSecond();
  446 + long todayEndTs = todayStartTs + 86400;
  447 + int graps = ordersMapper.selectCount(new LambdaQueryWrapper<Orders>()
  448 + .eq(Orders::getOldRiderId, riderId)
  449 + .ge(Orders::getGrapTime, todayStartTs)
  450 + .lt(Orders::getGrapTime, todayEndTs)).intValue();
  451 + vo.setGraps(graps);
  452 + return vo;
  453 + }
  454 +
  455 + @Override
  456 + public List<RiderMonthCountVO> getMonthCount(Long riderId, int year) {
  457 + if (year == 0) year = LocalDate.now().getYear();
  458 + int startDate = year * 10000 + 101; // yyyyMMdd 格式起始
  459 + int endDate = (year + 1) * 10000 + 101;
  460 +
  461 + List<RiderOrderCount> list = countMapper.selectList(
  462 + new LambdaQueryWrapper<RiderOrderCount>()
  463 + .eq(RiderOrderCount::getUid, riderId)
  464 + .ge(RiderOrderCount::getCountDate, startDate)
  465 + .lt(RiderOrderCount::getCountDate, endDate)
  466 + .orderByAsc(RiderOrderCount::getCountDate));
  467 +
  468 + int currentYearMonth = Integer.parseInt(
  469 + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")));
  470 +
  471 + List<RiderMonthCountVO> result = new ArrayList<>();
  472 + for (var c : list) {
  473 + RiderMonthCountVO vo = new RiderMonthCountVO();
  474 + int ym = c.getCountDate() / 100; // yyyyMM
  475 + int month = ym % 100;
  476 + if (ym == currentYearMonth) {
  477 + vo.setTitle("本月");
  478 + vo.setDes(String.format("%02d-01至%s", month,
  479 + LocalDate.now().format(DateTimeFormatter.ofPattern("MM-dd"))));
  480 + } else {
  481 + vo.setTitle(month + "月");
  482 + vo.setDes("");
  483 + }
  484 + vo.setOrders(c.getOrders());
  485 + vo.setTransfers(c.getTransfers());
  486 + vo.setDistance(c.getDistance());
  487 + result.add(vo);
  488 + }
  489 + return result;
  490 + }
  491 +
  492 + @Override
  493 + public List<OrderVO> getCountList(Long riderId, int type, int page) {
  494 + // getCountList:以 oldriderid 为维度查询骑手历史订单
  495 + // type=0全部(完成+转单) 1已完成 2已转单
  496 + if (!List.of(0, 1, 2).contains(type)) return List.of();
  497 +
  498 + LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<Orders>()
  499 + .eq(Orders::getOldRiderId, riderId)
  500 + .orderByDesc(Orders::getId);
  501 +
  502 + if (type == 0) {
  503 + // 全部:(status=6 且 isTrans=0) 或 isTrans=1(转单成功)
  504 + wrapper.and(w -> w
  505 + .nested(n -> n.eq(Orders::getStatus, 6).eq(Orders::getIsTrans, 0))
  506 + .or()
  507 + .eq(Orders::getIsTrans, 1));
  508 + } else if (type == 1) {
  509 + wrapper.eq(Orders::getStatus, 6).eq(Orders::getIsTrans, 0);
  510 + } else {
  511 + wrapper.eq(Orders::getIsTrans, 1);
  512 + }
  513 +
  514 + int offset = (page - 1) * PAGE_SIZE;
  515 + wrapper.last("LIMIT " + offset + "," + PAGE_SIZE);
  516 +
  517 + List<Orders> orders = ordersMapper.selectList(wrapper);
  518 + List<OrderVO> result = new ArrayList<>();
  519 + for (Orders o : orders) result.add(toVO(o, riderId));
  520 + return result;
  521 + }
  522 +}
... ...
src/main/java/com/diligrp/rider/service/impl/SubstationServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/SubstationServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.exception.BizException;
  6 +import com.diligrp.rider.entity.Substation;
  7 +import com.diligrp.rider.mapper.SubstationMapper;
  8 +import com.diligrp.rider.service.SubstationService;
  9 +import lombok.RequiredArgsConstructor;
  10 +import org.springframework.stereotype.Service;
  11 +import org.springframework.util.DigestUtils;
  12 +
  13 +import java.nio.charset.StandardCharsets;
  14 +import java.util.List;
  15 +
  16 +@Service
  17 +@RequiredArgsConstructor
  18 +public class SubstationServiceImpl implements SubstationService {
  19 +
  20 + private final SubstationMapper substationMapper;
  21 +
  22 + @Override
  23 + public List<Substation> list(String keyword) {
  24 + LambdaQueryWrapper<Substation> wrapper = new LambdaQueryWrapper<Substation>()
  25 + .orderByDesc(Substation::getId);
  26 + if (keyword != null && !keyword.isBlank()) {
  27 + wrapper.like(Substation::getUserLogin, keyword)
  28 + .or().like(Substation::getUserNickname, keyword)
  29 + .or().like(Substation::getMobile, keyword);
  30 + }
  31 + return substationMapper.selectList(wrapper);
  32 + }
  33 +
  34 + @Override
  35 + public void add(Substation substation) {
  36 + if (substation.getCityId() == null || substation.getCityId() < 1) {
  37 + throw new BizException("请选择管理城市");
  38 + }
  39 +
  40 + Long loginExists = substationMapper.selectCount(new LambdaQueryWrapper<Substation>()
  41 + .eq(Substation::getUserLogin, substation.getUserLogin()));
  42 + if (loginExists > 0) throw new BizException("账号已存在,请更换");
  43 +
  44 + substation.setUserPass(encryptPass(substation.getUserPass()));
  45 + substation.setUserStatus(1);
  46 + substation.setCreateTime(System.currentTimeMillis() / 1000);
  47 + substationMapper.insert(substation);
  48 + }
  49 +
  50 + @Override
  51 + public void edit(Substation substation) {
  52 + Substation existing = substationMapper.selectById(substation.getId());
  53 + if (existing == null) throw new BizException("分站管理员不存在");
  54 + // 密码为空则不更新
  55 + if (substation.getUserPass() == null || substation.getUserPass().isBlank()) {
  56 + substation.setUserPass(null);
  57 + } else {
  58 + substation.setUserPass(encryptPass(substation.getUserPass()));
  59 + }
  60 + substationMapper.updateById(substation);
  61 + }
  62 +
  63 + @Override
  64 + public void ban(Long id) {
  65 + substationMapper.update(null, new LambdaUpdateWrapper<Substation>()
  66 + .eq(Substation::getId, id).set(Substation::getUserStatus, 0));
  67 + }
  68 +
  69 + @Override
  70 + public void cancelBan(Long id) {
  71 + substationMapper.update(null, new LambdaUpdateWrapper<Substation>()
  72 + .eq(Substation::getId, id).set(Substation::getUserStatus, 1));
  73 + }
  74 +
  75 + @Override
  76 + public void del(Long id) {
  77 + substationMapper.deleteById(id);
  78 + }
  79 +
  80 + @Override
  81 + public Substation getByCityId(Long cityId) {
  82 + return substationMapper.selectOne(new LambdaQueryWrapper<Substation>()
  83 + .eq(Substation::getCityId, cityId).last("LIMIT 1"));
  84 + }
  85 +
  86 + @Override
  87 + public void changePassword(Long substationId, String oldPassword, String newPassword) {
  88 + Substation sub = substationMapper.selectById(substationId);
  89 + if (sub == null) throw new BizException("账号不存在");
  90 + if (!encryptPass(oldPassword).equals(sub.getUserPass())) {
  91 + throw new BizException("原密码不正确");
  92 + }
  93 + if (encryptPass(newPassword).equals(sub.getUserPass())) {
  94 + throw new BizException("新密码不能与原密码相同");
  95 + }
  96 + substationMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Substation>()
  97 + .eq(Substation::getId, substationId)
  98 + .set(Substation::getUserPass, encryptPass(newPassword)));
  99 + }
  100 +
  101 + private String encryptPass(String pass) {
  102 + return DigestUtils.md5DigestAsHex(pass.getBytes(StandardCharsets.UTF_8));
  103 + }
  104 +}
... ...
src/main/java/com/diligrp/rider/service/impl/WebhookServiceImpl.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/service/impl/WebhookServiceImpl.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.fasterxml.jackson.databind.ObjectMapper;
  5 +import com.diligrp.rider.entity.OpenApp;
  6 +import com.diligrp.rider.entity.WebhookLog;
  7 +import com.diligrp.rider.mapper.OpenAppMapper;
  8 +import com.diligrp.rider.mapper.WebhookLogMapper;
  9 +import com.diligrp.rider.service.WebhookService;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.springframework.scheduling.annotation.Async;
  13 +import org.springframework.stereotype.Service;
  14 +
  15 +import java.net.URI;
  16 +import java.net.http.HttpClient;
  17 +import java.net.http.HttpRequest;
  18 +import java.net.http.HttpResponse;
  19 +import java.time.Duration;
  20 +import java.util.List;
  21 +
  22 +@Slf4j
  23 +@Service
  24 +@RequiredArgsConstructor
  25 +public class WebhookServiceImpl implements WebhookService {
  26 +
  27 + private final OpenAppMapper openAppMapper;
  28 + private final WebhookLogMapper webhookLogMapper;
  29 + private final ObjectMapper objectMapper;
  30 +
  31 + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
  32 + .connectTimeout(Duration.ofSeconds(5))
  33 + .build();
  34 +
  35 + @Override
  36 + @Async
  37 + public void send(String event, Long bizId, String payload) {
  38 + // 查找订阅该事件的所有应用
  39 + List<OpenApp> apps = openAppMapper.selectList(
  40 + new LambdaQueryWrapper<OpenApp>().eq(OpenApp::getStatus, 1));
  41 +
  42 + for (OpenApp app : apps) {
  43 + if (app.getWebhookUrl() == null || app.getWebhookUrl().isBlank()) continue;
  44 + if (!isSubscribed(app.getWebhookEvents(), event)) continue;
  45 + doSend(app, event, bizId, payload, 0);
  46 + }
  47 + }
  48 +
  49 + @Override
  50 + public void retry(Long logId) {
  51 + WebhookLog log = webhookLogMapper.selectById(logId);
  52 + if (log == null || log.getStatus() == 1) return;
  53 + OpenApp app = openAppMapper.selectById(log.getAppId());
  54 + if (app == null) return;
  55 + doSend(app, log.getEvent(), log.getBizId(), log.getPayload(), log.getRetryCount() + 1);
  56 + }
  57 +
  58 + private void doSend(OpenApp app, String event, Long bizId, String payload, int retryCount) {
  59 + WebhookLog webhookLog = new WebhookLog();
  60 + webhookLog.setAppId(app.getId());
  61 + webhookLog.setEvent(event);
  62 + webhookLog.setBizId(bizId);
  63 + webhookLog.setUrl(app.getWebhookUrl());
  64 + webhookLog.setPayload(payload);
  65 + webhookLog.setRetryCount(retryCount);
  66 + webhookLog.setCreateTime(System.currentTimeMillis() / 1000);
  67 +
  68 + int responseCode = 0;
  69 + String responseBody = "";
  70 + int status = 0;
  71 +
  72 + try {
  73 + HttpRequest request = HttpRequest.newBuilder()
  74 + .uri(URI.create(app.getWebhookUrl()))
  75 + .header("Content-Type", "application/json")
  76 + .header("X-App-Key", app.getAppKey())
  77 + .header("X-Event", event)
  78 + .header("X-Timestamp", String.valueOf(System.currentTimeMillis() / 1000))
  79 + .POST(HttpRequest.BodyPublishers.ofString(payload))
  80 + .timeout(Duration.ofSeconds(10))
  81 + .build();
  82 +
  83 + HttpResponse<String> response = HTTP_CLIENT.send(request,
  84 + HttpResponse.BodyHandlers.ofString());
  85 + responseCode = response.statusCode();
  86 + responseBody = response.body();
  87 + if (responseCode == 200) status = 1;
  88 + } catch (Exception e) {
  89 + log.warn("Webhook 发送失败 appId={} event={} err={}", app.getId(), event, e.getMessage());
  90 + responseBody = e.getMessage();
  91 + }
  92 +
  93 + webhookLog.setResponseCode(responseCode);
  94 + webhookLog.setResponseBody(responseBody.length() > 500 ? responseBody.substring(0, 500) : responseBody);
  95 + webhookLog.setStatus(status);
  96 + webhookLogMapper.insert(webhookLog);
  97 + }
  98 +
  99 + /** 检查应用是否订阅了某事件 */
  100 + private boolean isSubscribed(String webhookEvents, String event) {
  101 + if (webhookEvents == null || webhookEvents.isBlank()) return false;
  102 + try {
  103 + List<String> events = objectMapper.readValue(webhookEvents,
  104 + objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
  105 + return events.contains(event) || events.contains("*");
  106 + } catch (Exception e) {
  107 + return false;
  108 + }
  109 + }
  110 +}
... ...
src/main/java/com/diligrp/rider/task/OrderScheduleTask.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/task/OrderScheduleTask.java
  1 +package com.diligrp.rider.task;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.fasterxml.jackson.databind.ObjectMapper;
  6 +import com.diligrp.rider.entity.Orders;
  7 +import com.diligrp.rider.mapper.OrdersMapper;
  8 +import com.diligrp.rider.service.WebhookService;
  9 +import lombok.RequiredArgsConstructor;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.scheduling.annotation.EnableScheduling;
  12 +import org.springframework.scheduling.annotation.Scheduled;
  13 +import org.springframework.stereotype.Component;
  14 +
  15 +import java.util.HashMap;
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
  19 +/**
  20 + * 订单定时任务
  21 + * OrderhandleCron(每3秒执行)
  22 + */
  23 +@Slf4j
  24 +@Component
  25 +@EnableScheduling
  26 +@RequiredArgsConstructor
  27 +public class OrderScheduleTask {
  28 +
  29 + private final OrdersMapper ordersMapper;
  30 + private final WebhookService webhookService;
  31 + private final ObjectMapper objectMapper;
  32 +
  33 + /**
  34 + * 超时未接单订单自动取消(30分钟)
  35 + * Orders::cancel()
  36 + * 每分钟执行一次
  37 + */
  38 + @Scheduled(fixedDelay = 60_000)
  39 + public void autoCancelTimeout() {
  40 + try {
  41 + long expireTime = System.currentTimeMillis() / 1000 - 30 * 60;
  42 + List<Orders> timeoutOrders = ordersMapper.selectList(
  43 + new LambdaQueryWrapper<Orders>()
  44 + .eq(Orders::getStatus, 2)
  45 + .le(Orders::getAddTime, expireTime));
  46 +
  47 + for (Orders order : timeoutOrders) {
  48 + int updated = ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  49 + .eq(Orders::getId, order.getId())
  50 + .eq(Orders::getStatus, 2)
  51 + .set(Orders::getStatus, 10));
  52 + if (updated > 0) {
  53 + log.info("订单超时自动取消 orderId={}", order.getId());
  54 + // 通知接入方
  55 + notifyCancel(order);
  56 + }
  57 + }
  58 + } catch (Exception e) {
  59 + log.error("超时取消任务异常", e);
  60 + }
  61 + }
  62 +
  63 + /**
  64 + * 检查骑手是否有新的指派订单(每3秒,dispatchNotice)
  65 + * 注:实时推送版本需 WebSocket,此处仅做日志记录
  66 + * 后续接入 WebSocket 后可在此触发推送
  67 + */
  68 + @Scheduled(fixedDelay = 3_000)
  69 + public void checkDispatchOrders() {
  70 + // TODO: 接入 WebSocket 后在此推送给骑手
  71 + // 目前骑手通过轮询 /api/rider/order/list?type=1 获取待接单列表
  72 + }
  73 +
  74 + private void notifyCancel(Orders order) {
  75 + try {
  76 + if (order.getAppKey() == null || order.getAppKey().isBlank()) return;
  77 + Map<String, Object> payload = new HashMap<>();
  78 + payload.put("event", "order.cancelled");
  79 + payload.put("outOrderNo", order.getOutOrderNo());
  80 + payload.put("deliveryOrderId", order.getId());
  81 + payload.put("orderNo", order.getOrderNo());
  82 + payload.put("status", 10);
  83 + payload.put("reason", "超时无人接单,系统自动取消");
  84 + payload.put("timestamp", System.currentTimeMillis() / 1000);
  85 + webhookService.send("order.cancelled", order.getId(),
  86 + objectMapper.writeValueAsString(payload));
  87 + } catch (Exception e) {
  88 + log.warn("取消通知失败 orderId={}", order.getId(), e);
  89 + }
  90 + }
  91 +}
... ...
src/main/java/com/diligrp/rider/util/GeoUtil.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/util/GeoUtil.java
  1 +package com.diligrp.rider.util;
  2 +
  3 +/**
  4 + * 地理距离计算工具
  5 + * getDistance() 函数,使用 Haversine 公式
  6 + */
  7 +public class GeoUtil {
  8 +
  9 + private static final double EARTH_RADIUS_KM = 6371.0;
  10 +
  11 + /**
  12 + * 计算两点之间的距离(千米)
  13 + * @param lat1 起点纬度
  14 + * @param lng1 起点经度
  15 + * @param lat2 终点纬度
  16 + * @param lng2 终点经度
  17 + * @return 距离(km),保留1位小数
  18 + */
  19 + public static double calcDistanceKm(double lat1, double lng1, double lat2, double lng2) {
  20 + double dLat = Math.toRadians(lat2 - lat1);
  21 + double dLng = Math.toRadians(lng2 - lng1);
  22 + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
  23 + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
  24 + * Math.sin(dLng / 2) * Math.sin(dLng / 2);
  25 + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  26 + double distKm = EARTH_RADIUS_KM * c;
  27 + // 保留1位小数
  28 + return Math.round(distKm * 10.0) / 10.0;
  29 + }
  30 +}
... ...
src/main/java/com/diligrp/rider/util/SignUtil.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/util/SignUtil.java
  1 +package com.diligrp.rider.util;
  2 +
  3 +import javax.crypto.Mac;
  4 +import javax.crypto.spec.SecretKeySpec;
  5 +import java.nio.charset.StandardCharsets;
  6 +import java.util.UUID;
  7 +
  8 +/**
  9 + * 开放平台签名工具
  10 + * 签名算法:HmacSHA256
  11 + * 签名字符串:appKey + timestamp + nonce,按字典序拼接后用 AppSecret 做 HMAC-SHA256
  12 + */
  13 +public class SignUtil {
  14 +
  15 + /**
  16 + * 生成签名
  17 + * @param appKey 应用Key
  18 + * @param timestamp 时间戳(秒)
  19 + * @param nonce 随机字符串
  20 + * @param appSecret 应用密钥
  21 + */
  22 + public static String sign(String appKey, String timestamp, String nonce, String appSecret) {
  23 + // 拼接待签名字符串(字典序排列)
  24 + String[] parts = {appKey, timestamp, nonce};
  25 + java.util.Arrays.sort(parts);
  26 + String signStr = String.join("", parts);
  27 + return hmacSha256(signStr, appSecret);
  28 + }
  29 +
  30 + public static boolean verify(String appKey, String timestamp, String nonce,
  31 + String sign, String appSecret) {
  32 + // 防重放:timestamp 与当前时间差不超过5分钟
  33 + try {
  34 + long ts = Long.parseLong(timestamp);
  35 + long now = System.currentTimeMillis() / 1000;
  36 + if (Math.abs(now - ts) > 300) return false;
  37 + } catch (NumberFormatException e) {
  38 + return false;
  39 + }
  40 + String expected = sign(appKey, timestamp, nonce, appSecret);
  41 + return expected.equalsIgnoreCase(sign);
  42 + }
  43 +
  44 + /** 生成随机 AppKey(32位) */
  45 + public static String generateAppKey() {
  46 + return UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
  47 + }
  48 +
  49 + /** 生成随机 AppSecret(64位) */
  50 + public static String generateAppSecret() {
  51 + return UUID.randomUUID().toString().replace("-", "")
  52 + + UUID.randomUUID().toString().replace("-", "");
  53 + }
  54 +
  55 + private static String hmacSha256(String data, String key) {
  56 + try {
  57 + Mac mac = Mac.getInstance("HmacSHA256");
  58 + mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
  59 + byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
  60 + StringBuilder sb = new StringBuilder();
  61 + for (byte b : bytes) sb.append(String.format("%02x", b));
  62 + return sb.toString();
  63 + } catch (Exception e) {
  64 + throw new RuntimeException("签名计算失败", e);
  65 + }
  66 + }
  67 +
  68 + public static void main(String[] args) {
  69 + System.out.println(SignUtil.sign("8444D338919C4498","1774854151","111","7e0d80b82acb426e90a8b74ad4f23114500e65737ed34ce4803710cd2d4a324e"));
  70 + }
  71 +}
... ...
src/main/java/com/diligrp/rider/vo/AdminLoginVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/AdminLoginVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class AdminLoginVO {
  7 + private Long id;
  8 + private String userLogin;
  9 + private String userNickname;
  10 + /** 角色:admin / substation */
  11 + private String role;
  12 + /** 分站关联城市ID(substation角色才有) */
  13 + private Long cityId;
  14 + private String token;
  15 +}
... ...
src/main/java/com/diligrp/rider/vo/BalanceVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/BalanceVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.math.BigDecimal;
  6 +import java.util.List;
  7 +
  8 +@Data
  9 +public class BalanceVO {
  10 + /** 当前余额 */
  11 + private BigDecimal balance;
  12 + /** 流水列表 */
  13 + private List<BalanceRecordVO> records;
  14 +
  15 + @Data
  16 + public static class BalanceRecordVO {
  17 + private Long id;
  18 + private Integer type;
  19 + private String typeName;
  20 + private String action;
  21 + private String orderNo;
  22 + private BigDecimal nums;
  23 + private BigDecimal total;
  24 + private String addTime;
  25 + }
  26 +}
... ...
src/main/java/com/diligrp/rider/vo/CityVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/CityVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.math.BigDecimal;
  6 +import java.util.List;
  7 +
  8 +@Data
  9 +public class CityVO {
  10 + private Long id;
  11 + private Long pid;
  12 + private String name;
  13 + private String areaCode;
  14 + private Integer status;
  15 + private String statusName;
  16 + private BigDecimal rate;
  17 + private Integer listOrder;
  18 + /** 下级城市(市级列表) */
  19 + private List<CityVO> children;
  20 +}
... ...
src/main/java/com/diligrp/rider/vo/DeliveryFeePlanDetailVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/DeliveryFeePlanDetailVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
  4 +import lombok.Data;
  5 +import lombok.EqualsAndHashCode;
  6 +
  7 +@Data
  8 +@EqualsAndHashCode(callSuper = true)
  9 +public class DeliveryFeePlanDetailVO extends DeliveryFeePlanVO {
  10 +
  11 + private DeliveryPricingConfigDTO config;
  12 +}
... ...
src/main/java/com/diligrp/rider/vo/DeliveryFeePlanVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/DeliveryFeePlanVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class DeliveryFeePlanVO {
  7 +
  8 + private Long id;
  9 +
  10 + private Long cityId;
  11 +
  12 + private String name;
  13 +
  14 + private Integer isDefault;
  15 +
  16 + private Integer status;
  17 +
  18 + private Integer listOrder;
  19 +
  20 + private String remark;
  21 +
  22 + private Long createTime;
  23 +
  24 + private Long updateTime;
  25 +}
... ...
src/main/java/com/diligrp/rider/vo/DeliveryFeeResultVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/DeliveryFeeResultVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.math.BigDecimal;
  6 +
  7 +/**
  8 + * 配送费计算结果 VO
  9 + * Helpsend.computed() 返回值
  10 + */
  11 +@Data
  12 +public class DeliveryFeeResultVO {
  13 +
  14 + /** 基础距离费(起步距离内) */
  15 + private BigDecimal moneyBasic;
  16 +
  17 + /** 基础距离说明,如"(3km)" */
  18 + private String moneyBasicTxt;
  19 +
  20 + /** 超出距离费 */
  21 + private BigDecimal moneyDistance;
  22 +
  23 + /** 超出距离说明,如"(2.5km)" */
  24 + private String moneyDistanceTxt;
  25 +
  26 + /** 超重费 */
  27 + private BigDecimal moneyWeight;
  28 +
  29 + /** 超重说明 */
  30 + private String moneyWeightTxt;
  31 +
  32 + /** 件数费 */
  33 + private BigDecimal moneyPiece;
  34 +
  35 + /** 件数费说明 */
  36 + private String moneyPieceTxt;
  37 +
  38 + /** 时段附加费 */
  39 + private BigDecimal moneyTime;
  40 +
  41 + /** 保底费用 */
  42 + private BigDecimal minFee;
  43 +
  44 + /** 是否命中保底 */
  45 + private Integer minFeeApplied;
  46 +
  47 + /** 合计配送费 */
  48 + private BigDecimal totalFee;
  49 +
  50 + /** 实际配送距离(km) */
  51 + private BigDecimal distance;
  52 +
  53 + /** 实际重量(kg) */
  54 + private BigDecimal weight;
  55 +
  56 + /** 实际件数 */
  57 + private Integer pieces;
  58 +
  59 + /** 预计送达时间(分钟) */
  60 + private Integer estimatedMinutes;
  61 +}
... ...
src/main/java/com/diligrp/rider/vo/DeliveryOrderCreateVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/DeliveryOrderCreateVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.math.BigDecimal;
  6 +
  7 +/**
  8 + * 创建配送订单返回 VO
  9 + */
  10 +@Data
  11 +public class DeliveryOrderCreateVO {
  12 + /** 配送中台订单ID */
  13 + private Long deliveryOrderId;
  14 + /** 配送中台订单号 */
  15 + private String orderNo;
  16 + /** 外部系统订单号 */
  17 + private String outOrderNo;
  18 + /** 配送费明细:基础费 */
  19 + private BigDecimal moneyBasic;
  20 + /** 配送费明细:超距费 */
  21 + private BigDecimal moneyDistance;
  22 + /** 配送费明细:时段附加费 */
  23 + private BigDecimal moneyTime;
  24 + /** 总配送费 */
  25 + private BigDecimal totalFee;
  26 + /** 配送距离(km) */
  27 + private BigDecimal distance;
  28 + /** 预计送达时间(分钟) */
  29 + private Integer estimatedMinutes;
  30 + /** 订单状态:2=待接单 */
  31 + private Integer status;
  32 +}
... ...
src/main/java/com/diligrp/rider/vo/NearbyRiderVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/NearbyRiderVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 附近骑手信息 VO
  7 + */
  8 +@Data
  9 +public class NearbyRiderVO {
  10 + /** 经度 */
  11 + private String lng;
  12 + /** 纬度 */
  13 + private String lat;
  14 + /** 与查询点的距离(km) */
  15 + private Double distance;
  16 +}
... ...
src/main/java/com/diligrp/rider/vo/OrderVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/OrderVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +import java.util.List;
  8 +
  9 +@Data
  10 +public class OrderVO {
  11 + private Long id;
  12 + private String orderNo;
  13 + private String outOrderNo;
  14 + private Integer type;
  15 + private String typeName;
  16 + private Integer status;
  17 + private String statusName;
  18 + @JsonProperty("fName")
  19 + private String fName;
  20 + @JsonProperty("fAddr")
  21 + private String fAddr;
  22 + @JsonProperty("fLng")
  23 + private String fLng;
  24 + @JsonProperty("fLat")
  25 + private String fLat;
  26 + @JsonProperty("tName")
  27 + private String tName;
  28 + @JsonProperty("tAddr")
  29 + private String tAddr;
  30 + @JsonProperty("tLng")
  31 + private String tLng;
  32 + @JsonProperty("tLat")
  33 + private String tLat;
  34 + private String recipName;
  35 + private String recipPhone;
  36 + private BigDecimal income;
  37 + private Integer isTrans;
  38 + private String addTime;
  39 + private String grapTime;
  40 + private String pickTime;
  41 + private String completeTime;
  42 + private String transTime;
  43 + private Object extra;
  44 + /** 货物清单 */
  45 + private List<Object> items;
  46 + /** 整单货物备注 */
  47 + private String itemRemark;
  48 + /** 订单备注(from extra.remark) */
  49 + private String des;
  50 + /** 预约标识 */
  51 + private Integer isPre;
  52 + /** 服务时间 */
  53 + private String service_time;
  54 +}
... ...
src/main/java/com/diligrp/rider/vo/RiderMonthCountVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/RiderMonthCountVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class RiderMonthCountVO {
  7 + /** 月份标题,如"3月",本月显示"本月" */
  8 + private String title;
  9 + /** 日期区间描述,如"03-01至03-25" */
  10 + private String des;
  11 + /** 完成订单数 */
  12 + private Integer orders;
  13 + /** 转单数 */
  14 + private Integer transfers;
  15 + /** 配送距离(米) */
  16 + private Long distance;
  17 + /** 统计月份时间戳 */
  18 + private Long time;
  19 +}
... ...
src/main/java/com/diligrp/rider/vo/RiderTodayCountVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/RiderTodayCountVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class RiderTodayCountVO {
  7 + /** 今日完成订单数 */
  8 + private Integer orders;
  9 + /** 今日转单数 */
  10 + private Integer transfers;
  11 + /** 今日配送距离(米) */
  12 + private Long distance;
  13 + /** 今日抢单数(今日接单总数) */
  14 + private Integer graps;
  15 +}
... ...
src/main/java/com/diligrp/rider/vo/RiderVO.java 0 → 100644
  1 +++ a/src/main/java/com/diligrp/rider/vo/RiderVO.java
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.math.BigDecimal;
  6 +
  7 +@Data
  8 +public class RiderVO {
  9 + private Long id;
  10 + private String mobile;
  11 + private String userNickname;
  12 + private String avatar;
  13 + private Integer type;
  14 + private String typeName;
  15 + private Integer userStatus;
  16 + private Integer status;
  17 + private String statusName;
  18 + private BigDecimal balance;
  19 + private Integer isRest;
  20 + private Long cityId;
  21 + private String token;
  22 +}
... ...
src/main/resources/application.yml 0 → 100644
  1 +++ a/src/main/resources/application.yml
  1 +server:
  2 + port: 8080
  3 +
  4 +spring:
  5 + datasource:
  6 + driver-class-name: com.mysql.cj.jdbc.Driver
  7 + url: jdbc:mysql://mysql.diligrp.com:3306/dili_rider?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
  8 + username: root
  9 + password: OTM0NjAwMTMyMjZlNzgy
  10 + data:
  11 + redis:
  12 + host: redis.diligrp.com
  13 + port: 6379
  14 + password:
  15 + database: 0
  16 + timeout: 3000ms
  17 +
  18 +mybatis-plus:
  19 + mapper-locations: classpath:mapper/*.xml
  20 + type-aliases-package: com.diligrp.rider.entity
  21 + configuration:
  22 + map-underscore-to-camel-case: true
  23 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  24 + global-config:
  25 + db-config:
  26 + logic-delete-field: isDel
  27 + logic-delete-value: 1
  28 + logic-not-delete-value: 0
  29 +
  30 +jwt:
  31 + secret: diligrp-rider-secret-key-2024-please-change-this
  32 + expire: 604800 # 7天,单位秒
  33 +
  34 +logging:
  35 + level:
  36 + com.diligrp.rider: debug
... ...
src/main/resources/data-init.sql 0 → 100644
  1 +++ a/src/main/resources/data-init.sql
  1 +-- 外卖配送中台 初始化数据脚本
  2 +-- 执行前请确保已执行 schema.sql 建表
  3 +-- 执行方式:mysql -u root -p dili_rider < data-init.sql
  4 +
  5 +USE dili_rider;
  6 +
  7 +-- ============================================================
  8 +-- 1. 城市数据(省+市两级)
  9 +-- ============================================================
  10 +INSERT INTO `city` (`id`, `pid`, `name`, `area_code`, `status`, `rate`, `list_order`, `config`) VALUES
  11 +(1, 0, '广东省', '44000000', 0, 0.00, 1, NULL),
  12 +(2, 1, '广州市', '44010000', 1, 10.00, 1,
  13 +'{
  14 + "type": [6],
  15 + "type6": {
  16 + "feeMode": 2,
  17 + "fixMoney": 0,
  18 + "distanceSwitch": 1,
  19 + "distanceBasic": 3,
  20 + "distanceBasicMoney": 4.00,
  21 + "distanceMoreMoney": 1.50,
  22 + "distanceMode": 1,
  23 + "distanceType": 1,
  24 + "weightSwitch": 0,
  25 + "weightBasic": 0,
  26 + "weightBasicMoney": 0,
  27 + "weightMoreMoney": 0,
  28 + "weightType": 1,
  29 + "times": [
  30 + {"start": 0, "end": 480, "isOpen": 0, "money": 0},
  31 + {"start": 480, "end": 1320, "isOpen": 1, "money": 0},
  32 + {"start": 1320, "end": 1440, "isOpen": 1, "money": 2}
  33 + ]
  34 + },
  35 + "distanceBasic": 3,
  36 + "distanceBasicTime": 30,
  37 + "distanceMoreTime": 10
  38 +}'),
  39 +(3, 1, '深圳市', '44030000', 1, 10.00, 2,
  40 +'{
  41 + "type": [6],
  42 + "type6": {
  43 + "feeMode": 1,
  44 + "fixMoney": 5.00,
  45 + "distanceSwitch": 0,
  46 + "distanceBasic": 0,
  47 + "distanceBasicMoney": 0,
  48 + "distanceMoreMoney": 0,
  49 + "distanceMode": 1,
  50 + "distanceType": 1,
  51 + "weightSwitch": 0,
  52 + "weightBasic": 0,
  53 + "weightBasicMoney": 0,
  54 + "weightMoreMoney": 0,
  55 + "weightType": 1,
  56 + "times": []
  57 + },
  58 + "distanceBasic": 3,
  59 + "distanceBasicTime": 25,
  60 + "distanceMoreTime": 8
  61 +}');
  62 +
  63 +-- ============================================================
  64 +-- 2. 分站管理员(每个已开通城市一个)
  65 +-- 默认密码均为 admin123(MD5: 0192023a7bbd73250516f069df18b500)
  66 +-- ============================================================
  67 +INSERT INTO `substation` (`city_id`, `user_login`, `user_nickname`, `user_pass`, `mobile`, `user_status`, `create_time`) VALUES
  68 +(2, 'gz_admin', '广州分站管理员', '0192023a7bbd73250516f069df18b500', '13800000001', 1, UNIX_TIMESTAMP()),
  69 +(3, 'sz_admin', '深圳分站管理员', '0192023a7bbd73250516f069df18b500', '13800000002', 1, UNIX_TIMESTAMP());
  70 +
  71 +-- ============================================================
  72 +-- 3. 骑手等级配置(广州)
  73 +-- ============================================================
  74 +INSERT INTO `rider_level` (`city_id`, `level_id`, `name`, `is_default`, `trans_nums`,
  75 + `run_fee_mode`, `run_fix_money`, `run_rate`, `distance_basic`, `distance_basic_money`,
  76 + `distance_more_money`, `distance_max_money`, `work_fee_mode`, `work_fix_money`, `work_rate`) VALUES
  77 +-- 普通骑手:按比例拿配送费的70%
  78 +(2, 1, '普通骑手', 1, 3,
  79 + 2, 0.00, 70.00, 0, 0.00, 0.00, 0.00,
  80 + 1, 5.00, 0.00),
  81 +-- 资深骑手:按比例拿配送费的80%
  82 +(2, 2, '资深骑手', 0, 5,
  83 + 2, 0.00, 80.00, 0, 0.00, 0.00, 0.00,
  84 + 1, 6.00, 0.00);
  85 +
  86 +-- 深圳骑手等级
  87 +INSERT INTO `rider_level` (`city_id`, `level_id`, `name`, `is_default`, `trans_nums`,
  88 + `run_fee_mode`, `run_fix_money`, `run_rate`, `distance_basic`, `distance_basic_money`,
  89 + `distance_more_money`, `distance_max_money`, `work_fee_mode`, `work_fix_money`, `work_rate`) VALUES
  90 +(3, 1, '普通骑手', 1, 3,
  91 + 2, 0.00, 70.00, 0, 0.00, 0.00, 0.00,
  92 + 1, 5.00, 0.00);
  93 +
  94 +-- ============================================================
  95 +-- 4. 示例骑手账号
  96 +-- 默认密码均为 test1234(MD5: 16d7a4fca7442dda3ad93c9a726597e4)
  97 +-- ============================================================
  98 +INSERT INTO `rider` (`mobile`, `user_login`, `user_nickname`, `user_pass`, `city_id`, `level_id`,
  99 + `type`, `user_status`, `balance`, `is_rest`, `create_time`) VALUES
  100 +('13900000001', 'phone_rider001', '张骑手', '16d7a4fca7442dda3ad93c9a726597e4', 2, 1, 1, 1, 0.00, 0, UNIX_TIMESTAMP()),
  101 +('13900000002', 'phone_rider002', '李骑手', '16d7a4fca7442dda3ad93c9a726597e4', 2, 1, 2, 1, 0.00, 0, UNIX_TIMESTAMP());
  102 +
  103 +-- ============================================================
  104 +-- 5. 示例商家店铺
  105 +-- ============================================================
  106 +INSERT INTO `merchant_store` (`name`, `thumb`, `city_id`, `address`, `lng`, `lat`,
  107 + `operating_state`, `automatic_order`, `shipping_type`, `free_shipping`, `up_to_send`,
  108 + `open_date`, `open_time`, `about`, `list_order`, `is_del`, `add_time`) VALUES
  109 +('测试餐厅', '', 2, '广州市天河区测试路1号', '113.330010', '23.132891',
  110 + 1, 1, 1, 30.00, 15.00,
  111 + '[1,2,3,4,5,6,7]', '["09:00","22:00"]', '测试店铺,仅供开发调试', 1, 0, UNIX_TIMESTAMP());
  112 +
  113 +-- 创建商家账号
  114 +INSERT INTO `merchant_users` (`store_id`, `mobile`, `user_nickname`, `user_status`, `type`, `create_time`)
  115 +VALUES (LAST_INSERT_ID(), '13700000001', '测试餐厅老板', 1, 1, UNIX_TIMESTAMP());
  116 +
  117 +-- ============================================================
  118 +-- 6. 开放平台示例应用
  119 +-- ============================================================
  120 +INSERT INTO `open_app` (`app_name`, `app_key`, `app_secret`, `store_id`, `status`,
  121 + `webhook_url`, `webhook_events`, `remark`, `create_time`) VALUES
  122 +('内部电商系统', 'TESTAPPKEY00001', 'testsecret0000000000000000000000000000000000000000000000000001', 0, 1,
  123 + '', '["order.paid","order.completed","order.cancelled"]', '用于测试的内部应用', UNIX_TIMESTAMP());
  124 +
  125 +-- ============================================================
  126 +-- 完成提示
  127 +-- ============================================================
  128 +SELECT '初始化完成!' AS 提示;
  129 +SELECT '骑手登录账号: 13900000001 / 13900000002,密码: test1234' AS 骑手账号;
  130 +SELECT '分站管理员: gz_admin / sz_admin,密码: admin123' AS 分站账号;
  131 +SELECT '商家手机号: 13700000001' AS 商家账号;
  132 +SELECT '开放平台 AppKey: TESTAPPKEY00001' AS 开放平台;
  133 +
  134 +-- ============================================================
  135 +-- 7. 超级管理员账号
  136 +-- 默认密码:admin123(MD5: 0192023a7bbd73250516f069df18b500)
  137 +-- ============================================================
  138 +INSERT INTO `admin_user` (`user_login`, `user_pass`, `user_nickname`, `user_status`, `create_time`) VALUES
  139 +('admin', '0192023a7bbd73250516f069df18b500', '超级管理员', 1, UNIX_TIMESTAMP());
  140 +
  141 +SELECT '超级管理员: admin / admin123(role=admin)' AS 超管账号;
... ...
src/main/resources/mapper/OrdersMapper.xml 0 → 100644
  1 +++ a/src/main/resources/mapper/OrdersMapper.xml
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="com.diligrp.rider.mapper.OrdersMapper">
  4 +
  5 + <select id="countTodayTrans" resultType="int">
  6 + SELECT COUNT(*)
  7 + FROM orders
  8 + WHERE old_rider_id = #{riderId}
  9 + AND is_trans = 1
  10 + AND trans_time >= #{todayStart}
  11 + </select>
  12 +
  13 +</mapper>
... ...
src/main/resources/mapper/RiderOrderCountMapper.xml 0 → 100644
  1 +++ a/src/main/resources/mapper/RiderOrderCountMapper.xml
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="com.diligrp.rider.mapper.RiderOrderCountMapper">
  4 +
  5 + <!--
  6 + 骑手当日统计 upsert:有则累加,无则插入
  7 + -->
  8 + <insert id="upsertCount">
  9 + INSERT INTO rider_order_count (uid, count_date, orders, distance, transfers)
  10 + VALUES (#{uid}, #{countDate}, #{orders}, #{distance}, #{transfers})
  11 + ON DUPLICATE KEY UPDATE
  12 + orders = orders + VALUES(orders),
  13 + distance = distance + VALUES(distance),
  14 + transfers = transfers + VALUES(transfers)
  15 + </insert>
  16 +
  17 +</mapper>
... ...
src/main/resources/mapper/RiderOrderRefuseMapper.xml 0 → 100644
  1 +++ a/src/main/resources/mapper/RiderOrderRefuseMapper.xml
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="com.diligrp.rider.mapper.RiderOrderRefuseMapper">
  4 +
  5 + <select id="selectRefuseOrderIds" resultType="long">
  6 + SELECT oid
  7 + FROM rider_orders_refuse
  8 + WHERE rider_id = #{riderId}
  9 + </select>
  10 +
  11 +</mapper>
... ...
src/main/resources/schema.sql 0 → 100644
  1 +++ a/src/main/resources/schema.sql
  1 +-- 外卖骑手配送模块 数据库建表脚本
  2 +-- 数据库:dili_rider
  3 +
  4 +CREATE DATABASE IF NOT EXISTS dili_rider DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  5 +USE dili_rider;
  6 +
  7 +-- 骑手信息表
  8 +CREATE TABLE `rider` (
  9 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '骑手ID',
  10 + `mobile` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号',
  11 + `user_login` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '登录名',
  12 + `user_nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称',
  13 + `user_pass` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '密码(MD5)',
  14 + `avatar` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像',
  15 + `avatar_thumb` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像缩略图',
  16 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '城市ID',
  17 + `level_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '等级ID',
  18 + `type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1=兼职 2=全职',
  19 + `user_status` TINYINT NOT NULL DEFAULT 2 COMMENT '审核状态:0=拒绝 1=通过 2=待审核',
  20 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '账号状态:0=禁用 1=正常',
  21 + `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '余额(兼职用)',
  22 + `is_rest` TINYINT NOT NULL DEFAULT 0 COMMENT '是否休息:0=否 1=是',
  23 + `id_no` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '身份证号',
  24 + `thumb` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '手持身份证照片',
  25 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '注册时间',
  26 + `is_del` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=正常 1=已删除',
  27 + PRIMARY KEY (`id`),
  28 + UNIQUE KEY `uk_mobile` (`mobile`),
  29 + KEY `idx_city_id` (`city_id`)
  30 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手信息表';
  31 +
  32 +-- 骑手等级配置表
  33 +CREATE TABLE `rider_level` (
  34 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  35 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '城市ID',
  36 + `level_id` INT NOT NULL DEFAULT 0 COMMENT '等级编号',
  37 + `name` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '等级名称',
  38 + `is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认',
  39 + `trans_nums` INT NOT NULL DEFAULT 0 COMMENT '每日转单次数上限',
  40 + `run_fee_mode` TINYINT NOT NULL DEFAULT 1 COMMENT '跑腿收入模式:1=固定 2=比例 3=距离',
  41 + `run_fix_money` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '跑腿固定金额',
  42 + `run_rate` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '跑腿比例(%)',
  43 + `distance_basic` INT NOT NULL DEFAULT 0 COMMENT '起始距离(米)',
  44 + `distance_basic_money` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '基础配送费',
  45 + `distance_more_money` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '超出每公里费',
  46 + `distance_max_money` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '最高配送费上限',
  47 + `work_fee_mode` TINYINT NOT NULL DEFAULT 1 COMMENT '办事收入模式:1=固定 2=比例',
  48 + `work_fix_money` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '办事固定金额',
  49 + `work_rate` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '办事比例(%)',
  50 + PRIMARY KEY (`id`),
  51 + UNIQUE KEY `uk_city_level` (`city_id`, `level_id`),
  52 + KEY `idx_city_default` (`city_id`, `is_default`)
  53 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手等级配置表';
  54 +
  55 +-- 骑手实时位置表
  56 +CREATE TABLE `rider_location` (
  57 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  58 + `uid` BIGINT UNSIGNED NOT NULL COMMENT '骑手ID',
  59 + `lng` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '经度',
  60 + `lat` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '纬度',
  61 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间',
  62 + PRIMARY KEY (`id`),
  63 + UNIQUE KEY `uk_uid` (`uid`)
  64 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手实时位置表';
  65 +
  66 +-- 骑手余额流水表
  67 +CREATE TABLE `rider_balance` (
  68 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  69 + `uid` BIGINT UNSIGNED NOT NULL COMMENT '骑手ID',
  70 + `type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1=收入 2=提现',
  71 + `action` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '动作标识',
  72 + `action_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联ID',
  73 + `order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '订单号',
  74 + `nums` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '变动金额',
  75 + `total` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '变动后余额',
  76 + `add_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '记录时间',
  77 + PRIMARY KEY (`id`),
  78 + KEY `idx_uid` (`uid`),
  79 + KEY `idx_action_id` (`action_id`)
  80 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手余额流水表';
  81 +
  82 +-- 骑手订单统计表
  83 +CREATE TABLE `rider_order_count` (
  84 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  85 + `uid` BIGINT UNSIGNED NOT NULL COMMENT '骑手ID',
  86 + `count_date` INT NOT NULL COMMENT '统计日期yyyyMMdd',
  87 + `orders` INT NOT NULL DEFAULT 0 COMMENT '完成订单数',
  88 + `transfers` INT NOT NULL DEFAULT 0 COMMENT '转单数',
  89 + `distance` BIGINT NOT NULL DEFAULT 0 COMMENT '配送距离(米)',
  90 + PRIMARY KEY (`id`),
  91 + UNIQUE KEY `uk_uid_date` (`uid`, `count_date`)
  92 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手订单统计表';
  93 +
  94 +-- 骑手拒单记录表
  95 +CREATE TABLE `rider_orders_refuse` (
  96 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  97 + `rider_id` BIGINT UNSIGNED NOT NULL COMMENT '骑手ID',
  98 + `oid` BIGINT UNSIGNED NOT NULL COMMENT '订单ID',
  99 + `add_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '拒单时间',
  100 + PRIMARY KEY (`id`),
  101 + KEY `idx_rider_id` (`rider_id`),
  102 + KEY `idx_oid` (`oid`)
  103 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手拒单记录表';
  104 +
  105 +-- 订单主表
  106 +CREATE TABLE `orders` (
  107 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  108 + `order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '订单号',
  109 + `uid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户ID',
  110 + `rider_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '骑手ID',
  111 + `old_rider_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '原始骑手ID',
  112 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '城市ID',
  113 + `type` TINYINT NOT NULL DEFAULT 6 COMMENT '订单类型:6=外卖配送',
  114 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1待支付 2已支付 3已接单 4服务中 6已完成 7退款申请 8退款成功 9退款拒绝 10已取消',
  115 + `pay_type` TINYINT NOT NULL DEFAULT 0 COMMENT '支付类型:1=支付宝 2=微信',
  116 + `money` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
  117 + `money_delivery` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '配送费',
  118 + `money_total` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '实付总金额',
  119 + `rider_income` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '骑手收入',
  120 + `substation_income` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '站点收入',
  121 + `is_income` TINYINT NOT NULL DEFAULT 0 COMMENT '结算状态:0=未结算 1=待结算 2=已结算',
  122 + `is_trans` TINYINT NOT NULL DEFAULT 0 COMMENT '转单状态:0=未转 1=通过 2=申请中 3=拒绝',
  123 + `code` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '完成码',
  124 + `f_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '起点名称',
  125 + `f_addr` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '起点地址',
  126 + `f_lng` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '起点经度',
  127 + `f_lat` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '起点纬度',
  128 + `t_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '终点名称',
  129 + `t_addr` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '终点地址',
  130 + `t_lng` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '终点经度',
  131 + `t_lat` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '终点纬度',
  132 + `recip_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '收件人姓名',
  133 + `recip_phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '收件人电话',
  134 + `extra` TEXT COMMENT '附加信息JSON(距离、重量等)',
  135 + `thumbs` TEXT COMMENT '取件照片JSON数组',
  136 + `store_oid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联店铺订单ID',
  137 + `is_del` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
  138 + `add_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '下单时间',
  139 + `pay_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付时间',
  140 + `grap_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '接单时间',
  141 + `pick_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '取件时间',
  142 + `complete_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '完成时间',
  143 + `trans_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '转单时间',
  144 + PRIMARY KEY (`id`),
  145 + UNIQUE KEY `uk_order_no` (`order_no`),
  146 + KEY `idx_rider_id` (`rider_id`),
  147 + KEY `idx_uid` (`uid`),
  148 + KEY `idx_city_status` (`city_id`, `status`),
  149 + KEY `idx_old_rider_trans` (`old_rider_id`, `is_trans`, `trans_time`)
  150 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
  151 +
  152 +-- 城市表(配送中台核心配置)
  153 +CREATE TABLE `city` (
  154 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '城市ID',
  155 + `pid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父级ID,0=省级',
  156 + `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '城市名称',
  157 + `area_code` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '行政区划码',
  158 + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=未开通 1=已开通',
  159 + `rate` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '平台抽成比例(%)',
  160 + `list_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
  161 + PRIMARY KEY (`id`),
  162 + KEY `idx_pid_order` (`pid`, `list_order`),
  163 + KEY `idx_area_code` (`area_code`)
  164 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='城市表';
  165 +
  166 +-- 配送计价方案主表
  167 +CREATE TABLE `delivery_fee_plan` (
  168 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  169 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID',
  170 + `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '方案名称',
  171 + `is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认方案',
  172 + `min_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '保底费用',
  173 + `distance_basic` DECIMAL(10,2) NOT NULL DEFAULT 3.00 COMMENT '预计送达基础距离(km)',
  174 + `distance_basic_time` INT NOT NULL DEFAULT 30 COMMENT '预计送达基础时间(分钟)',
  175 + `distance_more_time` INT NOT NULL DEFAULT 10 COMMENT '预计送达超出每km增加时间(分钟)',
  176 + `rider_distance` DECIMAL(10,2) NOT NULL DEFAULT 3.00 COMMENT '附近骑手展示范围(km)',
  177 + `rider_time` INT NOT NULL DEFAULT 0 COMMENT '预计接单时间(分钟)',
  178 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用',
  179 + `list_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
  180 + `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注',
  181 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  182 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  183 + PRIMARY KEY (`id`),
  184 + KEY `idx_city_default` (`city_id`, `is_default`),
  185 + KEY `idx_city_status` (`city_id`, `status`)
  186 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送计价方案主表';
  187 +
  188 +-- 配送计价维度主配置表
  189 +CREATE TABLE `delivery_fee_plan_dimension` (
  190 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  191 + `plan_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '方案ID',
  192 + `dimension_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '维度类型:base/distance/weight/piece/time',
  193 + `enabled` TINYINT NOT NULL DEFAULT 0 COMMENT '是否启用',
  194 + `base_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '基础费',
  195 + `start_distance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '起步里程(km)',
  196 + `start_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '起步费用',
  197 + `first_weight` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '首重(kg)',
  198 + `first_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '首重费用',
  199 + `unit_weight_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '续重单价',
  200 + `cap_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '封顶费用',
  201 + `extra_json` TEXT COMMENT '扩展配置',
  202 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  203 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  204 + PRIMARY KEY (`id`),
  205 + UNIQUE KEY `uk_plan_dimension` (`plan_id`, `dimension_type`)
  206 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送计价维度主配置表';
  207 +
  208 +-- 里程阶梯表
  209 +CREATE TABLE `delivery_fee_plan_distance_step` (
  210 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  211 + `plan_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '方案ID',
  212 + `end_distance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '结束里程(km)',
  213 + `unit_distance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '每档里程(km)',
  214 + `unit_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '每档加价',
  215 + `list_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
  216 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  217 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  218 + PRIMARY KEY (`id`),
  219 + KEY `idx_plan_order` (`plan_id`, `list_order`)
  220 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送计价里程阶梯表';
  221 +
  222 +-- 件数区间表
  223 +CREATE TABLE `delivery_fee_plan_piece_rule` (
  224 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  225 + `plan_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '方案ID',
  226 + `start_piece` INT NOT NULL DEFAULT 0 COMMENT '起始件数',
  227 + `end_piece` INT NOT NULL DEFAULT 0 COMMENT '结束件数',
  228 + `fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '费用',
  229 + `list_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
  230 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  231 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  232 + PRIMARY KEY (`id`),
  233 + KEY `idx_plan_order` (`plan_id`, `list_order`)
  234 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送计价件数区间表';
  235 +
  236 +-- 时段附加费表
  237 +CREATE TABLE `delivery_fee_plan_time_rule` (
  238 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  239 + `plan_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '方案ID',
  240 + `start_minute` INT NOT NULL DEFAULT 0 COMMENT '开始分钟',
  241 + `end_minute` INT NOT NULL DEFAULT 0 COMMENT '结束分钟',
  242 + `fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '附加费',
  243 + `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用',
  244 + `list_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
  245 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  246 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  247 + PRIMARY KEY (`id`),
  248 + KEY `idx_plan_order` (`plan_id`, `list_order`)
  249 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配送计价时段附加费表';
  250 +
  251 +-- 分站管理员表(每城市一个,管理本城市骑手和订单)
  252 +CREATE TABLE `substation` (
  253 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分站ID',
  254 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '管理城市ID',
  255 + `user_login` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '登录账号',
  256 + `user_nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称',
  257 + `user_pass` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '密码(MD5)',
  258 + `mobile` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号',
  259 + `avatar` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像',
  260 + `user_status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
  261 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
  262 + PRIMARY KEY (`id`),
  263 + UNIQUE KEY `uk_user_login` (`user_login`)
  264 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分站管理员表(一个城市/租户下可有多个管理员)';
  265 +
  266 +-- 商家入驻申请表
  267 +CREATE TABLE `merchant_enter` (
  268 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  269 + `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '联系人姓名',
  270 + `mobile` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号',
  271 + `store_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '店铺名称',
  272 + `type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1=商家入驻 2=骑手入驻 3=商务合作',
  273 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '城市ID',
  274 + `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注',
  275 + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=未处理 1=已通过 -1=已拒绝',
  276 + `add_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '申请时间',
  277 + PRIMARY KEY (`id`),
  278 + KEY `idx_status_type` (`status`, `type`),
  279 + KEY `idx_city_id` (`city_id`)
  280 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家入驻申请表';
  281 +
  282 +-- 商家账号表
  283 +CREATE TABLE `merchant_users` (
  284 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  285 + `store_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联店铺ID',
  286 + `mobile` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号(登录账号)',
  287 + `user_nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称',
  288 + `user_status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
  289 + `type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1=商家',
  290 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
  291 + PRIMARY KEY (`id`),
  292 + UNIQUE KEY `uk_mobile` (`mobile`),
  293 + KEY `idx_store_id` (`store_id`)
  294 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家账号表';
  295 +
  296 +-- 商家店铺表
  297 +CREATE TABLE `merchant_store` (
  298 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '店铺ID',
  299 + `name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '店铺名称',
  300 + `thumb` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '封面图',
  301 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属城市ID',
  302 + `address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '店铺地址',
  303 + `lng` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '经度',
  304 + `lat` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '纬度',
  305 + `operating_state` TINYINT NOT NULL DEFAULT 1 COMMENT '营业状态:0=打烊 1=营业',
  306 + `automatic_order` TINYINT NOT NULL DEFAULT 0 COMMENT '自动接单:0=否 1=是',
  307 + `shipping_type` TINYINT NOT NULL DEFAULT 1 COMMENT '配送类型:1=外卖配送 2=到店自提',
  308 + `free_shipping` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '免运费门槛,0=不免',
  309 + `up_to_send` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '起送金额,0=不限',
  310 + `open_date` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '营业日期JSON,如[1,2,3,4,5]',
  311 + `open_time` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '营业时间JSON,如["09:00","22:00"]',
  312 + `about` TEXT COMMENT '店铺简介',
  313 + `account_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联账号ID',
  314 + `app_key` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '接入方AppKey,为空=平台自建',
  315 + `out_store_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '接入方门店编号,用于推单时自动匹配',
  316 + `list_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
  317 + `is_del` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除',
  318 + `add_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
  319 + PRIMARY KEY (`id`),
  320 + KEY `idx_city_id` (`city_id`),
  321 + KEY `idx_app_out_store` (`app_key`, `out_store_id`),
  322 + KEY `idx_order_del` (`list_order`, `is_del`)
  323 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家店铺表';
  324 +
  325 +-- 开放平台应用表
  326 +CREATE TABLE `open_app` (
  327 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  328 + `app_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '应用名称',
  329 + `app_key` VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'AppKey',
  330 + `app_secret` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'AppSecret',
  331 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联城市/租户ID(必填,租户隔离核心字段)',
  332 + `store_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联店铺ID,0=不限制',
  333 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
  334 + `webhook_url` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Webhook回调地址',
  335 + `webhook_events` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '订阅事件JSON数组',
  336 + `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注',
  337 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  338 + PRIMARY KEY (`id`),
  339 + UNIQUE KEY `uk_app_key` (`app_key`),
  340 + KEY `idx_city_id` (`city_id`)
  341 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开放平台应用表';
  342 +
  343 +-- Webhook 推送日志表
  344 +CREATE TABLE `webhook_log` (
  345 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  346 + `app_id` BIGINT UNSIGNED NOT NULL COMMENT '应用ID',
  347 + `event` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '事件类型',
  348 + `biz_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '业务ID',
  349 + `url` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '推送URL',
  350 + `payload` TEXT COMMENT '推送内容JSON',
  351 + `response_code` INT NOT NULL DEFAULT 0 COMMENT 'HTTP响应码',
  352 + `response_body` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '响应内容',
  353 + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=失败 1=成功',
  354 + `retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
  355 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  356 + PRIMARY KEY (`id`),
  357 + KEY `idx_app_event` (`app_id`, `event`),
  358 + KEY `idx_biz_id` (`biz_id`),
  359 + KEY `idx_status` (`status`)
  360 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Webhook推送日志表';
  361 +
  362 +-- 超级管理员表
  363 +CREATE TABLE `admin_user` (
  364 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  365 + `user_login` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '登录账号',
  366 + `user_pass` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '密码(MD5)',
  367 + `user_nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称',
  368 + `user_status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
  369 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  370 + PRIMARY KEY (`id`),
  371 + UNIQUE KEY `uk_user_login` (`user_login`)
  372 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='超级管理员表';
  373 +
  374 +-- orders 表补充字段(如已有 orders 表,执行以下 ALTER)
  375 +ALTER TABLE `orders` ADD COLUMN `out_order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '外部系统订单号' AFTER `order_no`;
  376 +ALTER TABLE `orders` ADD COLUMN `app_key` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '接入方AppKey' AFTER `out_order_no`;
  377 +ALTER TABLE `orders` ADD COLUMN `callback_url` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '状态回调地址' AFTER `app_key`;
  378 +ALTER TABLE `orders` ADD INDEX `idx_app_out_order` (`app_key`, `out_order_no`);
  379 +
  380 +-- 骑手评价表
  381 +CREATE TABLE `rider_evaluate` (
  382 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  383 + `uid` BIGINT UNSIGNED NOT NULL COMMENT '评价用户ID',
  384 + `oid` BIGINT UNSIGNED NOT NULL COMMENT '订单ID',
  385 + `rid` BIGINT UNSIGNED NOT NULL COMMENT '骑手ID',
  386 + `content` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '评价内容',
  387 + `star` TINYINT NOT NULL DEFAULT 5 COMMENT '星级1-5',
  388 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  389 + `add_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  390 + PRIMARY KEY (`id`),
  391 + UNIQUE KEY `uk_uid_oid` (`uid`, `oid`),
  392 + KEY `idx_rid` (`rid`)
  393 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='骑手评价表';
  394 +
  395 +-- 退款原因配置表
  396 +CREATE TABLE `orders_refund_reason` (
  397 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  398 + `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '原因描述',
  399 + `role` TINYINT NOT NULL DEFAULT 1 COMMENT '1=用户 2=骑手',
  400 + `list_order` INT NOT NULL DEFAULT 0,
  401 + PRIMARY KEY (`id`)
  402 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款原因配置表';
  403 +
  404 +-- 退款申请记录表
  405 +CREATE TABLE `orders_refund_record` (
  406 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  407 + `oid` BIGINT UNSIGNED NOT NULL COMMENT '订单ID',
  408 + `order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '订单号',
  409 + `uid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '申请人ID',
  410 + `role` TINYINT NOT NULL DEFAULT 1 COMMENT '1=用户 2=骑手',
  411 + `reason_id` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  412 + `reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '退款原因',
  413 + `money` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '退款金额',
  414 + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=待处理 1=通过 2=拒绝',
  415 + `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '处理备注',
  416 + `add_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  417 + `handle_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  418 + PRIMARY KEY (`id`),
  419 + KEY `idx_oid` (`oid`),
  420 + KEY `idx_status` (`status`)
  421 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款申请记录表';
  422 +
  423 +-- 退款原因初始数据
  424 +INSERT INTO `orders_refund_reason` (`name`, `role`, `list_order`) VALUES
  425 +('骑手长时间未接单', 1, 1),
  426 +('骑手态度恶劣', 1, 2),
  427 +('物品损坏', 1, 3),
  428 +('其他原因', 1, 99),
  429 +('用户恶意单', 2, 1),
  430 +('无法完成配送', 2, 2),
  431 +('其他原因', 2, 99);
  432 +
  433 +-- 外部门店表(接入方通过开放平台同步自己系统的门店)
  434 +CREATE TABLE `ext_store` (
  435 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  436 + `app_key` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '所属应用AppKey',
  437 + `out_store_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '接入方门店原始ID',
  438 + `name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '门店名称',
  439 + `address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '门店地址',
  440 + `lng` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '经度',
  441 + `lat` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '纬度',
  442 + `city_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属城市ID',
  443 + `phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '联系电话',
  444 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=关闭 1=营业',
  445 + `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注',
  446 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  447 + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  448 + PRIMARY KEY (`id`),
  449 + UNIQUE KEY `uk_app_store` (`app_key`, `out_store_id`),
  450 + KEY `idx_city_id` (`city_id`)
  451 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='外部门店表';
  452 +
  453 +-- orders 表补充货物快照字段
  454 +ALTER TABLE `orders` ADD COLUMN `items_json` TEXT COMMENT '货物清单快照JSON' AFTER `callback_url`;
  455 +ALTER TABLE `orders` ADD COLUMN `item_remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '整单货物备注' AFTER `items_json`;
  456 +ALTER TABLE `orders` ADD COLUMN `ext_store_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联外部门店ID' AFTER `item_remark`;
0 457 \ No newline at end of file
... ...
src/test/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImplTest.java 0 → 100644
  1 +++ a/src/test/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImplTest.java
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
  4 +import com.diligrp.rider.dto.DeliveryPricingRuleDTO;
  5 +import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
  6 +import com.diligrp.rider.service.CityService;
  7 +import com.diligrp.rider.util.GeoUtil;
  8 +import com.diligrp.rider.vo.DeliveryFeeResultVO;
  9 +import org.junit.jupiter.api.Test;
  10 +
  11 +import java.math.BigDecimal;
  12 +import java.math.RoundingMode;
  13 +import java.time.LocalDateTime;
  14 +import java.time.ZoneId;
  15 +import java.util.Arrays;
  16 +import java.util.List;
  17 +
  18 +import static org.junit.jupiter.api.Assertions.assertEquals;
  19 +import static org.mockito.Mockito.mock;
  20 +
  21 +class DeliveryFeeServiceImplTest {
  22 +
  23 + private final DeliveryFeeServiceImpl service = new DeliveryFeeServiceImpl(mock(CityService.class));
  24 +
  25 + @Test
  26 + void shouldApplyMinFee() {
  27 + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig();
  28 + pricingConfig.getType6().setBaseSwitch(1);
  29 + pricingConfig.getType6().setBaseFee(new BigDecimal("2.00"));
  30 + pricingConfig.getType6().setDistanceSwitch(0);
  31 + pricingConfig.getType6().setWeightSwitch(0);
  32 + pricingConfig.getType6().setMinFee(new BigDecimal("5.00"));
  33 +
  34 + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, baseCalc());
  35 +
  36 + assertEquals(new BigDecimal("2.00"), result.getMoneyBasic());
  37 + assertEquals(new BigDecimal("5.00"), result.getTotalFee());
  38 + assertEquals(1, result.getMinFeeApplied());
  39 + }
  40 +
  41 + @Test
  42 + void shouldCalculateDistanceStepFee() {
  43 + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig();
  44 + pricingConfig.getType6().setDistanceBasic(new BigDecimal("1.0"));
  45 + pricingConfig.getType6().setDistanceBasicMoney(new BigDecimal("4.0"));
  46 + pricingConfig.getType6().setWeightSwitch(0);
  47 + pricingConfig.getType6().setDistanceSteps(List.of(
  48 + distanceStep("3.0", "1.0", "2.0", 0),
  49 + distanceStep("5.0", "1.0", "3.0", 1)
  50 + ));
  51 +
  52 + DeliveryFeeCalcDTO calc = baseCalc();
  53 + calc.setStartLat("31.2304");
  54 + calc.setStartLng("121.4737");
  55 + calc.setEndLat("31.2574");
  56 + calc.setEndLng("121.4737");
  57 +
  58 + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, calc);
  59 +
  60 + BigDecimal actualDistance = BigDecimal.valueOf(GeoUtil.calcDistanceKm(31.2304, 121.4737, 31.2574, 121.4737))
  61 + .setScale(0, RoundingMode.CEILING)
  62 + .setScale(1);
  63 + BigDecimal expectedDistanceFee = actualDistance.compareTo(new BigDecimal("3.0")) <= 0
  64 + ? new BigDecimal("4.00")
  65 + : new BigDecimal("7.00");
  66 + assertEquals(actualDistance, result.getDistance());
  67 + assertEquals(expectedDistanceFee, result.getMoneyDistance());
  68 + assertEquals(result.getMoneyBasic().add(result.getMoneyDistance()), result.getTotalFee());
  69 + }
  70 +
  71 + @Test
  72 + void shouldCapWeightFee() {
  73 + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig();
  74 + pricingConfig.getType6().setDistanceSwitch(0);
  75 + pricingConfig.getType6().setWeightFirst(new BigDecimal("5"));
  76 + pricingConfig.getType6().setWeightFirstFee(new BigDecimal("3"));
  77 + pricingConfig.getType6().setWeightUnitFee(new BigDecimal("2"));
  78 + pricingConfig.getType6().setWeightCapFee(new BigDecimal("10"));
  79 +
  80 + DeliveryFeeCalcDTO calc = baseCalc();
  81 + calc.setWeight(new BigDecimal("10"));
  82 +
  83 + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, calc);
  84 +
  85 + assertEquals(new BigDecimal("3"), result.getMoneyBasic());
  86 + assertEquals(new BigDecimal("7.00"), result.getMoneyWeight());
  87 + assertEquals(new BigDecimal("10.00"), result.getTotalFee());
  88 + }
  89 +
  90 + @Test
  91 + void shouldApplyPieceRule() {
  92 + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig();
  93 + pricingConfig.getType6().setDistanceSwitch(0);
  94 + pricingConfig.getType6().setWeightSwitch(0);
  95 + pricingConfig.getType6().setPieceSwitch(1);
  96 + pricingConfig.getType6().setPieceRules(List.of(
  97 + pieceRule(1, 2, "1.00", 0),
  98 + pieceRule(3, 5, "3.50", 1)
  99 + ));
  100 +
  101 + DeliveryFeeCalcDTO calc = baseCalc();
  102 + calc.setPieces(4);
  103 +
  104 + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, calc);
  105 +
  106 + assertEquals(new BigDecimal("3.50"), result.getMoneyPiece());
  107 + assertEquals("(3-5件)", result.getMoneyPieceTxt());
  108 + assertEquals(new BigDecimal("3.50"), result.getTotalFee());
  109 + }
  110 +
  111 + @Test
  112 + void shouldApplyCrossDayTimeFee() {
  113 + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig();
  114 + pricingConfig.getType6().setDistanceSwitch(0);
  115 + pricingConfig.getType6().setWeightSwitch(0);
  116 + pricingConfig.getType6().setTimes(List.of(timeRule(22 * 60, 6 * 60, "2.50", 1)));
  117 +
  118 + DeliveryFeeCalcDTO calc = baseCalc();
  119 + calc.setServiceTime(LocalDateTime.of(2026, 4, 2, 23, 30)
  120 + .atZone(ZoneId.of("Asia/Shanghai"))
  121 + .toEpochSecond());
  122 +
  123 + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, calc);
  124 +
  125 + assertEquals(new BigDecimal("2.50"), result.getMoneyTime());
  126 + assertEquals(new BigDecimal("2.50"), result.getTotalFee());
  127 + }
  128 +
  129 + private DeliveryPricingConfigDTO defaultPricingConfig() {
  130 + DeliveryPricingConfigDTO config = new DeliveryPricingConfigDTO();
  131 + config.setType(Arrays.asList(6));
  132 + config.setDistanceBasic(new BigDecimal("3"));
  133 + config.setDistanceBasicTime(30);
  134 + config.setDistanceMoreTime(10);
  135 + config.setRiderDistance(new BigDecimal("3"));
  136 +
  137 + DeliveryPricingRuleDTO type6 = new DeliveryPricingRuleDTO();
  138 + type6.setFeeMode(2);
  139 + type6.setBaseSwitch(0);
  140 + type6.setBaseFee(BigDecimal.ZERO);
  141 + type6.setDistanceSwitch(1);
  142 + type6.setDistanceBasic(new BigDecimal("3"));
  143 + type6.setDistanceBasicMoney(new BigDecimal("4"));
  144 + type6.setDistanceType(2);
  145 + type6.setWeightSwitch(1);
  146 + type6.setWeightFirst(new BigDecimal("5"));
  147 + type6.setWeightFirstFee(BigDecimal.ZERO);
  148 + type6.setWeightUnitFee(BigDecimal.ONE);
  149 + type6.setWeightCapFee(new BigDecimal("30"));
  150 + type6.setPieceSwitch(0);
  151 + config.setType6(type6);
  152 + return config;
  153 + }
  154 +
  155 + private DeliveryFeeCalcDTO baseCalc() {
  156 + DeliveryFeeCalcDTO calc = new DeliveryFeeCalcDTO();
  157 + calc.setCityId(1L);
  158 + calc.setOrderType(6);
  159 + calc.setStartLng("121.4737");
  160 + calc.setStartLat("31.2304");
  161 + calc.setEndLng("121.4737");
  162 + calc.setEndLat("31.2304");
  163 + calc.setWeight(BigDecimal.ZERO);
  164 + calc.setPieces(0);
  165 + calc.setServiceTime(0L);
  166 + return calc;
  167 + }
  168 +
  169 + private DeliveryPricingRuleDTO.DistanceStepDTO distanceStep(String endDistance, String unitDistance, String unitFee, int order) {
  170 + DeliveryPricingRuleDTO.DistanceStepDTO dto = new DeliveryPricingRuleDTO.DistanceStepDTO();
  171 + dto.setEndDistance(new BigDecimal(endDistance));
  172 + dto.setUnitDistance(new BigDecimal(unitDistance));
  173 + dto.setUnitFee(new BigDecimal(unitFee));
  174 + dto.setListOrder(order);
  175 + return dto;
  176 + }
  177 +
  178 + private DeliveryPricingRuleDTO.PieceRuleDTO pieceRule(int start, int end, String fee, int order) {
  179 + DeliveryPricingRuleDTO.PieceRuleDTO dto = new DeliveryPricingRuleDTO.PieceRuleDTO();
  180 + dto.setStartPiece(start);
  181 + dto.setEndPiece(end);
  182 + dto.setFee(new BigDecimal(fee));
  183 + dto.setListOrder(order);
  184 + return dto;
  185 + }
  186 +
  187 + private DeliveryPricingRuleDTO.TimePeriodDTO timeRule(int start, int end, String fee, int isOpen) {
  188 + DeliveryPricingRuleDTO.TimePeriodDTO dto = new DeliveryPricingRuleDTO.TimePeriodDTO();
  189 + dto.setStart(start);
  190 + dto.setEnd(end);
  191 + dto.setMoney(new BigDecimal(fee));
  192 + dto.setIsOpen(isOpen);
  193 + return dto;
  194 + }
  195 +}
... ...