Commit f5c2dda98ab577e575e87813031e8717e25f96e3

Authored by 杨刚
1 parent 5a1187de

新增菜单与角色管理功能,包括角色分配菜单、菜单树查询和相关接口

Showing 49 changed files with 1652 additions and 64 deletions
CLAUDE.md 0 → 100644
  1 +# CLAUDE.md
  2 +
  3 +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
  4 +
  5 +## Common commands
  6 +
  7 +- `mvn spring-boot:run` — run the service with the checked-in `src/main/resources/application.yml`
  8 +- `mvn test` — run all tests
  9 +- `mvn -Dtest=DeliveryFeeServiceImplTest test` — run the pricing-engine test class
  10 +- `mvn -Dtest=DeliveryFeeServiceImplTest#shouldApplyMinFee test` — run one test method
  11 +- `mvn package` — build the jar under `target/`
  12 +- `mvn -DskipTests package` — build without running tests
  13 +- `mysql -u root -p dili_rider < src/main/resources/schema.sql` — initialize schema
  14 +- `mysql -u root -p dili_rider < src/main/resources/data-init.sql` — load seed data
  15 +
  16 +Notes:
  17 +- There is no Maven wrapper in this repo; use the system `mvn`.
  18 +- No dedicated lint/format command is configured in `pom.xml`.
  19 +- Test coverage is currently sparse; the only checked-in unit tests are in `src/test/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImplTest.java`.
  20 +
  21 +## Architecture overview
  22 +
  23 +This is a Java 17 / Spring Boot 3.2 monolith for rider delivery operations. The main entrypoint is `src/main/java/com/diligrp/rider/RiderServiceApplication.java`, which enables MyBatis mapper scanning and `@EnableAsync`.
  24 +
  25 +### Main layers
  26 +
  27 +Code follows a conventional Spring layout under `src/main/java/com/diligrp/rider/`:
  28 +- `controller` — REST endpoints grouped by caller type
  29 +- `service` / `service/impl` — business logic
  30 +- `mapper` — MyBatis-Plus mappers
  31 +- `entity` — database entities
  32 +- `dto` / `vo` — request and response shapes
  33 +- `config` — interceptors, JWT utilities, MVC and WebSocket wiring
  34 +- `task` — scheduled background jobs
  35 +- `websocket` — live rider-location subscriptions and push
  36 +- `common` — shared `Result`, enums, and exception handling
  37 +
  38 +Most persistence uses MyBatis-Plus query/update wrappers. XML SQL exists, but only in a few files under `src/main/resources/mapper/`. Logical delete is globally configured through the `isDel` field in `src/main/resources/application.yml`.
  39 +
  40 +### API surfaces
  41 +
  42 +Controllers are split by audience rather than by technical module:
  43 +- `/api/rider/**` — rider app APIs
  44 +- `/api/admin/**` — admin and substation admin APIs
  45 +- `/api/platform/**` — super-admin platform APIs
  46 +- `/api/open/**` — signed open-platform APIs for third-party integrations
  47 +- `/api/delivery/fee/**` — internal fee-calculation APIs
  48 +
  49 +All controllers return the shared `Result<T>` envelope from `src/main/java/com/diligrp/rider/common/result/Result.java`. Cross-cutting exception mapping lives in `src/main/java/com/diligrp/rider/common/exception/GlobalExceptionHandler.java`.
  50 +
  51 +### Authentication and tenant/city scoping
  52 +
  53 +This service does not use Spring Security. Authentication is interceptor-driven:
  54 +- `src/main/java/com/diligrp/rider/config/AuthInterceptor.java` handles JWT auth for rider/admin/platform APIs.
  55 +- `src/main/java/com/diligrp/rider/config/OpenApiInterceptor.java` handles signed auth for `/api/open/**` using `X-App-Key`, `X-Timestamp`, `X-Nonce`, and `X-Sign`.
  56 +- `src/main/java/com/diligrp/rider/config/WebMvcConfig.java` wires both interceptors.
  57 +- JWT creation/parsing lives in `src/main/java/com/diligrp/rider/config/JwtUtil.java`.
  58 +
  59 +Important boundary rule: city/tenant identity is derived from trusted server-side state, not from caller input.
  60 +- Rider/admin JWTs inject `riderId`, `adminId`, `role`, and sometimes `cityId` into the request.
  61 +- Substation admins get `cityId` from the `substation` record, not from the request.
  62 +- Open-platform requests derive `cityId` from the bound `OpenApp`, not from payload fields.
  63 +
  64 +If you touch auth or routing, preserve that pattern.
  65 +
  66 +Also note: password checking in `RiderAuthServiceImpl` and `AdminAuthServiceImpl` uses MD5 hashing, so do not assume bcrypt/Spring Security conventions are already in place.
  67 +
  68 +### Core business flows
  69 +
  70 +#### Delivery pricing is DB-driven
  71 +
  72 +The pricing engine is centered on `src/main/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImpl.java`, but the actual pricing configuration comes from DB tables, not hardcoded constants.
  73 +
  74 +`src/main/java/com/diligrp/rider/service/impl/CityServiceImpl.java` assembles the active pricing plan from:
  75 +- `delivery_fee_plan`
  76 +- `delivery_fee_plan_dimension`
  77 +- `delivery_fee_plan_distance_step`
  78 +- `delivery_fee_plan_piece_rule`
  79 +- `delivery_fee_plan_time_rule`
  80 +
  81 +That assembled config is then used to compute:
  82 +- base fee
  83 +- distance fee / distance steps
  84 +- weight fee
  85 +- piece-count fee
  86 +- time-window surcharge
  87 +- minimum fee
  88 +- estimated delivery time
  89 +
  90 +If you change pricing behavior, check both `CityServiceImpl` and `DeliveryFeeServiceImpl`, and extend `DeliveryFeeServiceImplTest`.
  91 +
  92 +#### Open-platform order creation drives the main order lifecycle
  93 +
  94 +`src/main/java/com/diligrp/rider/service/impl/DeliveryOrderServiceImpl.java` is the main open-platform order entry path. It:
  95 +- resolves the `OpenApp` from `appKey`
  96 +- forces `cityId` from the app binding
  97 +- optionally hydrates store info from merchant data
  98 +- computes the delivery fee
  99 +- creates the `orders` record
  100 +- emits webhook notifications for order events
  101 +
  102 +That service is a good starting point when tracing order ingestion and external callbacks.
  103 +
  104 +#### Dispatch is a scoring engine over DB state
  105 +
  106 +`src/main/java/com/diligrp/rider/service/impl/DispatchServiceImpl.java` performs rider selection. It scores candidates using current city, online/rest state, rider location, order load, refusal history, daily counts, and configured dispatch conditions.
  107 +
  108 +The dispatch engine depends on:
  109 +- the active dispatch rule template for the city
  110 +- current `rider_location` rows
  111 +- open `orders`
  112 +- rider/day statistics
  113 +
  114 +Scheduled jobs then advance the order lifecycle:
  115 +- `src/main/java/com/diligrp/rider/task/DispatchScheduleTask.java` runs every 3 seconds to auto-dispatch timed-out grab orders
  116 +- `src/main/java/com/diligrp/rider/task/OrderScheduleTask.java` runs every 60 seconds to auto-cancel stale unaccepted orders
  117 +
  118 +This app is DB-state-driven; there is no message queue coordinating dispatch.
  119 +
  120 +### Real-time location flow
  121 +
  122 +Live rider location is implemented with raw Spring WebSocket, not STOMP/SockJS.
  123 +- WebSocket endpoint: `/ws/location`
  124 +- Config: `src/main/java/com/diligrp/rider/config/LocationWebSocketConfig.java`
  125 +- Handshake auth: `src/main/java/com/diligrp/rider/websocket/LocationWebSocketHandshakeInterceptor.java`
  126 +- Update/push path: `src/main/java/com/diligrp/rider/service/impl/RiderLocationServiceImpl.java`
  127 +
  128 +Rider location updates are written to `rider_location` and then pushed to subscribed admin clients. Super admins must provide `cityId` when connecting; substation admins derive it from their account.
  129 +
  130 +### External integrations
  131 +
  132 +External notifications are sent asynchronously by `src/main/java/com/diligrp/rider/service/impl/WebhookServiceImpl.java` using JDK `HttpClient` plus `@Async`. There is no Kafka/RabbitMQ-style event bus in this repo.
  133 +
  134 +Redis is present, but its main visible use is SMS verification code storage in `src/main/java/com/diligrp/rider/service/impl/RiderAuthServiceImpl.java`; do not assume Redis-backed sessions or broad caching layers exist.
  135 +
  136 +### Database and runtime config
  137 +
  138 +- Main runtime config is `src/main/resources/application.yml`
  139 +- Schema lives in `src/main/resources/schema.sql`
  140 +- Seed data lives in `src/main/resources/data-init.sql`
  141 +
  142 +There is only one checked-in Spring config file; do not assume profile-specific config files already exist.
src/main/java/com/diligrp/rider/common/auth/AdminScopeGuard.java 0 → 100644
  1 +package com.diligrp.rider.common.auth;
  2 +
  3 +import com.diligrp.rider.common.exception.BizException;
  4 +import org.springframework.stereotype.Component;
  5 +
  6 +@Component
  7 +public class AdminScopeGuard {
  8 +
  9 + public void assertCityAccessible(Long currentCityId, Long targetCityId) {
  10 + if (currentCityId == null || currentCityId < 1) {
  11 + return;
  12 + }
  13 + if (targetCityId == null || !currentCityId.equals(targetCityId)) {
  14 + throw new BizException("只能操作当前城市数据");
  15 + }
  16 + }
  17 +}
src/main/java/com/diligrp/rider/common/enums/AdminRoleScopeEnum.java 0 → 100644
  1 +package com.diligrp.rider.common.enums;
  2 +
  3 +public enum AdminRoleScopeEnum {
  4 + PLATFORM,
  5 + SUBSTATION
  6 +}
src/main/java/com/diligrp/rider/common/enums/MenuScopeEnum.java 0 → 100644
  1 +package com.diligrp.rider.common.enums;
  2 +
  3 +public enum MenuScopeEnum {
  4 + PLATFORM,
  5 + SUBSTATION,
  6 + BOTH
  7 +}
src/main/java/com/diligrp/rider/controller/AdminRefundController.java
@@ -5,6 +5,7 @@ import com.diligrp.rider.entity.OrderRefundReason; @@ -5,6 +5,7 @@ import com.diligrp.rider.entity.OrderRefundReason;
5 import com.diligrp.rider.entity.OrderRefundRecord; 5 import com.diligrp.rider.entity.OrderRefundRecord;
6 import com.diligrp.rider.service.RefundService; 6 import com.diligrp.rider.service.RefundService;
7 import com.diligrp.rider.service.RiderEvaluateService; 7 import com.diligrp.rider.service.RiderEvaluateService;
  8 +import jakarta.servlet.http.HttpServletRequest;
8 import lombok.RequiredArgsConstructor; 9 import lombok.RequiredArgsConstructor;
9 import org.springframework.web.bind.annotation.*; 10 import org.springframework.web.bind.annotation.*;
10 11
@@ -29,8 +30,8 @@ public class AdminRefundController { @@ -29,8 +30,8 @@ public class AdminRefundController {
29 30
30 /** 查看订单退款记录 */ 31 /** 查看订单退款记录 */
31 @GetMapping("/refund/record") 32 @GetMapping("/refund/record")
32 - public Result<OrderRefundRecord> record(@RequestParam Long orderId) {  
33 - return Result.success(refundService.getByOrderId(orderId)); 33 + public Result<OrderRefundRecord> record(@RequestParam Long orderId, HttpServletRequest request) {
  34 + return Result.success(refundService.getByOrderId(orderId, resolveScopedCityId(request)));
34 } 35 }
35 36
36 /** 37 /**
@@ -42,8 +43,9 @@ public class AdminRefundController { @@ -42,8 +43,9 @@ public class AdminRefundController {
42 public Result<Void> handle( 43 public Result<Void> handle(
43 @RequestParam Long recordId, 44 @RequestParam Long recordId,
44 @RequestParam int status, 45 @RequestParam int status,
45 - @RequestParam(required = false, defaultValue = "") String remark) {  
46 - refundService.handleRefund(recordId, status, remark); 46 + @RequestParam(required = false, defaultValue = "") String remark,
  47 + HttpServletRequest request) {
  48 + refundService.handleRefund(recordId, status, remark, resolveScopedCityId(request));
47 return Result.success(); 49 return Result.success();
48 } 50 }
49 51
@@ -52,7 +54,14 @@ public class AdminRefundController { @@ -52,7 +54,14 @@ public class AdminRefundController {
52 public Result<List<?>> evaluateList( 54 public Result<List<?>> evaluateList(
53 @RequestParam Long riderId, 55 @RequestParam Long riderId,
54 @RequestParam(defaultValue = "0") int type, 56 @RequestParam(defaultValue = "0") int type,
55 - @RequestParam(defaultValue = "1") int page) {  
56 - return Result.success(evaluateService.getRiderEvaluates(riderId, type, page)); 57 + @RequestParam(defaultValue = "1") int page,
  58 + HttpServletRequest request) {
  59 + return Result.success(evaluateService.getRiderEvaluates(riderId, type, page, resolveScopedCityId(request)));
  60 + }
  61 +
  62 + private Long resolveScopedCityId(HttpServletRequest request) {
  63 + return "substation".equals(request.getAttribute("role"))
  64 + ? (Long) request.getAttribute("cityId")
  65 + : null;
57 } 66 }
58 } 67 }
src/main/java/com/diligrp/rider/controller/AdminRiderController.java
@@ -55,8 +55,10 @@ public class AdminRiderController { @@ -55,8 +55,10 @@ public class AdminRiderController {
55 55
56 /** 审核骑手:status=0拒绝 1通过 */ 56 /** 审核骑手:status=0拒绝 1通过 */
57 @PostMapping("/setStatus") 57 @PostMapping("/setStatus")
58 - public Result<Void> setStatus(@RequestParam Long riderId, @RequestParam int status) {  
59 - adminRiderService.setStatus(riderId, status); 58 + public Result<Void> setStatus(@RequestParam Long riderId,
  59 + @RequestParam int status,
  60 + HttpServletRequest request) {
  61 + adminRiderService.setStatus(riderId, status, resolveScopedCityId(request));
60 return Result.success(); 62 return Result.success();
61 } 63 }
62 64
@@ -74,29 +76,37 @@ public class AdminRiderController { @@ -74,29 +76,37 @@ public class AdminRiderController {
74 76
75 /** 启用/禁用骑手账号:status=0禁用 1启用 */ 77 /** 启用/禁用骑手账号:status=0禁用 1启用 */
76 @PostMapping("/setEnableStatus") 78 @PostMapping("/setEnableStatus")
77 - public Result<Void> setEnableStatus(@RequestParam Long riderId, @RequestParam int status) {  
78 - adminRiderService.setEnableStatus(riderId, status); 79 + public Result<Void> setEnableStatus(@RequestParam Long riderId,
  80 + @RequestParam int status,
  81 + HttpServletRequest request) {
  82 + adminRiderService.setEnableStatus(riderId, status, resolveScopedCityId(request));
79 return Result.success(); 83 return Result.success();
80 } 84 }
81 85
82 /** 切换骑手类型:type=1兼职 2全职 */ 86 /** 切换骑手类型:type=1兼职 2全职 */
83 @PostMapping("/setType") 87 @PostMapping("/setType")
84 - public Result<Void> setType(@RequestParam Long riderId, @RequestParam int type) {  
85 - adminRiderService.setType(riderId, type); 88 + public Result<Void> setType(@RequestParam Long riderId,
  89 + @RequestParam int type,
  90 + HttpServletRequest request) {
  91 + adminRiderService.setType(riderId, type, resolveScopedCityId(request));
86 return Result.success(); 92 return Result.success();
87 } 93 }
88 94
89 /** 指派骑手接单 */ 95 /** 指派骑手接单 */
90 @PostMapping("/order/designate") 96 @PostMapping("/order/designate")
91 - public Result<Void> designate(@RequestParam Long orderId, @RequestParam Long riderId) {  
92 - adminRiderService.designate(orderId, riderId); 97 + public Result<Void> designate(@RequestParam Long orderId,
  98 + @RequestParam Long riderId,
  99 + HttpServletRequest request) {
  100 + adminRiderService.designate(orderId, riderId, resolveScopedCityId(request));
93 return Result.success(); 101 return Result.success();
94 } 102 }
95 103
96 /** 处理转单申请:trans=1通过 3拒绝 */ 104 /** 处理转单申请:trans=1通过 3拒绝 */
97 @PostMapping("/order/setTrans") 105 @PostMapping("/order/setTrans")
98 - public Result<Void> setTrans(@RequestParam Long orderId, @RequestParam int trans) {  
99 - adminRiderService.setTrans(orderId, trans); 106 + public Result<Void> setTrans(@RequestParam Long orderId,
  107 + @RequestParam int trans,
  108 + HttpServletRequest request) {
  109 + adminRiderService.setTrans(orderId, trans, resolveScopedCityId(request));
100 return Result.success(); 110 return Result.success();
101 } 111 }
102 112
@@ -145,10 +155,15 @@ public class AdminRiderController { @@ -145,10 +155,15 @@ public class AdminRiderController {
145 @RequestParam(required = false) String appKey, 155 @RequestParam(required = false) String appKey,
146 @RequestParam(required = false) String outOrderNo, 156 @RequestParam(required = false) String outOrderNo,
147 @RequestParam(required = false) Integer status, 157 @RequestParam(required = false) Integer status,
148 - @RequestParam(defaultValue = "1") int page) { 158 + @RequestParam(defaultValue = "1") int page,
  159 + HttpServletRequest request) {
149 LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<Orders>() 160 LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<Orders>()
150 .eq(Orders::getIsDel, 0) 161 .eq(Orders::getIsDel, 0)
151 .orderByDesc(Orders::getId); 162 .orderByDesc(Orders::getId);
  163 + Long cityId = resolveScopedCityId(request);
  164 + if (cityId != null) {
  165 + wrapper.eq(Orders::getCityId, cityId);
  166 + }
152 if (appKey != null && !appKey.isBlank()) wrapper.eq(Orders::getAppKey, appKey); 167 if (appKey != null && !appKey.isBlank()) wrapper.eq(Orders::getAppKey, appKey);
153 if (outOrderNo != null && !outOrderNo.isBlank()) wrapper.like(Orders::getOutOrderNo, outOrderNo); 168 if (outOrderNo != null && !outOrderNo.isBlank()) wrapper.like(Orders::getOutOrderNo, outOrderNo);
154 if (status != null) wrapper.eq(Orders::getStatus, status); 169 if (status != null) wrapper.eq(Orders::getStatus, status);
@@ -156,4 +171,10 @@ public class AdminRiderController { @@ -156,4 +171,10 @@ public class AdminRiderController {
156 wrapper.last("LIMIT " + offset + ",20"); 171 wrapper.last("LIMIT " + offset + ",20");
157 return Result.success(ordersMapper.selectList(wrapper)); 172 return Result.success(ordersMapper.selectList(wrapper));
158 } 173 }
  174 +
  175 + private Long resolveScopedCityId(HttpServletRequest request) {
  176 + return "substation".equals(request.getAttribute("role"))
  177 + ? (Long) request.getAttribute("cityId")
  178 + : null;
  179 + }
159 } 180 }
src/main/java/com/diligrp/rider/controller/PlatformAdminUserController.java 0 → 100644
  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.AdminUser;
  6 +import com.diligrp.rider.service.AdminUserManageService;
  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/admin-user")
  15 +@RequiredArgsConstructor
  16 +public class PlatformAdminUserController {
  17 +
  18 + private final AdminUserManageService adminUserManageService;
  19 +
  20 + @GetMapping("/list")
  21 + public Result<List<AdminUser>> list(@RequestParam(required = false) String keyword) {
  22 + return Result.success(adminUserManageService.list(keyword));
  23 + }
  24 +
  25 + @PostMapping("/add")
  26 + public Result<Void> add(@RequestBody AdminUser adminUser) {
  27 + adminUserManageService.add(adminUser);
  28 + return Result.success();
  29 + }
  30 +
  31 + @PutMapping("/edit")
  32 + public Result<Void> edit(@RequestBody AdminUser adminUser) {
  33 + adminUserManageService.edit(adminUser);
  34 + return Result.success();
  35 + }
  36 +
  37 + @PostMapping("/ban")
  38 + public Result<Void> ban(@RequestParam Long id) {
  39 + adminUserManageService.ban(id);
  40 + return Result.success();
  41 + }
  42 +
  43 + @PostMapping("/cancelBan")
  44 + public Result<Void> cancelBan(@RequestParam Long id) {
  45 + adminUserManageService.cancelBan(id);
  46 + return Result.success();
  47 + }
  48 +
  49 + @DeleteMapping("/del")
  50 + public Result<Void> del(@RequestParam Long id) {
  51 + adminUserManageService.del(id);
  52 + return Result.success();
  53 + }
  54 +
  55 + @PostMapping("/changePassword")
  56 + public Result<Void> changePassword(@RequestParam Long id,
  57 + @Valid @RequestBody ChangePasswordDTO dto) {
  58 + adminUserManageService.changePassword(id, dto.getOldPassword(), dto.getNewPassword());
  59 + return Result.success();
  60 + }
  61 +}
src/main/java/com/diligrp/rider/controller/PlatformSubstationController.java
@@ -70,10 +70,9 @@ public class PlatformSubstationController { @@ -70,10 +70,9 @@ public class PlatformSubstationController {
70 */ 70 */
71 @PostMapping("/changePassword") 71 @PostMapping("/changePassword")
72 public Result<Void> 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()); 73 + @RequestParam Long id,
  74 + @Valid @RequestBody ChangePasswordDTO dto) {
  75 + substationService.changePassword(id, dto.getOldPassword(), dto.getNewPassword());
77 return Result.success(); 76 return Result.success();
78 } 77 }
79 } 78 }
src/main/java/com/diligrp/rider/controller/PlatformSystemMenuController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.AdminMenuSaveDTO;
  5 +import com.diligrp.rider.service.SystemMenuService;
  6 +import com.diligrp.rider.vo.AdminRoleMenuTreeVO;
  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/system/menu")
  15 +@RequiredArgsConstructor
  16 +public class PlatformSystemMenuController {
  17 +
  18 + private final SystemMenuService systemMenuService;
  19 +
  20 + @GetMapping("/tree")
  21 + public Result<List<AdminRoleMenuTreeVO>> tree() {
  22 + return Result.success(systemMenuService.tree());
  23 + }
  24 +
  25 + @PostMapping("/add")
  26 + public Result<Void> add(@Valid @RequestBody AdminMenuSaveDTO dto) {
  27 + systemMenuService.add(dto);
  28 + return Result.success();
  29 + }
  30 +
  31 + @PutMapping("/edit")
  32 + public Result<Void> edit(@Valid @RequestBody AdminMenuSaveDTO dto) {
  33 + systemMenuService.edit(dto);
  34 + return Result.success();
  35 + }
  36 +
  37 + @DeleteMapping("/del")
  38 + public Result<Void> delete(@RequestParam Long id) {
  39 + systemMenuService.delete(id);
  40 + return Result.success();
  41 + }
  42 +
  43 + @PostMapping("/setVisible")
  44 + public Result<Void> setVisible(@RequestParam Long id, @RequestParam int visible) {
  45 + systemMenuService.setVisible(id, visible);
  46 + return Result.success();
  47 + }
  48 +
  49 + @PostMapping("/setStatus")
  50 + public Result<Void> setStatus(@RequestParam Long id, @RequestParam int status) {
  51 + systemMenuService.setStatus(id, status);
  52 + return Result.success();
  53 + }
  54 +}
src/main/java/com/diligrp/rider/controller/PlatformSystemRoleController.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.result.Result;
  4 +import com.diligrp.rider.dto.AdminRoleMenuAssignDTO;
  5 +import com.diligrp.rider.service.SystemRoleMenuService;
  6 +import com.diligrp.rider.vo.AdminRoleMenuTreeVO;
  7 +import com.diligrp.rider.vo.AdminRoleVO;
  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/platform/system/role")
  16 +@RequiredArgsConstructor
  17 +public class PlatformSystemRoleController {
  18 +
  19 + private final SystemRoleMenuService systemRoleMenuService;
  20 +
  21 + @GetMapping("/list")
  22 + public Result<List<AdminRoleVO>> list() {
  23 + return Result.success(systemRoleMenuService.listRoles());
  24 + }
  25 +
  26 + @GetMapping("/{roleId}/menu-tree")
  27 + public Result<List<AdminRoleMenuTreeVO>> menuTree(@PathVariable Long roleId) {
  28 + return Result.success(systemRoleMenuService.getRoleMenuTree(roleId));
  29 + }
  30 +
  31 + @PostMapping("/{roleId}/menus")
  32 + public Result<Void> assignMenus(@PathVariable Long roleId,
  33 + @Valid @RequestBody AdminRoleMenuAssignDTO dto) {
  34 + systemRoleMenuService.assignMenus(roleId, dto);
  35 + return Result.success();
  36 + }
  37 +}
src/main/java/com/diligrp/rider/controller/RiderExtController.java
@@ -104,7 +104,7 @@ public class RiderExtController { @@ -104,7 +104,7 @@ public class RiderExtController {
104 @RequestParam(defaultValue = "1") int page, 104 @RequestParam(defaultValue = "1") int page,
105 HttpServletRequest request) { 105 HttpServletRequest request) {
106 Long riderId = (Long) request.getAttribute("riderId"); 106 Long riderId = (Long) request.getAttribute("riderId");
107 - return Result.success(evaluateService.getRiderEvaluates(riderId, type, page)); 107 + return Result.success(evaluateService.getRiderEvaluates(riderId, type, page, null));
108 } 108 }
109 109
110 /** 本月好评数 */ 110 /** 本月好评数 */
src/main/java/com/diligrp/rider/dto/AdminMenuSaveDTO.java 0 → 100644
  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 AdminMenuSaveDTO {
  9 + private Long id;
  10 +
  11 + @NotBlank(message = "菜单编码不能为空")
  12 + private String code;
  13 +
  14 + @NotBlank(message = "菜单名称不能为空")
  15 + private String name;
  16 +
  17 + @NotBlank(message = "菜单类型不能为空")
  18 + private String type;
  19 +
  20 + private String path;
  21 +
  22 + private String icon;
  23 +
  24 + @NotNull(message = "父级菜单不能为空")
  25 + private Long parentId;
  26 +
  27 + @NotBlank(message = "菜单范围不能为空")
  28 + private String menuScope;
  29 +
  30 + private Integer listOrder = 0;
  31 +
  32 + private Integer visible = 1;
  33 +
  34 + private Integer status = 1;
  35 +}
src/main/java/com/diligrp/rider/dto/AdminRoleMenuAssignDTO.java 0 → 100644
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.ArrayList;
  6 +import java.util.List;
  7 +
  8 +@Data
  9 +public class AdminRoleMenuAssignDTO {
  10 + private List<Long> menuIds = new ArrayList<>();
  11 +}
src/main/java/com/diligrp/rider/entity/AdminUser.java
@@ -25,5 +25,8 @@ public class AdminUser { @@ -25,5 +25,8 @@ public class AdminUser {
25 /** 状态:0=禁用 1=正常 */ 25 /** 状态:0=禁用 1=正常 */
26 private Integer userStatus; 26 private Integer userStatus;
27 27
  28 + /** 绑定菜单角色ID */
  29 + private Long roleId;
  30 +
28 private Long createTime; 31 private Long createTime;
29 } 32 }
src/main/java/com/diligrp/rider/entity/Substation.java
@@ -35,5 +35,8 @@ public class Substation { @@ -35,5 +35,8 @@ public class Substation {
35 /** 状态:0=禁用 1=正常 */ 35 /** 状态:0=禁用 1=正常 */
36 private Integer userStatus; 36 private Integer userStatus;
37 37
  38 + /** 绑定菜单角色ID */
  39 + private Long roleId;
  40 +
38 private Long createTime; 41 private Long createTime;
39 } 42 }
src/main/java/com/diligrp/rider/entity/SysMenu.java 0 → 100644
  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 +@Data
  9 +@TableName("sys_menu")
  10 +public class SysMenu {
  11 +
  12 + @TableId(type = IdType.AUTO)
  13 + private Long id;
  14 +
  15 + private String code;
  16 +
  17 + private String name;
  18 +
  19 + private String type;
  20 +
  21 + private String path;
  22 +
  23 + private String icon;
  24 +
  25 + private Long parentId;
  26 +
  27 + private String menuScope;
  28 +
  29 + private Integer listOrder;
  30 +
  31 + private Integer visible;
  32 +
  33 + private Integer status;
  34 +
  35 + private Long createTime;
  36 +}
src/main/java/com/diligrp/rider/entity/SysRole.java 0 → 100644
  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 +@Data
  9 +@TableName("sys_role")
  10 +public class SysRole {
  11 +
  12 + @TableId(type = IdType.AUTO)
  13 + private Long id;
  14 +
  15 + private String code;
  16 +
  17 + private String name;
  18 +
  19 + private String roleScope;
  20 +
  21 + private Integer status;
  22 +
  23 + private Long createTime;
  24 +}
src/main/java/com/diligrp/rider/entity/SysRoleMenu.java 0 → 100644
  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 +@Data
  9 +@TableName("sys_role_menu")
  10 +public class SysRoleMenu {
  11 +
  12 + @TableId(type = IdType.AUTO)
  13 + private Long id;
  14 +
  15 + private Long roleId;
  16 +
  17 + private Long menuId;
  18 +
  19 + private Long createTime;
  20 +}
src/main/java/com/diligrp/rider/mapper/SysMenuMapper.java 0 → 100644
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.SysMenu;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface SysMenuMapper extends BaseMapper<SysMenu> {
  9 +}
src/main/java/com/diligrp/rider/mapper/SysRoleMapper.java 0 → 100644
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.SysRole;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface SysRoleMapper extends BaseMapper<SysRole> {
  9 +}
src/main/java/com/diligrp/rider/mapper/SysRoleMenuMapper.java 0 → 100644
  1 +package com.diligrp.rider.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.diligrp.rider.entity.SysRoleMenu;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface SysRoleMenuMapper extends BaseMapper<SysRoleMenu> {
  9 +}
src/main/java/com/diligrp/rider/service/AdminMenuService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.SysRole;
  4 +import com.diligrp.rider.vo.AdminMenuVO;
  5 +
  6 +import java.util.List;
  7 +
  8 +public interface AdminMenuService {
  9 + SysRole resolveRole(Long roleId, String roleType);
  10 +
  11 + List<AdminMenuVO> getMenus(SysRole role, String roleType);
  12 +
  13 + String resolveHomePath(List<AdminMenuVO> menus);
  14 +}
src/main/java/com/diligrp/rider/service/AdminRiderService.java
@@ -14,15 +14,15 @@ public interface AdminRiderService { @@ -14,15 +14,15 @@ public interface AdminRiderService {
14 /** 指派候选骑手列表 */ 14 /** 指派候选骑手列表 */
15 List<Rider> designateCandidates(Long orderId, Long cityId); 15 List<Rider> designateCandidates(Long orderId, Long cityId);
16 /** 审核骑手(通过/拒绝) */ 16 /** 审核骑手(通过/拒绝) */
17 - void setStatus(Long riderId, int status); 17 + void setStatus(Long riderId, int status, Long cityId);
18 /** 设置骑手等级,为空则使用默认等级 */ 18 /** 设置骑手等级,为空则使用默认等级 */
19 void setLevel(Long riderId, Long levelId, Long cityId); 19 void setLevel(Long riderId, Long levelId, Long cityId);
20 /** 启用/禁用骑手账号 */ 20 /** 启用/禁用骑手账号 */
21 - void setEnableStatus(Long riderId, int status); 21 + void setEnableStatus(Long riderId, int status, Long cityId);
22 /** 切换全职/兼职 */ 22 /** 切换全职/兼职 */
23 - void setType(Long riderId, int type); 23 + void setType(Long riderId, int type, Long cityId);
24 /** 指派骑手接单 */ 24 /** 指派骑手接单 */
25 - void designate(Long orderId, Long riderId); 25 + void designate(Long orderId, Long riderId, Long cityId);
26 /** 处理转单申请(1=通过 3=拒绝) */ 26 /** 处理转单申请(1=通过 3=拒绝) */
27 - void setTrans(Long orderId, int trans); 27 + void setTrans(Long orderId, int trans, Long cityId);
28 } 28 }
src/main/java/com/diligrp/rider/service/AdminUserManageService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.AdminUser;
  4 +
  5 +import java.util.List;
  6 +
  7 +public interface AdminUserManageService {
  8 + List<AdminUser> list(String keyword);
  9 +
  10 + void add(AdminUser adminUser);
  11 +
  12 + void edit(AdminUser adminUser);
  13 +
  14 + void ban(Long id);
  15 +
  16 + void cancelBan(Long id);
  17 +
  18 + void del(Long id);
  19 +
  20 + void changePassword(Long id, String oldPassword, String newPassword);
  21 +}
src/main/java/com/diligrp/rider/service/MenuBootstrapService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +public interface MenuBootstrapService {
  4 + void initializeDefaults();
  5 +}
src/main/java/com/diligrp/rider/service/RefundService.java
@@ -11,7 +11,7 @@ public interface RefundService { @@ -11,7 +11,7 @@ public interface RefundService {
11 /** 用户/骑手申请退款 */ 11 /** 用户/骑手申请退款 */
12 void applyRefund(Long orderId, Long uid, int role, Long reasonId, String reason); 12 void applyRefund(Long orderId, Long uid, int role, Long reasonId, String reason);
13 /** 分站审核退款:status=1通过 2拒绝 */ 13 /** 分站审核退款:status=1通过 2拒绝 */
14 - void handleRefund(Long recordId, int status, String remark); 14 + void handleRefund(Long recordId, int status, String remark, Long cityId);
15 /** 查询订单退款记录 */ 15 /** 查询订单退款记录 */
16 - OrderRefundRecord getByOrderId(Long orderId); 16 + OrderRefundRecord getByOrderId(Long orderId, Long cityId);
17 } 17 }
src/main/java/com/diligrp/rider/service/RiderEvaluateService.java
@@ -8,7 +8,7 @@ public interface RiderEvaluateService { @@ -8,7 +8,7 @@ public interface RiderEvaluateService {
8 /** 用户对骑手评价(订单完成后) */ 8 /** 用户对骑手评价(订单完成后) */
9 void evaluate(Long uid, Long orderId, int star, String content); 9 void evaluate(Long uid, Long orderId, int star, String content);
10 /** 骑手评价列表 */ 10 /** 骑手评价列表 */
11 - List<RiderEvaluate> getRiderEvaluates(Long riderId, Integer type, int page); 11 + List<RiderEvaluate> getRiderEvaluates(Long riderId, Integer type, int page, Long cityId);
12 /** 本月好评数 */ 12 /** 本月好评数 */
13 int getMonthGoodCount(Long riderId); 13 int getMonthGoodCount(Long riderId);
14 } 14 }
src/main/java/com/diligrp/rider/service/RoleScopeGuardService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.entity.SysRole;
  4 +
  5 +public interface RoleScopeGuardService {
  6 + SysRole requireRole(Long roleId, String requiredScope);
  7 +}
src/main/java/com/diligrp/rider/service/SubstationService.java
@@ -19,6 +19,6 @@ public interface SubstationService { @@ -19,6 +19,6 @@ public interface SubstationService {
19 void del(Long id); 19 void del(Long id);
20 /** 根据城市ID获取分站管理员 */ 20 /** 根据城市ID获取分站管理员 */
21 Substation getByCityId(Long cityId); 21 Substation getByCityId(Long cityId);
22 - /** 分站管理员修改自己的密码 */ 22 + /** 重置目标分站管理员密码 */
23 void changePassword(Long substationId, String oldPassword, String newPassword); 23 void changePassword(Long substationId, String oldPassword, String newPassword);
24 } 24 }
src/main/java/com/diligrp/rider/service/SystemMenuService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.AdminMenuSaveDTO;
  4 +import com.diligrp.rider.vo.AdminRoleMenuTreeVO;
  5 +
  6 +import java.util.List;
  7 +
  8 +public interface SystemMenuService {
  9 + List<AdminRoleMenuTreeVO> tree();
  10 +
  11 + void add(AdminMenuSaveDTO dto);
  12 +
  13 + void edit(AdminMenuSaveDTO dto);
  14 +
  15 + void delete(Long id);
  16 +
  17 + void setVisible(Long id, int visible);
  18 +
  19 + void setStatus(Long id, int status);
  20 +}
src/main/java/com/diligrp/rider/service/SystemRoleMenuService.java 0 → 100644
  1 +package com.diligrp.rider.service;
  2 +
  3 +import com.diligrp.rider.dto.AdminRoleMenuAssignDTO;
  4 +import com.diligrp.rider.vo.AdminRoleMenuTreeVO;
  5 +import com.diligrp.rider.vo.AdminRoleVO;
  6 +
  7 +import java.util.List;
  8 +
  9 +public interface SystemRoleMenuService {
  10 + List<AdminRoleVO> listRoles();
  11 +
  12 + List<AdminRoleMenuTreeVO> getRoleMenuTree(Long roleId);
  13 +
  14 + void assignMenus(Long roleId, AdminRoleMenuAssignDTO dto);
  15 +}
src/main/java/com/diligrp/rider/service/impl/AdminAuthServiceImpl.java
@@ -5,22 +5,34 @@ import com.diligrp.rider.common.exception.BizException; @@ -5,22 +5,34 @@ import com.diligrp.rider.common.exception.BizException;
5 import com.diligrp.rider.config.JwtUtil; 5 import com.diligrp.rider.config.JwtUtil;
6 import com.diligrp.rider.dto.AdminLoginDTO; 6 import com.diligrp.rider.dto.AdminLoginDTO;
7 import com.diligrp.rider.entity.AdminUser; 7 import com.diligrp.rider.entity.AdminUser;
  8 +import com.diligrp.rider.entity.City;
8 import com.diligrp.rider.entity.Substation; 9 import com.diligrp.rider.entity.Substation;
  10 +import com.diligrp.rider.entity.SysRole;
9 import com.diligrp.rider.mapper.AdminUserMapper; 11 import com.diligrp.rider.mapper.AdminUserMapper;
  12 +import com.diligrp.rider.mapper.CityMapper;
10 import com.diligrp.rider.mapper.SubstationMapper; 13 import com.diligrp.rider.mapper.SubstationMapper;
  14 +import com.diligrp.rider.service.AdminMenuService;
  15 +import com.diligrp.rider.vo.AdminLoginUserVO;
11 import com.diligrp.rider.vo.AdminLoginVO; 16 import com.diligrp.rider.vo.AdminLoginVO;
  17 +import com.diligrp.rider.vo.AdminMenuVO;
12 import lombok.RequiredArgsConstructor; 18 import lombok.RequiredArgsConstructor;
13 import org.springframework.stereotype.Service; 19 import org.springframework.stereotype.Service;
14 import org.springframework.util.DigestUtils; 20 import org.springframework.util.DigestUtils;
15 21
16 import java.nio.charset.StandardCharsets; 22 import java.nio.charset.StandardCharsets;
  23 +import java.util.List;
17 24
18 @Service 25 @Service
19 @RequiredArgsConstructor 26 @RequiredArgsConstructor
20 public class AdminAuthServiceImpl { 27 public class AdminAuthServiceImpl {
21 28
  29 + private static final String ROLE_TYPE_ADMIN = "admin";
  30 + private static final String ROLE_TYPE_SUBSTATION = "substation";
  31 +
22 private final AdminUserMapper adminUserMapper; 32 private final AdminUserMapper adminUserMapper;
23 private final SubstationMapper substationMapper; 33 private final SubstationMapper substationMapper;
  34 + private final CityMapper cityMapper;
  35 + private final AdminMenuService adminMenuService;
24 private final JwtUtil jwtUtil; 36 private final JwtUtil jwtUtil;
25 37
26 /** 38 /**
@@ -29,7 +41,7 @@ public class AdminAuthServiceImpl { @@ -29,7 +41,7 @@ public class AdminAuthServiceImpl {
29 * role=substation:分站管理员登录(substation表) 41 * role=substation:分站管理员登录(substation表)
30 */ 42 */
31 public AdminLoginVO login(AdminLoginDTO dto) { 43 public AdminLoginVO login(AdminLoginDTO dto) {
32 - if ("admin".equals(dto.getRole())) { 44 + if (ROLE_TYPE_ADMIN.equals(dto.getRole())) {
33 return loginAdmin(dto.getAccount(), dto.getPass()); 45 return loginAdmin(dto.getAccount(), dto.getPass());
34 } 46 }
35 return loginSubstation(dto.getAccount(), dto.getPass()); 47 return loginSubstation(dto.getAccount(), dto.getPass());
@@ -42,12 +54,22 @@ public class AdminAuthServiceImpl { @@ -42,12 +54,22 @@ public class AdminAuthServiceImpl {
42 if (!encryptPass(pass).equals(user.getUserPass())) throw new BizException("密码错误"); 54 if (!encryptPass(pass).equals(user.getUserPass())) throw new BizException("密码错误");
43 if (user.getUserStatus() == null || user.getUserStatus() == 0) throw new BizException("账号已被禁用"); 55 if (user.getUserStatus() == null || user.getUserStatus() == 0) throw new BizException("账号已被禁用");
44 56
  57 + SysRole role = adminMenuService.resolveRole(user.getRoleId(), ROLE_TYPE_ADMIN);
  58 + List<AdminMenuVO> menus = adminMenuService.getMenus(role, ROLE_TYPE_ADMIN);
  59 +
  60 + AdminLoginUserVO loginUser = new AdminLoginUserVO();
  61 + loginUser.setId(user.getId());
  62 + loginUser.setUserLogin(user.getUserLogin());
  63 + loginUser.setUserNickname(user.getUserNickname());
  64 + loginUser.setRole(ROLE_TYPE_ADMIN);
  65 + loginUser.setRoleType(ROLE_TYPE_ADMIN);
  66 + loginUser.setRoleCode(role.getCode());
  67 +
45 AdminLoginVO vo = new AdminLoginVO(); 68 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")); 69 + vo.setToken(jwtUtil.generateAdminToken(user.getId(), ROLE_TYPE_ADMIN));
  70 + vo.setUser(loginUser);
  71 + vo.setMenus(menus);
  72 + vo.setHomePath(adminMenuService.resolveHomePath(menus));
51 return vo; 73 return vo;
52 } 74 }
53 75
@@ -57,14 +79,27 @@ public class AdminAuthServiceImpl { @@ -57,14 +79,27 @@ public class AdminAuthServiceImpl {
57 if (sub == null) throw new BizException("账号不存在"); 79 if (sub == null) throw new BizException("账号不存在");
58 if (!encryptPass(pass).equals(sub.getUserPass())) throw new BizException("密码错误"); 80 if (!encryptPass(pass).equals(sub.getUserPass())) throw new BizException("密码错误");
59 if (sub.getUserStatus() == null || sub.getUserStatus() == 0) throw new BizException("账号已被禁用"); 81 if (sub.getUserStatus() == null || sub.getUserStatus() == 0) throw new BizException("账号已被禁用");
  82 + if (sub.getCityId() == null || sub.getCityId() < 1) throw new BizException("当前分站账号未绑定有效城市");
  83 +
  84 + SysRole role = adminMenuService.resolveRole(sub.getRoleId(), ROLE_TYPE_SUBSTATION);
  85 + List<AdminMenuVO> menus = adminMenuService.getMenus(role, ROLE_TYPE_SUBSTATION);
  86 + City city = cityMapper.selectById(sub.getCityId());
  87 +
  88 + AdminLoginUserVO loginUser = new AdminLoginUserVO();
  89 + loginUser.setId(sub.getId());
  90 + loginUser.setUserLogin(sub.getUserLogin());
  91 + loginUser.setUserNickname(sub.getUserNickname());
  92 + loginUser.setRole(ROLE_TYPE_SUBSTATION);
  93 + loginUser.setRoleType(ROLE_TYPE_SUBSTATION);
  94 + loginUser.setRoleCode(role.getCode());
  95 + loginUser.setCityId(sub.getCityId());
  96 + loginUser.setCityName(city != null ? city.getName() : null);
60 97
61 AdminLoginVO vo = new AdminLoginVO(); 98 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")); 99 + vo.setToken(jwtUtil.generateAdminToken(sub.getId(), ROLE_TYPE_SUBSTATION));
  100 + vo.setUser(loginUser);
  101 + vo.setMenus(menus);
  102 + vo.setHomePath(adminMenuService.resolveHomePath(menus));
68 return vo; 103 return vo;
69 } 104 }
70 105
src/main/java/com/diligrp/rider/service/impl/AdminMenuServiceImpl.java 0 → 100644
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.enums.AdminRoleScopeEnum;
  5 +import com.diligrp.rider.common.enums.MenuScopeEnum;
  6 +import com.diligrp.rider.common.exception.BizException;
  7 +import com.diligrp.rider.entity.SysMenu;
  8 +import com.diligrp.rider.entity.SysRole;
  9 +import com.diligrp.rider.entity.SysRoleMenu;
  10 +import com.diligrp.rider.mapper.SysMenuMapper;
  11 +import com.diligrp.rider.mapper.SysRoleMapper;
  12 +import com.diligrp.rider.mapper.SysRoleMenuMapper;
  13 +import com.diligrp.rider.service.AdminMenuService;
  14 +import com.diligrp.rider.vo.AdminMenuVO;
  15 +import lombok.RequiredArgsConstructor;
  16 +import org.springframework.stereotype.Service;
  17 +
  18 +import java.util.ArrayList;
  19 +import java.util.LinkedHashMap;
  20 +import java.util.LinkedHashSet;
  21 +import java.util.List;
  22 +import java.util.Map;
  23 +import java.util.Set;
  24 +
  25 +@Service
  26 +@RequiredArgsConstructor
  27 +public class AdminMenuServiceImpl implements AdminMenuService {
  28 +
  29 + private static final String ROLE_TYPE_ADMIN = "admin";
  30 + private static final String ROLE_TYPE_SUBSTATION = "substation";
  31 + private static final String ROLE_CODE_PLATFORM = "platform_admin";
  32 + private static final String ROLE_CODE_SUBSTATION = "substation_admin";
  33 +
  34 + private final SysRoleMapper sysRoleMapper;
  35 + private final SysMenuMapper sysMenuMapper;
  36 + private final SysRoleMenuMapper sysRoleMenuMapper;
  37 +
  38 + @Override
  39 + public SysRole resolveRole(Long roleId, String roleType) {
  40 + String fallbackCode = ROLE_TYPE_ADMIN.equals(roleType) ? ROLE_CODE_PLATFORM : ROLE_CODE_SUBSTATION;
  41 + String requiredScope = ROLE_TYPE_ADMIN.equals(roleType)
  42 + ? AdminRoleScopeEnum.PLATFORM.name()
  43 + : AdminRoleScopeEnum.SUBSTATION.name();
  44 +
  45 + SysRole role = null;
  46 + if (roleId != null && roleId > 0) {
  47 + role = sysRoleMapper.selectById(roleId);
  48 + }
  49 + if (role == null) {
  50 + role = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>()
  51 + .eq(SysRole::getCode, fallbackCode)
  52 + .eq(SysRole::getStatus, 1)
  53 + .last("LIMIT 1"));
  54 + }
  55 + if (role == null || role.getStatus() == null || role.getStatus() != 1) {
  56 + throw new BizException("当前账号未分配有效菜单角色");
  57 + }
  58 + if (!requiredScope.equals(role.getRoleScope())) {
  59 + throw new BizException("当前账号角色归属不匹配");
  60 + }
  61 + return role;
  62 + }
  63 +
  64 + @Override
  65 + public List<AdminMenuVO> getMenus(SysRole role, String roleType) {
  66 + List<SysRoleMenu> roleMenus = sysRoleMenuMapper.selectList(new LambdaQueryWrapper<SysRoleMenu>()
  67 + .eq(SysRoleMenu::getRoleId, role.getId()));
  68 + if (roleMenus.isEmpty()) {
  69 + return List.of();
  70 + }
  71 +
  72 + List<SysMenu> allMenus = sysMenuMapper.selectList(new LambdaQueryWrapper<SysMenu>()
  73 + .eq(SysMenu::getStatus, 1)
  74 + .eq(SysMenu::getVisible, 1)
  75 + .orderByAsc(SysMenu::getListOrder)
  76 + .orderByAsc(SysMenu::getId));
  77 + if (allMenus.isEmpty()) {
  78 + return List.of();
  79 + }
  80 +
  81 + Map<Long, SysMenu> menuMap = new LinkedHashMap<>();
  82 + for (SysMenu menu : allMenus) {
  83 + if (isScopeAllowed(menu.getMenuScope(), roleType)) {
  84 + menuMap.put(menu.getId(), menu);
  85 + }
  86 + }
  87 + if (menuMap.isEmpty()) {
  88 + return List.of();
  89 + }
  90 +
  91 + Set<Long> includedIds = new LinkedHashSet<>();
  92 + for (SysRoleMenu roleMenu : roleMenus) {
  93 + includeWithParents(roleMenu.getMenuId(), menuMap, includedIds);
  94 + }
  95 +
  96 + Map<Long, AdminMenuVO> voMap = new LinkedHashMap<>();
  97 + List<AdminMenuVO> roots = new ArrayList<>();
  98 + for (SysMenu menu : allMenus) {
  99 + if (!includedIds.contains(menu.getId())) {
  100 + continue;
  101 + }
  102 + AdminMenuVO vo = toVO(menu);
  103 + voMap.put(menu.getId(), vo);
  104 + Long parentId = menu.getParentId() == null ? 0L : menu.getParentId();
  105 + if (parentId > 0 && voMap.containsKey(parentId)) {
  106 + voMap.get(parentId).getChildren().add(vo);
  107 + } else {
  108 + roots.add(vo);
  109 + }
  110 + }
  111 + return roots;
  112 + }
  113 +
  114 + @Override
  115 + public String resolveHomePath(List<AdminMenuVO> menus) {
  116 + String homePath = findPathByCode(menus, "dashboard");
  117 + if (homePath != null && !homePath.isBlank()) {
  118 + return homePath;
  119 + }
  120 + String firstPath = findFirstPath(menus);
  121 + return firstPath == null || firstPath.isBlank() ? "/dashboard" : firstPath;
  122 + }
  123 +
  124 + private void includeWithParents(Long menuId, Map<Long, SysMenu> menuMap, Set<Long> includedIds) {
  125 + SysMenu current = menuMap.get(menuId);
  126 + while (current != null && includedIds.add(current.getId())) {
  127 + Long parentId = current.getParentId();
  128 + if (parentId == null || parentId < 1) {
  129 + break;
  130 + }
  131 + current = menuMap.get(parentId);
  132 + }
  133 + }
  134 +
  135 + private boolean isScopeAllowed(String menuScope, String roleType) {
  136 + if (MenuScopeEnum.BOTH.name().equals(menuScope)) {
  137 + return true;
  138 + }
  139 + if (ROLE_TYPE_ADMIN.equals(roleType)) {
  140 + return MenuScopeEnum.PLATFORM.name().equals(menuScope);
  141 + }
  142 + if (ROLE_TYPE_SUBSTATION.equals(roleType)) {
  143 + return MenuScopeEnum.SUBSTATION.name().equals(menuScope);
  144 + }
  145 + return false;
  146 + }
  147 +
  148 + private AdminMenuVO toVO(SysMenu menu) {
  149 + AdminMenuVO vo = new AdminMenuVO();
  150 + vo.setId(menu.getId());
  151 + vo.setCode(menu.getCode());
  152 + vo.setName(menu.getName());
  153 + vo.setType(menu.getType());
  154 + vo.setPath(menu.getPath());
  155 + vo.setIcon(menu.getIcon());
  156 + return vo;
  157 + }
  158 +
  159 + private String findPathByCode(List<AdminMenuVO> menus, String code) {
  160 + for (AdminMenuVO menu : menus) {
  161 + if (code.equals(menu.getCode()) && menu.getPath() != null && !menu.getPath().isBlank()) {
  162 + return menu.getPath();
  163 + }
  164 + String nested = findPathByCode(menu.getChildren(), code);
  165 + if (nested != null) {
  166 + return nested;
  167 + }
  168 + }
  169 + return null;
  170 + }
  171 +
  172 + private String findFirstPath(List<AdminMenuVO> menus) {
  173 + for (AdminMenuVO menu : menus) {
  174 + if (menu.getPath() != null && !menu.getPath().isBlank()) {
  175 + return menu.getPath();
  176 + }
  177 + String nested = findFirstPath(menu.getChildren());
  178 + if (nested != null) {
  179 + return nested;
  180 + }
  181 + }
  182 + return null;
  183 + }
  184 +}
src/main/java/com/diligrp/rider/service/impl/AdminRiderServiceImpl.java
@@ -2,6 +2,7 @@ package com.diligrp.rider.service.impl; @@ -2,6 +2,7 @@ package com.diligrp.rider.service.impl;
2 2
3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.auth.AdminScopeGuard;
5 import com.diligrp.rider.common.exception.BizException; 6 import com.diligrp.rider.common.exception.BizException;
6 import com.diligrp.rider.dto.AdminRiderAddDTO; 7 import com.diligrp.rider.dto.AdminRiderAddDTO;
7 import com.diligrp.rider.entity.Orders; 8 import com.diligrp.rider.entity.Orders;
@@ -35,6 +36,7 @@ public class AdminRiderServiceImpl implements AdminRiderService { @@ -35,6 +36,7 @@ public class AdminRiderServiceImpl implements AdminRiderService {
35 private final RiderLevelMapper riderLevelMapper; 36 private final RiderLevelMapper riderLevelMapper;
36 private final OrdersMapper ordersMapper; 37 private final OrdersMapper ordersMapper;
37 private final RiderBalanceMapper balanceMapper; 38 private final RiderBalanceMapper balanceMapper;
  39 + private final AdminScopeGuard adminScopeGuard;
38 40
39 @Override 41 @Override
40 public void add(AdminRiderAddDTO dto, Long cityId) { 42 public void add(AdminRiderAddDTO dto, Long cityId) {
@@ -107,9 +109,10 @@ public class AdminRiderServiceImpl implements AdminRiderService { @@ -107,9 +109,10 @@ public class AdminRiderServiceImpl implements AdminRiderService {
107 } 109 }
108 110
109 @Override 111 @Override
110 - public void setStatus(Long riderId, int status) { 112 + public void setStatus(Long riderId, int status, Long cityId) {
111 Rider rider = riderMapper.selectById(riderId); 113 Rider rider = riderMapper.selectById(riderId);
112 if (rider == null) throw new BizException("骑手不存在"); 114 if (rider == null) throw new BizException("骑手不存在");
  115 + adminScopeGuard.assertCityAccessible(cityId, rider.getCityId());
113 riderMapper.update(null, new LambdaUpdateWrapper<Rider>() 116 riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
114 .eq(Rider::getId, riderId) 117 .eq(Rider::getId, riderId)
115 .set(Rider::getUserStatus, status)); 118 .set(Rider::getUserStatus, status));
@@ -135,9 +138,10 @@ public class AdminRiderServiceImpl implements AdminRiderService { @@ -135,9 +138,10 @@ public class AdminRiderServiceImpl implements AdminRiderService {
135 } 138 }
136 139
137 @Override 140 @Override
138 - public void setEnableStatus(Long riderId, int status) { 141 + public void setEnableStatus(Long riderId, int status, Long cityId) {
139 Rider rider = riderMapper.selectById(riderId); 142 Rider rider = riderMapper.selectById(riderId);
140 if (rider == null) throw new BizException("骑手不存在"); 143 if (rider == null) throw new BizException("骑手不存在");
  144 + adminScopeGuard.assertCityAccessible(cityId, rider.getCityId());
141 riderMapper.update(null, new LambdaUpdateWrapper<Rider>() 145 riderMapper.update(null, new LambdaUpdateWrapper<Rider>()
142 .eq(Rider::getId, riderId) 146 .eq(Rider::getId, riderId)
143 .set(Rider::getStatus, status)); 147 .set(Rider::getStatus, status));
@@ -145,9 +149,10 @@ public class AdminRiderServiceImpl implements AdminRiderService { @@ -145,9 +149,10 @@ public class AdminRiderServiceImpl implements AdminRiderService {
145 149
146 @Override 150 @Override
147 @Transactional 151 @Transactional
148 - public void setType(Long riderId, int type) { 152 + public void setType(Long riderId, int type, Long cityId) {
149 Rider rider = riderMapper.selectById(riderId); 153 Rider rider = riderMapper.selectById(riderId);
150 if (rider == null) throw new BizException("骑手不存在"); 154 if (rider == null) throw new BizException("骑手不存在");
  155 + adminScopeGuard.assertCityAccessible(cityId, rider.getCityId());
151 if (type == 2) { 156 if (type == 2) {
152 if (rider.getBalance() != null && rider.getBalance().compareTo(BigDecimal.ZERO) > 0) { 157 if (rider.getBalance() != null && rider.getBalance().compareTo(BigDecimal.ZERO) > 0) {
153 throw new BizException("变更为全职前要保证余额为0"); 158 throw new BizException("变更为全职前要保证余额为0");
@@ -164,9 +169,15 @@ public class AdminRiderServiceImpl implements AdminRiderService { @@ -164,9 +169,15 @@ public class AdminRiderServiceImpl implements AdminRiderService {
164 169
165 @Override 170 @Override
166 @Transactional 171 @Transactional
167 - public void designate(Long orderId, Long riderId) { 172 + public void designate(Long orderId, Long riderId, Long cityId) {
168 Orders order = ordersMapper.selectById(orderId); 173 Orders order = ordersMapper.selectById(orderId);
169 if (order == null) throw new BizException("订单不存在"); 174 if (order == null) throw new BizException("订单不存在");
  175 + adminScopeGuard.assertCityAccessible(cityId, order.getCityId());
  176 + Rider rider = riderMapper.selectById(riderId);
  177 + if (rider == null) throw new BizException("骑手不存在");
  178 + if (!order.getCityId().equals(rider.getCityId())) {
  179 + throw new BizException("骑手与订单城市不匹配");
  180 + }
170 if (order.getStatus() == 1) throw new BizException("订单未支付,无法指派"); 181 if (order.getStatus() == 1) throw new BizException("订单未支付,无法指派");
171 if (order.getStatus() == 10) throw new BizException("订单已取消,无法指派"); 182 if (order.getStatus() == 10) throw new BizException("订单已取消,无法指派");
172 if (order.getStatus() != 2) throw new BizException("订单已服务中,无法指派"); 183 if (order.getStatus() != 2) throw new BizException("订单已服务中,无法指派");
@@ -191,9 +202,10 @@ public class AdminRiderServiceImpl implements AdminRiderService { @@ -191,9 +202,10 @@ public class AdminRiderServiceImpl implements AdminRiderService {
191 202
192 @Override 203 @Override
193 @Transactional 204 @Transactional
194 - public void setTrans(Long orderId, int trans) { 205 + public void setTrans(Long orderId, int trans, Long cityId) {
195 Orders order = ordersMapper.selectById(orderId); 206 Orders order = ordersMapper.selectById(orderId);
196 if (order == null) throw new BizException("订单不存在"); 207 if (order == null) throw new BizException("订单不存在");
  208 + adminScopeGuard.assertCityAccessible(cityId, order.getCityId());
197 if (order.getStatus() != 4) throw new BizException("订单状态错误,无法操作"); 209 if (order.getStatus() != 4) throw new BizException("订单状态错误,无法操作");
198 if (order.getIsTrans() != 2) throw new BizException("订单未申请转单,无法操作"); 210 if (order.getIsTrans() != 2) throw new BizException("订单未申请转单,无法操作");
199 211
src/main/java/com/diligrp/rider/service/impl/AdminUserManageServiceImpl.java 0 → 100644
  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.enums.AdminRoleScopeEnum;
  6 +import com.diligrp.rider.common.exception.BizException;
  7 +import com.diligrp.rider.entity.AdminUser;
  8 +import com.diligrp.rider.mapper.AdminUserMapper;
  9 +import com.diligrp.rider.service.AdminUserManageService;
  10 +import com.diligrp.rider.service.RoleScopeGuardService;
  11 +import lombok.RequiredArgsConstructor;
  12 +import org.springframework.stereotype.Service;
  13 +import org.springframework.util.DigestUtils;
  14 +
  15 +import java.nio.charset.StandardCharsets;
  16 +import java.util.List;
  17 +
  18 +@Service
  19 +@RequiredArgsConstructor
  20 +public class AdminUserManageServiceImpl implements AdminUserManageService {
  21 +
  22 + private final AdminUserMapper adminUserMapper;
  23 + private final RoleScopeGuardService roleScopeGuardService;
  24 +
  25 + @Override
  26 + public List<AdminUser> list(String keyword) {
  27 + LambdaQueryWrapper<AdminUser> wrapper = new LambdaQueryWrapper<AdminUser>()
  28 + .orderByDesc(AdminUser::getId);
  29 + if (keyword != null && !keyword.isBlank()) {
  30 + wrapper.like(AdminUser::getUserLogin, keyword)
  31 + .or().like(AdminUser::getUserNickname, keyword);
  32 + }
  33 + return adminUserMapper.selectList(wrapper);
  34 + }
  35 +
  36 + @Override
  37 + public void add(AdminUser adminUser) {
  38 + Long exists = adminUserMapper.selectCount(new LambdaQueryWrapper<AdminUser>()
  39 + .eq(AdminUser::getUserLogin, adminUser.getUserLogin()));
  40 + if (exists > 0) {
  41 + throw new BizException("账号已存在,请更换");
  42 + }
  43 + roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name());
  44 + adminUser.setUserPass(encryptPass(adminUser.getUserPass()));
  45 + adminUser.setUserStatus(1);
  46 + adminUser.setCreateTime(System.currentTimeMillis() / 1000);
  47 + adminUserMapper.insert(adminUser);
  48 + }
  49 +
  50 + @Override
  51 + public void edit(AdminUser adminUser) {
  52 + AdminUser existing = adminUserMapper.selectById(adminUser.getId());
  53 + if (existing == null) {
  54 + throw new BizException("平台账号不存在");
  55 + }
  56 + roleScopeGuardService.requireRole(adminUser.getRoleId(), AdminRoleScopeEnum.PLATFORM.name());
  57 + if (adminUser.getUserPass() == null || adminUser.getUserPass().isBlank()) {
  58 + adminUser.setUserPass(null);
  59 + } else {
  60 + adminUser.setUserPass(encryptPass(adminUser.getUserPass()));
  61 + }
  62 + adminUserMapper.updateById(adminUser);
  63 + }
  64 +
  65 + @Override
  66 + public void ban(Long id) {
  67 + adminUserMapper.update(null, new LambdaUpdateWrapper<AdminUser>()
  68 + .eq(AdminUser::getId, id)
  69 + .set(AdminUser::getUserStatus, 0));
  70 + }
  71 +
  72 + @Override
  73 + public void cancelBan(Long id) {
  74 + adminUserMapper.update(null, new LambdaUpdateWrapper<AdminUser>()
  75 + .eq(AdminUser::getId, id)
  76 + .set(AdminUser::getUserStatus, 1));
  77 + }
  78 +
  79 + @Override
  80 + public void del(Long id) {
  81 + adminUserMapper.deleteById(id);
  82 + }
  83 +
  84 + @Override
  85 + public void changePassword(Long id, String oldPassword, String newPassword) {
  86 + AdminUser user = adminUserMapper.selectById(id);
  87 + if (user == null) {
  88 + throw new BizException("平台账号不存在");
  89 + }
  90 + if (!encryptPass(oldPassword).equals(user.getUserPass())) {
  91 + throw new BizException("原密码不正确");
  92 + }
  93 + if (encryptPass(newPassword).equals(user.getUserPass())) {
  94 + throw new BizException("新密码不能与原密码相同");
  95 + }
  96 + adminUserMapper.update(null, new LambdaUpdateWrapper<AdminUser>()
  97 + .eq(AdminUser::getId, id)
  98 + .set(AdminUser::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/MenuBootstrapServiceImpl.java 0 → 100644
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.enums.AdminRoleScopeEnum;
  5 +import com.diligrp.rider.common.enums.MenuScopeEnum;
  6 +import com.diligrp.rider.entity.SysMenu;
  7 +import com.diligrp.rider.entity.SysRole;
  8 +import com.diligrp.rider.entity.SysRoleMenu;
  9 +import com.diligrp.rider.mapper.SysMenuMapper;
  10 +import com.diligrp.rider.mapper.SysRoleMapper;
  11 +import com.diligrp.rider.mapper.SysRoleMenuMapper;
  12 +import com.diligrp.rider.service.MenuBootstrapService;
  13 +import jakarta.annotation.PostConstruct;
  14 +import lombok.RequiredArgsConstructor;
  15 +import org.springframework.stereotype.Service;
  16 +
  17 +import java.util.ArrayList;
  18 +import java.util.LinkedHashMap;
  19 +import java.util.List;
  20 +import java.util.Map;
  21 +
  22 +@Service
  23 +@RequiredArgsConstructor
  24 +public class MenuBootstrapServiceImpl implements MenuBootstrapService {
  25 +
  26 + private final SysRoleMapper sysRoleMapper;
  27 + private final SysMenuMapper sysMenuMapper;
  28 + private final SysRoleMenuMapper sysRoleMenuMapper;
  29 +
  30 + @PostConstruct
  31 + @Override
  32 + public void initializeDefaults() {
  33 + SysRole platformRole = ensureRole("platform_admin", "平台管理员", AdminRoleScopeEnum.PLATFORM.name());
  34 + SysRole substationRole = ensureRole("substation_admin", "分站管理员", AdminRoleScopeEnum.SUBSTATION.name());
  35 +
  36 + List<SysMenu> seededMenus = seedMenus();
  37 + bindMenus(platformRole, seededMenus, MenuScopeEnum.PLATFORM, MenuScopeEnum.BOTH);
  38 + bindMenus(substationRole, seededMenus, MenuScopeEnum.SUBSTATION, MenuScopeEnum.BOTH);
  39 + }
  40 +
  41 + private SysRole ensureRole(String code, String name, String scope) {
  42 + SysRole role = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>()
  43 + .eq(SysRole::getCode, code)
  44 + .last("LIMIT 1"));
  45 + if (role != null) {
  46 + return role;
  47 + }
  48 + role = new SysRole();
  49 + role.setCode(code);
  50 + role.setName(name);
  51 + role.setRoleScope(scope);
  52 + role.setStatus(1);
  53 + role.setCreateTime(System.currentTimeMillis() / 1000);
  54 + sysRoleMapper.insert(role);
  55 + return role;
  56 + }
  57 +
  58 + private List<SysMenu> seedMenus() {
  59 + List<SysMenu> defaults = new ArrayList<>();
  60 + defaults.add(menu("dashboard", "工作台", "MENU", "/dashboard", "HomeOutlined", 0L, MenuScopeEnum.BOTH, 10));
  61 + defaults.add(menu("city.manage", "租户管理", "MENU", "/city", "GlobalOutlined", 0L, MenuScopeEnum.PLATFORM, 20));
  62 + defaults.add(menu("substation.manage", "分站管理", "MENU", "/substation", "ApartmentOutlined", 0L, MenuScopeEnum.PLATFORM, 30));
  63 + defaults.add(menu("merchant.root", "商家管理", "DIR", "", "ShopOutlined", 0L, MenuScopeEnum.PLATFORM, 40));
  64 + defaults.add(menu("merchant.enter", "入驻申请", "MENU", "/merchant/enter", "", 0L, MenuScopeEnum.PLATFORM, 41));
  65 + defaults.add(menu("merchant.store", "店铺管理", "MENU", "/merchant/store", "", 0L, MenuScopeEnum.PLATFORM, 42));
  66 + defaults.add(menu("rider.list", "骑手管理", "MENU", "/rider", "UserOutlined", 0L, MenuScopeEnum.BOTH, 50));
  67 + defaults.add(menu("rider.evaluate", "骑手评价", "MENU", "/rider/evaluate", "StarOutlined", 0L, MenuScopeEnum.BOTH, 60));
  68 + defaults.add(menu("order.root", "订单管理", "DIR", "", "UnorderedListOutlined", 0L, MenuScopeEnum.BOTH, 70));
  69 + defaults.add(menu("order.list", "订单列表", "MENU", "/order", "", 0L, MenuScopeEnum.BOTH, 71));
  70 + defaults.add(menu("order.refund", "退款管理", "MENU", "/refund", "", 0L, MenuScopeEnum.BOTH, 72));
  71 + defaults.add(menu("order.delivery", "配送订单", "MENU", "/delivery/order", "", 0L, MenuScopeEnum.BOTH, 73));
  72 + defaults.add(menu("config.root", "配置中心", "DIR", "", "ControlOutlined", 0L, MenuScopeEnum.BOTH, 80));
  73 + defaults.add(menu("fee.plan", "配送费配置", "MENU", "/config/fee-plan", "", 0L, MenuScopeEnum.BOTH, 81));
  74 + defaults.add(menu("dispatch.rule", "调度配置", "MENU", "/dispatch/rule", "", 0L, MenuScopeEnum.BOTH, 82));
  75 + defaults.add(menu("open.root", "开放平台", "DIR", "", "ApiOutlined", 0L, MenuScopeEnum.PLATFORM, 90));
  76 + defaults.add(menu("open.app", "应用管理", "MENU", "/open", "", 0L, MenuScopeEnum.PLATFORM, 91));
  77 + defaults.add(menu("open.mock_delivery", "模拟推单", "MENU", "/open/mock-delivery", "", 0L, MenuScopeEnum.PLATFORM, 92));
  78 + defaults.add(menu("system.root", "系统管理", "DIR", "", "ControlOutlined", 0L, MenuScopeEnum.PLATFORM, 100));
  79 + defaults.add(menu("system.menu", "菜单管理", "MENU", "/system/menu", "", 0L, MenuScopeEnum.PLATFORM, 101));
  80 + defaults.add(menu("system.role_menu", "角色菜单", "MENU", "/system/role-menu", "", 0L, MenuScopeEnum.PLATFORM, 102));
  81 + defaults.add(menu("admin.user", "平台账号", "MENU", "/admin-user", "", 0L, MenuScopeEnum.PLATFORM, 103));
  82 +
  83 + Map<String, SysMenu> persisted = new LinkedHashMap<>();
  84 + for (SysMenu menu : defaults) {
  85 + SysMenu existing = sysMenuMapper.selectOne(new LambdaQueryWrapper<SysMenu>()
  86 + .eq(SysMenu::getCode, menu.getCode())
  87 + .last("LIMIT 1"));
  88 + if (existing == null) {
  89 + menu.setCreateTime(System.currentTimeMillis() / 1000);
  90 + sysMenuMapper.insert(menu);
  91 + existing = menu;
  92 + }
  93 + persisted.put(existing.getCode(), existing);
  94 + }
  95 +
  96 + persisted.get("merchant.enter").setParentId(persisted.get("merchant.root").getId());
  97 + persisted.get("merchant.store").setParentId(persisted.get("merchant.root").getId());
  98 + persisted.get("order.list").setParentId(persisted.get("order.root").getId());
  99 + persisted.get("order.refund").setParentId(persisted.get("order.root").getId());
  100 + persisted.get("order.delivery").setParentId(persisted.get("order.root").getId());
  101 + persisted.get("fee.plan").setParentId(persisted.get("config.root").getId());
  102 + persisted.get("dispatch.rule").setParentId(persisted.get("config.root").getId());
  103 + persisted.get("open.app").setParentId(persisted.get("open.root").getId());
  104 + persisted.get("open.mock_delivery").setParentId(persisted.get("open.root").getId());
  105 + persisted.get("system.menu").setParentId(persisted.get("system.root").getId());
  106 + persisted.get("system.role_menu").setParentId(persisted.get("system.root").getId());
  107 + persisted.get("admin.user").setParentId(persisted.get("system.root").getId());
  108 +
  109 + for (SysMenu menu : persisted.values()) {
  110 + sysMenuMapper.updateById(menu);
  111 + }
  112 + return new ArrayList<>(persisted.values());
  113 + }
  114 +
  115 + private void bindMenus(SysRole role, List<SysMenu> menus, MenuScopeEnum primaryScope, MenuScopeEnum commonScope) {
  116 + for (SysMenu menu : menus) {
  117 + if (!primaryScope.name().equals(menu.getMenuScope()) && !commonScope.name().equals(menu.getMenuScope())) {
  118 + continue;
  119 + }
  120 + Long exists = sysRoleMenuMapper.selectCount(new LambdaQueryWrapper<SysRoleMenu>()
  121 + .eq(SysRoleMenu::getRoleId, role.getId())
  122 + .eq(SysRoleMenu::getMenuId, menu.getId()));
  123 + if (exists > 0) {
  124 + continue;
  125 + }
  126 + SysRoleMenu roleMenu = new SysRoleMenu();
  127 + roleMenu.setRoleId(role.getId());
  128 + roleMenu.setMenuId(menu.getId());
  129 + roleMenu.setCreateTime(System.currentTimeMillis() / 1000);
  130 + sysRoleMenuMapper.insert(roleMenu);
  131 + }
  132 + }
  133 +
  134 + private SysMenu menu(String code, String name, String type, String path, String icon,
  135 + Long parentId, MenuScopeEnum scope, int sort) {
  136 + SysMenu menu = new SysMenu();
  137 + menu.setCode(code);
  138 + menu.setName(name);
  139 + menu.setType(type);
  140 + menu.setPath(path);
  141 + menu.setIcon(icon);
  142 + menu.setParentId(parentId);
  143 + menu.setMenuScope(scope.name());
  144 + menu.setListOrder(sort);
  145 + menu.setVisible(1);
  146 + menu.setStatus(1);
  147 + return menu;
  148 + }
  149 +}
src/main/java/com/diligrp/rider/service/impl/RefundServiceImpl.java
@@ -2,6 +2,7 @@ package com.diligrp.rider.service.impl; @@ -2,6 +2,7 @@ package com.diligrp.rider.service.impl;
2 2
3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.auth.AdminScopeGuard;
5 import com.diligrp.rider.common.exception.BizException; 6 import com.diligrp.rider.common.exception.BizException;
6 import com.diligrp.rider.entity.OrderRefundReason; 7 import com.diligrp.rider.entity.OrderRefundReason;
7 import com.diligrp.rider.entity.OrderRefundRecord; 8 import com.diligrp.rider.entity.OrderRefundRecord;
@@ -33,6 +34,7 @@ public class RefundServiceImpl implements RefundService { @@ -33,6 +34,7 @@ public class RefundServiceImpl implements RefundService {
33 private final OrdersMapper ordersMapper; 34 private final OrdersMapper ordersMapper;
34 private final WebhookService webhookService; 35 private final WebhookService webhookService;
35 private final ObjectMapper objectMapper; 36 private final ObjectMapper objectMapper;
  37 + private final AdminScopeGuard adminScopeGuard;
36 38
37 @Override 39 @Override
38 public List<OrderRefundReason> getReasons(int role) { 40 public List<OrderRefundReason> getReasons(int role) {
@@ -88,11 +90,16 @@ public class RefundServiceImpl implements RefundService { @@ -88,11 +90,16 @@ public class RefundServiceImpl implements RefundService {
88 90
89 @Override 91 @Override
90 @Transactional 92 @Transactional
91 - public void handleRefund(Long recordId, int status, String remark) { 93 + public void handleRefund(Long recordId, int status, String remark, Long cityId) {
92 OrderRefundRecord record = recordMapper.selectById(recordId); 94 OrderRefundRecord record = recordMapper.selectById(recordId);
93 if (record == null) throw new BizException("退款记录不存在"); 95 if (record == null) throw new BizException("退款记录不存在");
94 if (record.getStatus() != 0) throw new BizException("该退款申请已处理"); 96 if (record.getStatus() != 0) throw new BizException("该退款申请已处理");
95 97
  98 + Orders order = ordersMapper.selectById(record.getOid());
  99 + if (order != null) {
  100 + adminScopeGuard.assertCityAccessible(cityId, order.getCityId());
  101 + }
  102 +
96 long now = System.currentTimeMillis() / 1000; 103 long now = System.currentTimeMillis() / 1000;
97 recordMapper.update(null, new LambdaUpdateWrapper<OrderRefundRecord>() 104 recordMapper.update(null, new LambdaUpdateWrapper<OrderRefundRecord>()
98 .eq(OrderRefundRecord::getId, recordId) 105 .eq(OrderRefundRecord::getId, recordId)
@@ -100,7 +107,7 @@ public class RefundServiceImpl implements RefundService { @@ -100,7 +107,7 @@ public class RefundServiceImpl implements RefundService {
100 .set(OrderRefundRecord::getRemark, remark) 107 .set(OrderRefundRecord::getRemark, remark)
101 .set(OrderRefundRecord::getHandleTime, now)); 108 .set(OrderRefundRecord::getHandleTime, now));
102 109
103 - Orders order = ordersMapper.selectById(record.getOid()); 110 + order = ordersMapper.selectById(record.getOid());
104 if (order == null) return; 111 if (order == null) return;
105 112
106 if (status == 1) { 113 if (status == 1) {
@@ -119,7 +126,12 @@ public class RefundServiceImpl implements RefundService { @@ -119,7 +126,12 @@ public class RefundServiceImpl implements RefundService {
119 } 126 }
120 127
121 @Override 128 @Override
122 - public OrderRefundRecord getByOrderId(Long orderId) { 129 + public OrderRefundRecord getByOrderId(Long orderId, Long cityId) {
  130 + Orders order = ordersMapper.selectById(orderId);
  131 + if (order == null) {
  132 + return null;
  133 + }
  134 + adminScopeGuard.assertCityAccessible(cityId, order.getCityId());
123 return recordMapper.selectOne(new LambdaQueryWrapper<OrderRefundRecord>() 135 return recordMapper.selectOne(new LambdaQueryWrapper<OrderRefundRecord>()
124 .eq(OrderRefundRecord::getOid, orderId) 136 .eq(OrderRefundRecord::getOid, orderId)
125 .orderByDesc(OrderRefundRecord::getId) 137 .orderByDesc(OrderRefundRecord::getId)
src/main/java/com/diligrp/rider/service/impl/RiderEvaluateServiceImpl.java
@@ -2,6 +2,7 @@ package com.diligrp.rider.service.impl; @@ -2,6 +2,7 @@ package com.diligrp.rider.service.impl;
2 2
3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.auth.AdminScopeGuard;
5 import com.diligrp.rider.common.exception.BizException; 6 import com.diligrp.rider.common.exception.BizException;
6 import com.diligrp.rider.entity.Orders; 7 import com.diligrp.rider.entity.Orders;
7 import com.diligrp.rider.entity.RiderEvaluate; 8 import com.diligrp.rider.entity.RiderEvaluate;
@@ -25,6 +26,7 @@ public class RiderEvaluateServiceImpl implements RiderEvaluateService { @@ -25,6 +26,7 @@ public class RiderEvaluateServiceImpl implements RiderEvaluateService {
25 private final RiderEvaluateMapper evaluateMapper; 26 private final RiderEvaluateMapper evaluateMapper;
26 private final OrdersMapper ordersMapper; 27 private final OrdersMapper ordersMapper;
27 private final RiderMapper riderMapper; 28 private final RiderMapper riderMapper;
  29 + private final AdminScopeGuard adminScopeGuard;
28 30
29 private static final int PAGE_SIZE = 20; 31 private static final int PAGE_SIZE = 20;
30 32
@@ -68,7 +70,12 @@ public class RiderEvaluateServiceImpl implements RiderEvaluateService { @@ -68,7 +70,12 @@ public class RiderEvaluateServiceImpl implements RiderEvaluateService {
68 } 70 }
69 71
70 @Override 72 @Override
71 - public List<RiderEvaluate> getRiderEvaluates(Long riderId, Integer type, int page) { 73 + public List<RiderEvaluate> getRiderEvaluates(Long riderId, Integer type, int page, Long cityId) {
  74 + Rider rider = riderMapper.selectById(riderId);
  75 + if (rider == null) {
  76 + throw new BizException("骑手不存在");
  77 + }
  78 + adminScopeGuard.assertCityAccessible(cityId, rider.getCityId());
72 LambdaQueryWrapper<RiderEvaluate> wrapper = new LambdaQueryWrapper<RiderEvaluate>() 79 LambdaQueryWrapper<RiderEvaluate> wrapper = new LambdaQueryWrapper<RiderEvaluate>()
73 .eq(RiderEvaluate::getRid, riderId) 80 .eq(RiderEvaluate::getRid, riderId)
74 .orderByDesc(RiderEvaluate::getId); 81 .orderByDesc(RiderEvaluate::getId);
src/main/java/com/diligrp/rider/service/impl/RoleScopeGuardServiceImpl.java 0 → 100644
  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.SysRole;
  6 +import com.diligrp.rider.mapper.SysRoleMapper;
  7 +import com.diligrp.rider.service.RoleScopeGuardService;
  8 +import lombok.RequiredArgsConstructor;
  9 +import org.springframework.stereotype.Service;
  10 +
  11 +@Service
  12 +@RequiredArgsConstructor
  13 +public class RoleScopeGuardServiceImpl implements RoleScopeGuardService {
  14 +
  15 + private final SysRoleMapper sysRoleMapper;
  16 +
  17 + @Override
  18 + public SysRole requireRole(Long roleId, String requiredScope) {
  19 + if (roleId == null || roleId < 1) {
  20 + throw new BizException("角色不能为空");
  21 + }
  22 + SysRole role = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>()
  23 + .eq(SysRole::getId, roleId)
  24 + .eq(SysRole::getStatus, 1)
  25 + .last("LIMIT 1"));
  26 + if (role == null) {
  27 + throw new BizException("角色不存在或已禁用");
  28 + }
  29 + if (!requiredScope.equals(role.getRoleScope())) {
  30 + throw new BizException("角色归属不匹配");
  31 + }
  32 + return role;
  33 + }
  34 +}
src/main/java/com/diligrp/rider/service/impl/SubstationServiceImpl.java
@@ -2,9 +2,11 @@ package com.diligrp.rider.service.impl; @@ -2,9 +2,11 @@ package com.diligrp.rider.service.impl;
2 2
3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.diligrp.rider.common.enums.AdminRoleScopeEnum;
5 import com.diligrp.rider.common.exception.BizException; 6 import com.diligrp.rider.common.exception.BizException;
6 import com.diligrp.rider.entity.Substation; 7 import com.diligrp.rider.entity.Substation;
7 import com.diligrp.rider.mapper.SubstationMapper; 8 import com.diligrp.rider.mapper.SubstationMapper;
  9 +import com.diligrp.rider.service.RoleScopeGuardService;
8 import com.diligrp.rider.service.SubstationService; 10 import com.diligrp.rider.service.SubstationService;
9 import lombok.RequiredArgsConstructor; 11 import lombok.RequiredArgsConstructor;
10 import org.springframework.stereotype.Service; 12 import org.springframework.stereotype.Service;
@@ -18,6 +20,7 @@ import java.util.List; @@ -18,6 +20,7 @@ import java.util.List;
18 public class SubstationServiceImpl implements SubstationService { 20 public class SubstationServiceImpl implements SubstationService {
19 21
20 private final SubstationMapper substationMapper; 22 private final SubstationMapper substationMapper;
  23 + private final RoleScopeGuardService roleScopeGuardService;
21 24
22 @Override 25 @Override
23 public List<Substation> list(String keyword) { 26 public List<Substation> list(String keyword) {
@@ -41,6 +44,7 @@ public class SubstationServiceImpl implements SubstationService { @@ -41,6 +44,7 @@ public class SubstationServiceImpl implements SubstationService {
41 .eq(Substation::getUserLogin, substation.getUserLogin())); 44 .eq(Substation::getUserLogin, substation.getUserLogin()));
42 if (loginExists > 0) throw new BizException("账号已存在,请更换"); 45 if (loginExists > 0) throw new BizException("账号已存在,请更换");
43 46
  47 + roleScopeGuardService.requireRole(substation.getRoleId(), AdminRoleScopeEnum.SUBSTATION.name());
44 substation.setUserPass(encryptPass(substation.getUserPass())); 48 substation.setUserPass(encryptPass(substation.getUserPass()));
45 substation.setUserStatus(1); 49 substation.setUserStatus(1);
46 substation.setCreateTime(System.currentTimeMillis() / 1000); 50 substation.setCreateTime(System.currentTimeMillis() / 1000);
@@ -51,6 +55,7 @@ public class SubstationServiceImpl implements SubstationService { @@ -51,6 +55,7 @@ public class SubstationServiceImpl implements SubstationService {
51 public void edit(Substation substation) { 55 public void edit(Substation substation) {
52 Substation existing = substationMapper.selectById(substation.getId()); 56 Substation existing = substationMapper.selectById(substation.getId());
53 if (existing == null) throw new BizException("分站管理员不存在"); 57 if (existing == null) throw new BizException("分站管理员不存在");
  58 + roleScopeGuardService.requireRole(substation.getRoleId(), AdminRoleScopeEnum.SUBSTATION.name());
54 // 密码为空则不更新 59 // 密码为空则不更新
55 if (substation.getUserPass() == null || substation.getUserPass().isBlank()) { 60 if (substation.getUserPass() == null || substation.getUserPass().isBlank()) {
56 substation.setUserPass(null); 61 substation.setUserPass(null);
src/main/java/com/diligrp/rider/service/impl/SystemMenuServiceImpl.java 0 → 100644
  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.enums.MenuScopeEnum;
  6 +import com.diligrp.rider.common.exception.BizException;
  7 +import com.diligrp.rider.dto.AdminMenuSaveDTO;
  8 +import com.diligrp.rider.entity.SysMenu;
  9 +import com.diligrp.rider.mapper.SysMenuMapper;
  10 +import com.diligrp.rider.service.SystemMenuService;
  11 +import com.diligrp.rider.vo.AdminRoleMenuTreeVO;
  12 +import lombok.RequiredArgsConstructor;
  13 +import org.springframework.stereotype.Service;
  14 +import org.springframework.transaction.annotation.Transactional;
  15 +import org.springframework.util.StringUtils;
  16 +
  17 +import java.util.ArrayList;
  18 +import java.util.LinkedHashMap;
  19 +import java.util.List;
  20 +import java.util.Map;
  21 +
  22 +@Service
  23 +@RequiredArgsConstructor
  24 +public class SystemMenuServiceImpl implements SystemMenuService {
  25 +
  26 + private final SysMenuMapper sysMenuMapper;
  27 +
  28 + @Override
  29 + public List<AdminRoleMenuTreeVO> tree() {
  30 + List<SysMenu> menus = listAllMenus();
  31 + return buildTree(menus, Map.of(), false);
  32 + }
  33 +
  34 + @Override
  35 + public void add(AdminMenuSaveDTO dto) {
  36 + validateSave(dto, null);
  37 + SysMenu menu = new SysMenu();
  38 + apply(menu, dto);
  39 + menu.setCreateTime(System.currentTimeMillis() / 1000);
  40 + sysMenuMapper.insert(menu);
  41 + }
  42 +
  43 + @Override
  44 + public void edit(AdminMenuSaveDTO dto) {
  45 + if (dto.getId() == null || dto.getId() < 1) {
  46 + throw new BizException("菜单ID不能为空");
  47 + }
  48 + SysMenu existing = requireMenu(dto.getId());
  49 + validateSave(dto, existing);
  50 + apply(existing, dto);
  51 + sysMenuMapper.updateById(existing);
  52 + }
  53 +
  54 + @Override
  55 + @Transactional
  56 + public void delete(Long id) {
  57 + SysMenu menu = requireMenu(id);
  58 + Long childCount = sysMenuMapper.selectCount(new LambdaQueryWrapper<SysMenu>()
  59 + .eq(SysMenu::getParentId, id));
  60 + if (childCount != null && childCount > 0) {
  61 + throw new BizException("请先删除子菜单");
  62 + }
  63 + sysMenuMapper.deleteById(menu.getId());
  64 + }
  65 +
  66 + @Override
  67 + public void setVisible(Long id, int visible) {
  68 + requireMenu(id);
  69 + sysMenuMapper.update(null, new LambdaUpdateWrapper<SysMenu>()
  70 + .eq(SysMenu::getId, id)
  71 + .set(SysMenu::getVisible, visible));
  72 + }
  73 +
  74 + @Override
  75 + public void setStatus(Long id, int status) {
  76 + requireMenu(id);
  77 + sysMenuMapper.update(null, new LambdaUpdateWrapper<SysMenu>()
  78 + .eq(SysMenu::getId, id)
  79 + .set(SysMenu::getStatus, status));
  80 + }
  81 +
  82 + public List<SysMenu> listAllMenus() {
  83 + return sysMenuMapper.selectList(new LambdaQueryWrapper<SysMenu>()
  84 + .orderByAsc(SysMenu::getListOrder)
  85 + .orderByAsc(SysMenu::getId));
  86 + }
  87 +
  88 + public List<AdminRoleMenuTreeVO> buildTree(List<SysMenu> menus, Map<Long, Boolean> checkedMap, boolean disableByScope) {
  89 + Map<Long, AdminRoleMenuTreeVO> voMap = new LinkedHashMap<>();
  90 + List<AdminRoleMenuTreeVO> roots = new ArrayList<>();
  91 + for (SysMenu menu : menus) {
  92 + AdminRoleMenuTreeVO vo = toVO(menu, checkedMap.get(menu.getId()));
  93 + if (disableByScope && checkedMap.get(menu.getId()) == null) {
  94 + vo.setDisabled(true);
  95 + }
  96 + voMap.put(menu.getId(), vo);
  97 + Long parentId = menu.getParentId() == null ? 0L : menu.getParentId();
  98 + if (parentId > 0 && voMap.containsKey(parentId)) {
  99 + voMap.get(parentId).getChildren().add(vo);
  100 + } else {
  101 + roots.add(vo);
  102 + }
  103 + }
  104 + return roots;
  105 + }
  106 +
  107 + private void validateSave(AdminMenuSaveDTO dto, SysMenu existing) {
  108 + validateTypeAndPath(dto);
  109 + validateScope(dto.getMenuScope());
  110 +
  111 + SysMenu duplicate = sysMenuMapper.selectOne(new LambdaQueryWrapper<SysMenu>()
  112 + .eq(SysMenu::getCode, dto.getCode())
  113 + .ne(existing != null, SysMenu::getId, existing != null ? existing.getId() : null)
  114 + .last("LIMIT 1"));
  115 + if (duplicate != null) {
  116 + throw new BizException("菜单编码已存在");
  117 + }
  118 +
  119 + Long parentId = dto.getParentId() == null ? 0L : dto.getParentId();
  120 + if (parentId > 0) {
  121 + SysMenu parent = requireMenu(parentId);
  122 + if (existing != null && existing.getId().equals(parent.getId())) {
  123 + throw new BizException("父级菜单不能选择自己");
  124 + }
  125 + if (existing != null) {
  126 + ensureNotDescendant(parent.getId(), existing.getId());
  127 + }
  128 + }
  129 + }
  130 +
  131 + private void validateTypeAndPath(AdminMenuSaveDTO dto) {
  132 + if (!"DIR".equals(dto.getType()) && !"MENU".equals(dto.getType())) {
  133 + throw new BizException("菜单类型只能是 DIR 或 MENU");
  134 + }
  135 + if ("MENU".equals(dto.getType()) && !StringUtils.hasText(dto.getPath())) {
  136 + throw new BizException("菜单路由不能为空");
  137 + }
  138 + }
  139 +
  140 + private void validateScope(String scope) {
  141 + for (MenuScopeEnum value : MenuScopeEnum.values()) {
  142 + if (value.name().equals(scope)) {
  143 + return;
  144 + }
  145 + }
  146 + throw new BizException("菜单范围不合法");
  147 + }
  148 +
  149 + private void ensureNotDescendant(Long parentId, Long selfId) {
  150 + Long currentId = parentId;
  151 + while (currentId != null && currentId > 0) {
  152 + if (currentId.equals(selfId)) {
  153 + throw new BizException("父级菜单不能选择当前菜单的子节点");
  154 + }
  155 + SysMenu current = sysMenuMapper.selectById(currentId);
  156 + currentId = current == null ? null : current.getParentId();
  157 + }
  158 + }
  159 +
  160 + private SysMenu requireMenu(Long id) {
  161 + SysMenu menu = sysMenuMapper.selectById(id);
  162 + if (menu == null) {
  163 + throw new BizException("菜单不存在");
  164 + }
  165 + return menu;
  166 + }
  167 +
  168 + private void apply(SysMenu menu, AdminMenuSaveDTO dto) {
  169 + menu.setCode(dto.getCode());
  170 + menu.setName(dto.getName());
  171 + menu.setType(dto.getType());
  172 + menu.setPath(StringUtils.hasText(dto.getPath()) ? dto.getPath().trim() : "");
  173 + menu.setIcon(StringUtils.hasText(dto.getIcon()) ? dto.getIcon().trim() : "");
  174 + menu.setParentId(dto.getParentId());
  175 + menu.setMenuScope(dto.getMenuScope());
  176 + menu.setListOrder(dto.getListOrder() == null ? 0 : dto.getListOrder());
  177 + menu.setVisible(dto.getVisible() == null ? 1 : dto.getVisible());
  178 + menu.setStatus(dto.getStatus() == null ? 1 : dto.getStatus());
  179 + }
  180 +
  181 + private AdminRoleMenuTreeVO toVO(SysMenu menu, Boolean checked) {
  182 + AdminRoleMenuTreeVO vo = new AdminRoleMenuTreeVO();
  183 + vo.setId(menu.getId());
  184 + vo.setCode(menu.getCode());
  185 + vo.setName(menu.getName());
  186 + vo.setType(menu.getType());
  187 + vo.setPath(menu.getPath());
  188 + vo.setIcon(menu.getIcon());
  189 + vo.setMenuScope(menu.getMenuScope());
  190 + vo.setVisible(menu.getVisible());
  191 + vo.setStatus(menu.getStatus());
  192 + vo.setListOrder(menu.getListOrder());
  193 + vo.setChecked(Boolean.TRUE.equals(checked));
  194 + return vo;
  195 + }
  196 +}
src/main/java/com/diligrp/rider/service/impl/SystemRoleMenuServiceImpl.java 0 → 100644
  1 +package com.diligrp.rider.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.diligrp.rider.common.enums.AdminRoleScopeEnum;
  5 +import com.diligrp.rider.common.enums.MenuScopeEnum;
  6 +import com.diligrp.rider.common.exception.BizException;
  7 +import com.diligrp.rider.dto.AdminRoleMenuAssignDTO;
  8 +import com.diligrp.rider.entity.SysMenu;
  9 +import com.diligrp.rider.entity.SysRole;
  10 +import com.diligrp.rider.entity.SysRoleMenu;
  11 +import com.diligrp.rider.mapper.SysRoleMapper;
  12 +import com.diligrp.rider.mapper.SysRoleMenuMapper;
  13 +import com.diligrp.rider.service.SystemRoleMenuService;
  14 +import com.diligrp.rider.vo.AdminRoleMenuTreeVO;
  15 +import com.diligrp.rider.vo.AdminRoleVO;
  16 +import lombok.RequiredArgsConstructor;
  17 +import org.springframework.stereotype.Service;
  18 +import org.springframework.transaction.annotation.Transactional;
  19 +
  20 +import java.util.ArrayList;
  21 +import java.util.LinkedHashMap;
  22 +import java.util.List;
  23 +import java.util.Map;
  24 +import java.util.Set;
  25 +import java.util.stream.Collectors;
  26 +
  27 +@Service
  28 +@RequiredArgsConstructor
  29 +public class SystemRoleMenuServiceImpl implements SystemRoleMenuService {
  30 +
  31 + private final SysRoleMapper sysRoleMapper;
  32 + private final SysRoleMenuMapper sysRoleMenuMapper;
  33 + private final SystemMenuServiceImpl systemMenuService;
  34 +
  35 + @Override
  36 + public List<AdminRoleVO> listRoles() {
  37 + List<SysRole> roles = sysRoleMapper.selectList(new LambdaQueryWrapper<SysRole>()
  38 + .eq(SysRole::getStatus, 1)
  39 + .orderByAsc(SysRole::getId));
  40 + List<AdminRoleVO> result = new ArrayList<>();
  41 + for (SysRole role : roles) {
  42 + AdminRoleVO vo = new AdminRoleVO();
  43 + vo.setId(role.getId());
  44 + vo.setCode(role.getCode());
  45 + vo.setName(role.getName());
  46 + vo.setRoleScope(role.getRoleScope());
  47 + result.add(vo);
  48 + }
  49 + return result;
  50 + }
  51 +
  52 + @Override
  53 + public List<AdminRoleMenuTreeVO> getRoleMenuTree(Long roleId) {
  54 + SysRole role = requireRole(roleId);
  55 + List<SysMenu> allowedMenus = filterMenusByScope(systemMenuService.listAllMenus(), role);
  56 + List<SysRoleMenu> assigned = sysRoleMenuMapper.selectList(new LambdaQueryWrapper<SysRoleMenu>()
  57 + .eq(SysRoleMenu::getRoleId, roleId));
  58 + Set<Long> assignedIds = assigned.stream().map(SysRoleMenu::getMenuId).collect(Collectors.toSet());
  59 + Map<Long, Boolean> checkedMap = new LinkedHashMap<>();
  60 + for (SysMenu menu : allowedMenus) {
  61 + checkedMap.put(menu.getId(), assignedIds.contains(menu.getId()));
  62 + }
  63 + return systemMenuService.buildTree(allowedMenus, checkedMap, false);
  64 + }
  65 +
  66 + @Override
  67 + @Transactional
  68 + public void assignMenus(Long roleId, AdminRoleMenuAssignDTO dto) {
  69 + SysRole role = requireRole(roleId);
  70 + List<SysMenu> allowedMenus = filterMenusByScope(systemMenuService.listAllMenus(), role);
  71 + Set<Long> allowedIds = allowedMenus.stream().map(SysMenu::getId).collect(Collectors.toSet());
  72 + List<Long> menuIds = dto.getMenuIds() == null ? List.of() : dto.getMenuIds();
  73 + for (Long menuId : menuIds) {
  74 + if (!allowedIds.contains(menuId)) {
  75 + throw new BizException("存在不允许分配的菜单");
  76 + }
  77 + }
  78 +
  79 + sysRoleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>()
  80 + .eq(SysRoleMenu::getRoleId, roleId));
  81 + long now = System.currentTimeMillis() / 1000;
  82 + for (Long menuId : menuIds) {
  83 + SysRoleMenu roleMenu = new SysRoleMenu();
  84 + roleMenu.setRoleId(roleId);
  85 + roleMenu.setMenuId(menuId);
  86 + roleMenu.setCreateTime(now);
  87 + sysRoleMenuMapper.insert(roleMenu);
  88 + }
  89 + }
  90 +
  91 + private SysRole requireRole(Long roleId) {
  92 + SysRole role = sysRoleMapper.selectById(roleId);
  93 + if (role == null || role.getStatus() == null || role.getStatus() != 1) {
  94 + throw new BizException("角色不存在或已禁用");
  95 + }
  96 + return role;
  97 + }
  98 +
  99 + private List<SysMenu> filterMenusByScope(List<SysMenu> menus, SysRole role) {
  100 + return menus.stream().filter(menu -> isScopeAllowed(role, menu)).collect(Collectors.toList());
  101 + }
  102 +
  103 + private boolean isScopeAllowed(SysRole role, SysMenu menu) {
  104 + if (MenuScopeEnum.BOTH.name().equals(menu.getMenuScope())) {
  105 + return true;
  106 + }
  107 + if (AdminRoleScopeEnum.PLATFORM.name().equals(role.getRoleScope())) {
  108 + return MenuScopeEnum.PLATFORM.name().equals(menu.getMenuScope());
  109 + }
  110 + if (AdminRoleScopeEnum.SUBSTATION.name().equals(role.getRoleScope())) {
  111 + return MenuScopeEnum.SUBSTATION.name().equals(menu.getMenuScope());
  112 + }
  113 + return false;
  114 + }
  115 +}
src/main/java/com/diligrp/rider/vo/AdminLoginUserVO.java 0 → 100644
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class AdminLoginUserVO {
  7 + private Long id;
  8 + private String userLogin;
  9 + private String userNickname;
  10 + /** 兼容前端现有判断:admin / substation */
  11 + private String role;
  12 + private String roleType;
  13 + private String roleCode;
  14 + private Long cityId;
  15 + private String cityName;
  16 +}
src/main/java/com/diligrp/rider/vo/AdminLoginVO.java
@@ -2,14 +2,12 @@ package com.diligrp.rider.vo; @@ -2,14 +2,12 @@ package com.diligrp.rider.vo;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
  5 +import java.util.List;
  6 +
5 @Data 7 @Data
6 public class AdminLoginVO { 8 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; 9 private String token;
  10 + private AdminLoginUserVO user;
  11 + private List<AdminMenuVO> menus;
  12 + private String homePath;
15 } 13 }
src/main/java/com/diligrp/rider/vo/AdminMenuVO.java 0 → 100644
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.ArrayList;
  6 +import java.util.List;
  7 +
  8 +@Data
  9 +public class AdminMenuVO {
  10 + private Long id;
  11 + private String code;
  12 + private String name;
  13 + private String type;
  14 + private String path;
  15 + private String icon;
  16 + private List<AdminMenuVO> children = new ArrayList<>();
  17 +}
src/main/java/com/diligrp/rider/vo/AdminRoleMenuTreeVO.java 0 → 100644
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.ArrayList;
  6 +import java.util.List;
  7 +
  8 +@Data
  9 +public class AdminRoleMenuTreeVO {
  10 + private Long id;
  11 + private String code;
  12 + private String name;
  13 + private String type;
  14 + private String path;
  15 + private String icon;
  16 + private String menuScope;
  17 + private Integer visible;
  18 + private Integer status;
  19 + private Integer listOrder;
  20 + private Boolean checked = false;
  21 + private Boolean disabled = false;
  22 + private List<AdminRoleMenuTreeVO> children = new ArrayList<>();
  23 +}
src/main/java/com/diligrp/rider/vo/AdminRoleVO.java 0 → 100644
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +@Data
  6 +public class AdminRoleVO {
  7 + private Long id;
  8 + private String code;
  9 + private String name;
  10 + private String roleScope;
  11 +}
src/main/resources/data-init.sql
@@ -64,9 +64,43 @@ INSERT INTO `city` (`id`, `pid`, `name`, `area_code`, `status`, `rate`, `list_or @@ -64,9 +64,43 @@ INSERT INTO `city` (`id`, `pid`, `name`, `area_code`, `status`, `rate`, `list_or
64 -- 2. 分站管理员(每个已开通城市一个) 64 -- 2. 分站管理员(每个已开通城市一个)
65 -- 默认密码均为 admin123(MD5: 0192023a7bbd73250516f069df18b500) 65 -- 默认密码均为 admin123(MD5: 0192023a7bbd73250516f069df18b500)
66 -- ============================================================ 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()); 67 +INSERT INTO `sys_role` (`code`, `name`, `role_scope`, `status`, `create_time`) VALUES
  68 +('platform_admin', '平台管理员', 'PLATFORM', 1, UNIX_TIMESTAMP()),
  69 +('substation_admin', '分站管理员', 'SUBSTATION', 1, UNIX_TIMESTAMP());
  70 +
  71 +INSERT INTO `sys_menu` (`code`, `name`, `type`, `path`, `icon`, `parent_id`, `menu_scope`, `list_order`, `visible`, `status`, `create_time`) VALUES
  72 +('dashboard', '工作台', 'MENU', '/dashboard', 'HomeOutlined', 0, 'BOTH', 10, 1, 1, UNIX_TIMESTAMP()),
  73 +('city.manage', '租户管理', 'MENU', '/city', 'GlobalOutlined', 0, 'PLATFORM', 20, 1, 1, UNIX_TIMESTAMP()),
  74 +('substation.manage', '分站管理', 'MENU', '/substation', 'ApartmentOutlined', 0, 'PLATFORM', 30, 1, 1, UNIX_TIMESTAMP()),
  75 +('merchant.root', '商家管理', 'DIR', '', 'ShopOutlined', 0, 'PLATFORM', 40, 1, 1, UNIX_TIMESTAMP()),
  76 +('merchant.enter', '入驻申请', 'MENU', '/merchant/enter', '', 4, 'PLATFORM', 41, 1, 1, UNIX_TIMESTAMP()),
  77 +('merchant.store', '店铺管理', 'MENU', '/merchant/store', '', 4, 'PLATFORM', 42, 1, 1, UNIX_TIMESTAMP()),
  78 +('rider.list', '骑手管理', 'MENU', '/rider', 'UserOutlined', 0, 'BOTH', 50, 1, 1, UNIX_TIMESTAMP()),
  79 +('rider.evaluate', '骑手评价', 'MENU', '/rider/evaluate', 'StarOutlined', 0, 'BOTH', 60, 1, 1, UNIX_TIMESTAMP()),
  80 +('order.root', '订单管理', 'DIR', '', 'UnorderedListOutlined', 0, 'BOTH', 70, 1, 1, UNIX_TIMESTAMP()),
  81 +('order.list', '订单列表', 'MENU', '/order', '', 9, 'BOTH', 71, 1, 1, UNIX_TIMESTAMP()),
  82 +('order.refund', '退款管理', 'MENU', '/refund', '', 9, 'BOTH', 72, 1, 1, UNIX_TIMESTAMP()),
  83 +('order.delivery', '配送订单', 'MENU', '/delivery/order', '', 9, 'BOTH', 73, 1, 1, UNIX_TIMESTAMP()),
  84 +('config.root', '配置中心', 'DIR', '', 'ControlOutlined', 0, 'BOTH', 80, 1, 1, UNIX_TIMESTAMP()),
  85 +('fee.plan', '配送费配置', 'MENU', '/config/fee-plan', '', 13, 'BOTH', 81, 1, 1, UNIX_TIMESTAMP()),
  86 +('dispatch.rule', '调度配置', 'MENU', '/dispatch/rule', '', 13, 'BOTH', 82, 1, 1, UNIX_TIMESTAMP()),
  87 +('open.root', '开放平台', 'DIR', '', 'ApiOutlined', 0, 'PLATFORM', 90, 1, 1, UNIX_TIMESTAMP()),
  88 +('open.app', '应用管理', 'MENU', '/open', '', 16, 'PLATFORM', 91, 1, 1, UNIX_TIMESTAMP()),
  89 +('open.mock_delivery', '模拟推单', 'MENU', '/open/mock-delivery', '', 16, 'PLATFORM', 92, 1, 1, UNIX_TIMESTAMP()),
  90 +('system.root', '系统管理', 'DIR', '', 'ControlOutlined', 0, 'PLATFORM', 100, 1, 1, UNIX_TIMESTAMP()),
  91 +('system.menu', '菜单管理', 'MENU', '/system/menu', '', 19, 'PLATFORM', 101, 1, 1, UNIX_TIMESTAMP()),
  92 +('system.role_menu', '角色菜单', 'MENU', '/system/role-menu', '', 19, 'PLATFORM', 102, 1, 1, UNIX_TIMESTAMP()),
  93 +('admin.user', '平台账号', 'MENU', '/admin-user', '', 19, 'PLATFORM', 103, 1, 1, UNIX_TIMESTAMP());
  94 +
  95 +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `create_time`)
  96 +SELECT 1, id, UNIX_TIMESTAMP() FROM `sys_menu` WHERE `menu_scope` IN ('PLATFORM', 'BOTH');
  97 +
  98 +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `create_time`)
  99 +SELECT 2, id, UNIX_TIMESTAMP() FROM `sys_menu` WHERE `menu_scope` IN ('SUBSTATION', 'BOTH');
  100 +
  101 +INSERT INTO `substation` (`city_id`, `user_login`, `user_nickname`, `user_pass`, `mobile`, `user_status`, `role_id`, `create_time`) VALUES
  102 +(2, 'gz_admin', '广州分站管理员', '0192023a7bbd73250516f069df18b500', '13800000001', 1, 2, UNIX_TIMESTAMP()),
  103 +(3, 'sz_admin', '深圳分站管理员', '0192023a7bbd73250516f069df18b500', '13800000002', 1, 2, UNIX_TIMESTAMP());
70 104
71 -- ============================================================ 105 -- ============================================================
72 -- 3. 骑手等级配置(广州) 106 -- 3. 骑手等级配置(广州)
@@ -135,7 +169,8 @@ SELECT &#39;开放平台 AppKey: TESTAPPKEY00001&#39; AS 开放平台; @@ -135,7 +169,8 @@ SELECT &#39;开放平台 AppKey: TESTAPPKEY00001&#39; AS 开放平台;
135 -- 7. 超级管理员账号 169 -- 7. 超级管理员账号
136 -- 默认密码:admin123(MD5: 0192023a7bbd73250516f069df18b500) 170 -- 默认密码:admin123(MD5: 0192023a7bbd73250516f069df18b500)
137 -- ============================================================ 171 -- ============================================================
138 -INSERT INTO `admin_user` (`user_login`, `user_pass`, `user_nickname`, `user_status`, `create_time`) VALUES  
139 -('admin', '0192023a7bbd73250516f069df18b500', '超级管理员', 1, UNIX_TIMESTAMP()); 172 +INSERT INTO `admin_user` (`user_login`, `user_pass`, `user_nickname`, `user_status`, `role_id`, `create_time`) VALUES
  173 +('admin', '0192023a7bbd73250516f069df18b500', '超级管理员', 1, 1, UNIX_TIMESTAMP());
140 174
141 SELECT '超级管理员: admin / admin123(role=admin)' AS 超管账号; 175 SELECT '超级管理员: admin / admin123(role=admin)' AS 超管账号;
  176 +SELECT '系统管理菜单: /system/menu /system/role-menu /admin-user' AS 系统管理;
src/main/resources/schema.sql
@@ -258,6 +258,7 @@ CREATE TABLE `substation` ( @@ -258,6 +258,7 @@ CREATE TABLE `substation` (
258 `mobile` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号', 258 `mobile` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '手机号',
259 `avatar` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像', 259 `avatar` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像',
260 `user_status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常', 260 `user_status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
  261 + `role_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '菜单角色ID',
261 `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', 262 `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
262 PRIMARY KEY (`id`), 263 PRIMARY KEY (`id`),
263 UNIQUE KEY `uk_user_login` (`user_login`) 264 UNIQUE KEY `uk_user_login` (`user_login`)
@@ -366,11 +367,51 @@ CREATE TABLE `admin_user` ( @@ -366,11 +367,51 @@ CREATE TABLE `admin_user` (
366 `user_pass` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '密码(MD5)', 367 `user_pass` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '密码(MD5)',
367 `user_nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称', 368 `user_nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称',
368 `user_status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常', 369 `user_status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
  370 + `role_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '菜单角色ID',
369 `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0, 371 `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
370 PRIMARY KEY (`id`), 372 PRIMARY KEY (`id`),
371 UNIQUE KEY `uk_user_login` (`user_login`) 373 UNIQUE KEY `uk_user_login` (`user_login`)
372 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='超级管理员表'; 374 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='超级管理员表';
373 375
  376 +CREATE TABLE `sys_role` (
  377 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  378 + `code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '角色编码',
  379 + `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '角色名称',
  380 + `role_scope` VARCHAR(32) NOT NULL DEFAULT 'PLATFORM' COMMENT '角色归属:PLATFORM/SUBSTATION',
  381 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
  382 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  383 + PRIMARY KEY (`id`),
  384 + UNIQUE KEY `uk_role_code` (`code`)
  385 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台菜单角色表';
  386 +
  387 +CREATE TABLE `sys_menu` (
  388 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  389 + `code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '菜单编码',
  390 + `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '菜单名称',
  391 + `type` VARCHAR(16) NOT NULL DEFAULT 'MENU' COMMENT '类型:DIR/MENU',
  392 + `path` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '前端路由路径',
  393 + `icon` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '前端图标名',
  394 + `parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父级菜单ID',
  395 + `menu_scope` VARCHAR(32) NOT NULL DEFAULT 'BOTH' COMMENT '菜单归属:PLATFORM/SUBSTATION/BOTH',
  396 + `list_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
  397 + `visible` TINYINT NOT NULL DEFAULT 1 COMMENT '是否显示:0=否 1=是',
  398 + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=正常',
  399 + KEY `idx_scope_parent` (`menu_scope`, `parent_id`, `list_order`),
  400 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  401 + PRIMARY KEY (`id`),
  402 + UNIQUE KEY `uk_menu_code` (`code`),
  403 + KEY `idx_parent_order` (`parent_id`, `list_order`)
  404 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台菜单表';
  405 +
  406 +CREATE TABLE `sys_role_menu` (
  407 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  408 + `role_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '角色ID',
  409 + `menu_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '菜单ID',
  410 + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0,
  411 + PRIMARY KEY (`id`),
  412 + UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`)
  413 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台角色菜单关系表';
  414 +
374 -- orders 表补充字段(如已有 orders 表,执行以下 ALTER) 415 -- orders 表补充字段(如已有 orders 表,执行以下 ALTER)
375 ALTER TABLE `orders` ADD COLUMN `out_order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '外部系统订单号' AFTER `order_no`; 416 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`; 417 ALTER TABLE `orders` ADD COLUMN `app_key` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '接入方AppKey' AFTER `out_order_no`;