Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/sit-OrderFood' into sit-OrderFood

刘云鑫 2 mesiacov pred
rodič
commit
fefd4afe4d
43 zmenil súbory, kde vykonal 1630 pridanie a 898 odobranie
  1. 15 74
      alien-api/src/main/resources/logback-spring.xml
  2. 15 10
      alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java
  3. 17 0
      alien-dining/src/main/java/shop/alien/dining/controller/StoreInfoController.java
  4. 93 0
      alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java
  5. 8 0
      alien-dining/src/main/java/shop/alien/dining/service/CartService.java
  6. 9 0
      alien-dining/src/main/java/shop/alien/dining/service/StoreInfoService.java
  7. 17 0
      alien-dining/src/main/java/shop/alien/dining/service/StoreOrderService.java
  8. 269 71
      alien-dining/src/main/java/shop/alien/dining/service/impl/CartServiceImpl.java
  9. 38 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreInfoServiceImpl.java
  10. 166 0
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java
  11. 4 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCart.java
  12. 4 0
      alien-entity/src/main/java/shop/alien/entity/store/StorePlatformUserRole.java
  13. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/CartItemDTO.java
  14. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreInfoWithHomepageCuisinesDTO.java
  15. 0 4
      alien-entity/src/main/java/shop/alien/entity/store/excelVo/StoreUserExcelVo.java
  16. 6 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackListVo.java
  17. 78 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/OrderInfoVO.java
  18. 6 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/PerformerVo.java
  19. 7 3
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreSubExcelVo.java
  20. 4 1
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreUserVo.java
  21. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/SubAccountStoreListVo.java
  22. 4 0
      alien-entity/src/main/java/shop/alien/entity/storePlatform/StoreOperationalActivity.java
  23. 3 0
      alien-entity/src/main/java/shop/alien/entity/storePlatform/vo/StoreOperationalActivityVO.java
  24. 3 0
      alien-entity/src/main/resources/mapper/LifeFeedbackMapper.xml
  25. 9 5
      alien-entity/src/main/resources/mapper/SubAccountStoreMapper.xml
  26. 6 1
      alien-gateway/src/main/java/shop/alien/gateway/service/impl/StoreUserServiceImpl.java
  27. 16 78
      alien-gateway/src/main/resources/logback-spring.xml
  28. 145 0
      alien-job/src/main/java/shop/alien/job/store/StoreOperationalActivityJob.java
  29. 14 73
      alien-job/src/main/resources/logback-spring.xml
  30. 15 74
      alien-lawyer/src/main/resources/logback-spring.xml
  31. 15 75
      alien-second/src/main/resources/logback-spring.xml
  32. 15 75
      alien-store-platform/src/main/resources/logback-spring.xml
  33. 9 5
      alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java
  34. 26 26
      alien-store/src/main/java/shop/alien/store/controller/StoreUserController.java
  35. 1 1
      alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java
  36. 9 0
      alien-store/src/main/java/shop/alien/store/service/LifeDiscountCouponStoreFriendService.java
  37. 5 9
      alien-store/src/main/java/shop/alien/store/service/StoreUserService.java
  38. 1 0
      alien-store/src/main/java/shop/alien/store/service/impl/BarPerformanceServiceImpl.java
  39. 18 1
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  40. 57 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponStoreFriendServiceImpl.java
  41. 457 236
      alien-store/src/main/java/shop/alien/store/service/impl/StoreUserServiceImpl.java
  42. 2 1
      alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java
  43. 12 75
      alien-store/src/main/resources/logback-spring.xml

+ 15 - 74
alien-api/src/main/resources/logback-spring.xml

@@ -1,40 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
-<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
-<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
-<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
-<!-- 该信息是由于设置了当配置文件变化时重新加载,所以每当达到扫描时间的时候就会检查配置文件是否错误。但是由于一般配置文件都放在了JAR包中,
-    而扫描的时候无法扫描JAR包内,因此会提示没有可以检查的文件,所以每隔一段时间就输出一次-->
-<configuration scan="false" scanPeriod="60 seconds" debug="true">
-    <contextName>logback-spring</contextName>
+<configuration scan="false" scanPeriod="60 seconds" debug="false">
 
-    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
     <!-- 定义全局参数常量 -->
     <property name="log.level" value="debug"/>
-    <property name="log.maxHistory" value="30"/><!-- 30表示30 -->
+    <property name="log.maxHistory" value="30"/><!-- 30表示30天 -->
     <springProperty scope="context" name="logging.path" source="logging.path" defaultValue="C:/project/ext/log"/>
     <!--输出文件前缀-->
-    <property name="FILENAME" value="xiaokuihua_api"/>
-
-    <!--0. 日志格式和颜色渲染 -->
-    <!-- 彩色日志依赖的渲染类 -->
-    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
-    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
-    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
+    <property name="FILENAME" value="alien"/>
 
     <!-- 文件输出格式 -->
     <property name="FILE_LOG_PATTERN" value="[%d{MM/dd HH:mm:ss.SSS}][%-10.10thread][%-5level][%-40.40c{1}:%5line]:[%15method] || %m%n"/>
-    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+    
+    <!-- 控制台输出格式:纯文本,无颜色,适合 Docker/EFK -->
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n"/>
 
     <!--1. 输出到控制台-->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <!-- 【关键】控制台只输出 INFO 及以上,防止 SQL 刷屏 ES -->
         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-            <level>${log.level}</level>
+            <level>INFO</level>
         </filter>
         <encoder>
             <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
-            <!-- 设置字符集 -->
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -42,27 +30,17 @@
     <!--2. 输出到文档-->
     <!-- DEBUG 日志 -->
     <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 当前的日志文件存放路径 -->
         <file>${logging.path}/DEBUG.log</file>
-        <!-- 日志滚动策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 历史日志文件的存放路径和名称 -->
             <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
-            <!-- 日志文件最大的保存历史 数量-->
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
             <pattern>${FILE_LOG_PATTERN}</pattern>
         </encoder>
-        <!--日志文件最大的大小-->
-        <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
-        <!--            <MaxFileSize>10MB</MaxFileSize>-->
-        <!--        </triggeringPolicy>-->
-        <!-- 此日志文档只记录debug级别的 -->
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>DEBUG</level>
-            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMatch>ACCEPT</onMatch>
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
@@ -117,41 +95,14 @@
         </filter>
     </appender>
 
-    <!--
-      <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
-      以及指定<appender>。<logger>仅有一个name属性,
-      一个可选的level和一个可选的addtivity属性。
-      name:用来指定受此logger约束的某一个包或者具体的某一个类。
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-         还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
-         如果未设置此属性,那么当前logger将会继承上级的级别。
-      addtivity:是否向上级logger传递打印信息。默认是true。
-      <logger name="org.springframework.web" level="info"/>
-      <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-    -->
-
-    <!--
-      使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
-      第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
-      第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
-      【logging.level.org.mybatis=debug logging.level.dao=debug】
-     -->
-    <!-- mybatis显示sql,修改此处扫描包名 -->
-
-
-    <!--
-      root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-      不能设置为INHERITED或者同义词NULL。默认是DEBUG
-      可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-    -->
+    <!-- 降噪配置 -->
+    <logger name="springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator" level="WARN"/>
+    <logger name="org.springframework.security.web.DefaultSecurityFilterChain " level="WARN"/>
+    <logger name="com.netflix.config.sources.URLConfigurationSource " level="WARN"/>
+    <logger name="com.netflix.discovery" level="WARN"/>
+    <logger name="org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]" level="WARN"/>
 
     <!-- 4. 最终的策略 -->
-    <!-- 4.1 开发环境:打印控制台-->
-    <!--打印sql-->
-    <!--    <logger name="com.veryhappy.music.dao" level="debug"/>-->
-
-    <!--打印log-->
     <root level="info">
         <appender-ref ref="CONSOLE"/>
         <appender-ref ref="DEBUG_FILE"/>
@@ -160,14 +111,4 @@
         <appender-ref ref="ERROR_FILE"/>
     </root>
 
-    <!--   4.2 生产环境:输出到文档-->
-    <springProfile name="pro">
-        <root level="info">
-            <appender-ref ref="CONSOLE"/>
-            <appender-ref ref="DEBUG_FILE"/>
-            <appender-ref ref="INFO_FILE"/>
-            <appender-ref ref="ERROR_FILE"/>
-            <appender-ref ref="WARN_FILE"/>
-        </root>
-    </springProfile>
 </configuration>

+ 15 - 10
alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java

@@ -34,8 +34,9 @@ public class DiningController {
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "就餐人数", required = true) @RequestParam Integer dinerCount) {
         try {
-            // 验证 token
-            if (!TokenUtil.hasValidToken()) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
             DiningPageInfoVO vo = diningService.getDiningPageInfo(tableId, dinerCount);
@@ -53,8 +54,9 @@ public class DiningController {
             @ApiParam(value = "搜索关键词", required = false) @RequestParam(required = false) String keyword,
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
-            // 验证 token
-            if (!TokenUtil.hasValidToken()) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
             // 限制关键词长度
@@ -78,8 +80,9 @@ public class DiningController {
             @ApiParam(value = "页码", required = false) @RequestParam(defaultValue = "1") Integer page,
             @ApiParam(value = "每页数量", required = false) @RequestParam(defaultValue = "12") Integer size) {
         try {
-            // 验证 token
-            if (!TokenUtil.hasValidToken()) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
             List<CuisineListVO> list = diningService.getCuisinesByCategory(storeId, categoryId, tableId, page, size);
@@ -96,8 +99,9 @@ public class DiningController {
             @ApiParam(value = "菜品ID", required = true) @PathVariable Integer cuisineId,
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
-            // 验证 token
-            if (!TokenUtil.hasValidToken()) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
             CuisineDetailVO vo = diningService.getCuisineDetail(cuisineId, tableId);
@@ -207,8 +211,9 @@ public class DiningController {
     public R<Integer> checkOrderLock(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
-            // 验证 token
-            if (!TokenUtil.hasValidToken()) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
             Integer lockUserId = diningService.checkOrderLock(tableId);

+ 17 - 0
alien-dining/src/main/java/shop/alien/dining/controller/StoreInfoController.java

@@ -8,6 +8,7 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreCuisine;
 import shop.alien.entity.store.StoreCuisineCategory;
 import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
 import shop.alien.dining.service.StoreInfoService;
 
 import java.util.List;
@@ -75,4 +76,20 @@ public class StoreInfoController {
             return R.fail("查询菜品信息列表失败: " + e.getMessage());
         }
     }
+
+    @ApiOperation(value = "根据商铺ID查询店铺信息和首页展示美食价目表", notes = "查询店铺信息和当前店铺绑定的首页展示美食价目表信息")
+    @GetMapping("/detail/{storeId}")
+    public R<StoreInfoWithHomepageCuisinesDTO> getStoreInfoWithHomepageCuisines(
+            @ApiParam(value = "商铺ID", required = true) @PathVariable Integer storeId) {
+        try {
+            if (storeId == null) {
+                return R.fail("商铺ID不能为空");
+            }
+            StoreInfoWithHomepageCuisinesDTO result = storeInfoService.getStoreInfoWithHomepageCuisines(storeId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("查询店铺信息和首页展示美食价目表失败: {}", e.getMessage(), e);
+            return R.fail("查询店铺信息和首页展示美食价目表失败: " + e.getMessage());
+        }
+    }
 }

+ 93 - 0
alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java

@@ -20,6 +20,7 @@ import shop.alien.entity.store.dto.AddCartItemDTO;
 import shop.alien.entity.store.dto.CartDTO;
 import shop.alien.entity.store.dto.ChangeTableDTO;
 import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderInfoVO;
 import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.mapper.StoreOrderDetailMapper;
 import shop.alien.mapper.StoreTableLogMapper;
@@ -131,6 +132,45 @@ public class StoreOrderController {
         }
     }
 
+    @ApiOperation(value = "清空购物车", notes = "清空购物车中所有商品,并推送SSE和WebSocket消息")
+    @DeleteMapping("/cart/clear")
+    public R<CartDTO> clearCart(
+            @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
+        try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
+            
+            // 清空购物车(会自动保留已下单的商品)
+            cartService.clearCart(tableId);
+            
+            // 创建空的购物车对象用于返回和推送
+            CartDTO emptyCart = new CartDTO();
+            emptyCart.setTableId(tableId);
+            emptyCart.setItems(java.util.Collections.emptyList());
+            emptyCart.setTotalAmount(java.math.BigDecimal.ZERO);
+            emptyCart.setTotalQuantity(0);
+            
+            // 查询桌号信息
+            StoreTable table = storeTableMapper.selectById(tableId);
+            if (table != null) {
+                emptyCart.setTableNumber(table.getTableNumber());
+                emptyCart.setStoreId(table.getStoreId());
+            }
+            
+            // 推送购物车更新消息(SSE)
+            sseService.pushCartUpdate(tableId, emptyCart);
+            // 推送购物车更新消息(WebSocket)
+//            CartWebSocketProcess.pushCartUpdate(tableId, emptyCart);
+            
+            return R.data(emptyCart);
+        } catch (Exception e) {
+            log.error("清空购物车失败: {}", e.getMessage(), e);
+            return R.fail("清空购物车失败: " + e.getMessage());
+        }
+    }
+
     @ApiOperation(value = "设置用餐人数", notes = "设置用餐人数,自动添加或更新餐具到购物车")
     @PostMapping("/cart/set-diner-count")
     public R<CartDTO> setDinerCount(
@@ -338,6 +378,23 @@ public class StoreOrderController {
         }
     }
 
+    @ApiOperation(value = "查询订单信息", notes = "根据订单ID查询订单完整信息(包含订单基本信息、菜品清单、价格明细)")
+    @GetMapping("/info/{orderId}")
+    public R<OrderInfoVO> getOrderInfo(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
+        try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            OrderInfoVO orderInfo = orderService.getOrderInfo(orderId);
+            return R.data(orderInfo);
+        } catch (Exception e) {
+            log.error("查询订单信息失败: {}", e.getMessage(), e);
+            return R.fail("查询订单信息失败: " + e.getMessage());
+        }
+    }
+
     @ApiOperation(value = "分页查询订单列表", notes = "分页查询订单列表")
     @GetMapping("/page")
     public R<IPage<StoreOrder>> getOrderPage(
@@ -443,4 +500,40 @@ public class StoreOrderController {
             return R.fail("更新订单优惠券失败: " + e.getMessage());
         }
     }
+
+    @ApiOperation(value = "管理员重置餐桌", notes = "管理员重置餐桌:删除购物车数据、订单数据,并重置餐桌表初始化")
+    @PostMapping("/admin/reset-table/{tableId}")
+    public R<Boolean> resetTable(@ApiParam(value = "餐桌ID", required = true) @PathVariable Integer tableId) {
+        try {
+            // TODO: 这里可以添加管理员权限验证
+            // if (!isAdmin(userId)) {
+            //     return R.fail("无权限执行此操作");
+            // }
+            
+            boolean result = orderService.resetTable(tableId);
+            if (result) {
+                // 推送购物车更新消息(清空购物车)
+                CartDTO emptyCart = new CartDTO();
+                emptyCart.setTableId(tableId);
+                emptyCart.setItems(java.util.Collections.emptyList());
+                emptyCart.setTotalAmount(java.math.BigDecimal.ZERO);
+                emptyCart.setTotalQuantity(0);
+                
+                // 查询桌号信息
+                StoreTable table = storeTableMapper.selectById(tableId);
+                if (table != null) {
+                    emptyCart.setTableNumber(table.getTableNumber());
+                    emptyCart.setStoreId(table.getStoreId());
+                }
+                
+                sseService.pushCartUpdate(tableId, emptyCart);
+                return R.data(true);
+            } else {
+                return R.fail("重置餐桌失败");
+            }
+        } catch (Exception e) {
+            log.error("重置餐桌失败: {}", e.getMessage(), e);
+            return R.fail("重置餐桌失败: " + e.getMessage());
+        }
+    }
 }

+ 8 - 0
alien-dining/src/main/java/shop/alien/dining/service/CartService.java

@@ -102,4 +102,12 @@ public interface CartService {
      * @return 购物车信息
      */
     CartDTO updateTablewareQuantity(Integer tableId, Integer quantity);
+
+    /**
+     * 锁定购物车商品数量(下单时调用,将当前数量锁定,不允许减少或删除)
+     *
+     * @param tableId 桌号ID
+     * @return 购物车信息
+     */
+    CartDTO lockCartItems(Integer tableId);
 }

+ 9 - 0
alien-dining/src/main/java/shop/alien/dining/service/StoreInfoService.java

@@ -3,6 +3,7 @@ package shop.alien.dining.service;
 import shop.alien.entity.store.StoreCuisine;
 import shop.alien.entity.store.StoreCuisineCategory;
 import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
 
 import java.util.List;
 
@@ -37,4 +38,12 @@ public interface StoreInfoService {
      * @return 菜品信息列表
      */
     List<StoreCuisine> getCuisinesByCategoryId(Integer categoryId);
+
+    /**
+     * 根据商铺ID查询店铺信息和首页展示美食价目表信息
+     *
+     * @param storeId 商铺ID
+     * @return 店铺信息和首页展示美食价目表信息
+     */
+    StoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId);
 }

+ 17 - 0
alien-dining/src/main/java/shop/alien/dining/service/StoreOrderService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.store.StoreOrder;
 import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderInfoVO;
 
 /**
  * 订单服务接口
@@ -93,4 +94,20 @@ public interface StoreOrderService extends IService<StoreOrder> {
      * @return 订单信息
      */
     StoreOrder updateOrderCoupon(Integer orderId, Integer couponId);
+
+    /**
+     * 管理员重置餐桌(删除购物车数据、订单数据,并重置餐桌表)
+     *
+     * @param tableId 餐桌ID
+     * @return 是否成功
+     */
+    boolean resetTable(Integer tableId);
+
+    /**
+     * 查询订单信息(包含订单基本信息、菜品清单、价格明细)
+     *
+     * @param orderId 订单ID
+     * @return 订单信息
+     */
+    OrderInfoVO getOrderInfo(Integer orderId);
 }

+ 269 - 71
alien-dining/src/main/java/shop/alien/dining/service/impl/CartServiceImpl.java

@@ -28,6 +28,9 @@ import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
 /**
@@ -45,6 +48,13 @@ public class CartServiceImpl implements CartService {
     private static final String COUPON_USED_KEY_PREFIX = "coupon:used:table:";
     private static final int CART_EXPIRE_SECONDS = 24 * 60 * 60; // 24小时过期
 
+    // 异步写入数据库的线程池(专门用于购物车数据库写入)
+    private static final ExecutorService CART_DB_WRITE_EXECUTOR = Executors.newFixedThreadPool(5, r -> {
+        Thread t = new Thread(r, "cart-db-write-" + System.currentTimeMillis());
+        t.setDaemon(true);
+        return t;
+    });
+
     private final BaseRedisService baseRedisService;
     private final StoreTableMapper storeTableMapper;
     private final StoreCuisineMapper storeCuisineMapper;
@@ -134,6 +144,7 @@ public class CartServiceImpl implements CartService {
                 item.setCuisineImage(cartItem.getCuisineImage());
                 item.setUnitPrice(cartItem.getUnitPrice());
                 item.setQuantity(cartItem.getQuantity());
+                item.setLockedQuantity(cartItem.getLockedQuantity());
                 item.setSubtotalAmount(cartItem.getSubtotalAmount());
                 item.setAddUserId(cartItem.getAddUserId());
                 item.setAddUserPhone(cartItem.getAddUserPhone());
@@ -191,24 +202,38 @@ public class CartServiceImpl implements CartService {
                 .orElse(null);
 
         if (existingItem != null) {
-            // 商品已存在,不允许重复添加
-            throw new RuntimeException("已添加过本商品");
-        }
-
-        // 添加新商品
-        CartItemDTO newItem = new CartItemDTO();
-        newItem.setCuisineId(cuisine.getId());
-        newItem.setCuisineName(cuisine.getName());
-        newItem.setCuisineType(cuisine.getCuisineType());
-        newItem.setCuisineImage(cuisine.getImages());
-        newItem.setUnitPrice(cuisine.getTotalPrice());
-        newItem.setQuantity(dto.getQuantity());
-        newItem.setSubtotalAmount(cuisine.getTotalPrice()
-                .multiply(BigDecimal.valueOf(dto.getQuantity())));
-        newItem.setAddUserId(userId);
-        newItem.setAddUserPhone(userPhone);
-        newItem.setRemark(dto.getRemark());
-        items.add(newItem);
+            // 商品已存在
+            Integer lockedQuantity = existingItem.getLockedQuantity();
+            if (lockedQuantity != null && lockedQuantity > 0) {
+                // 如果商品有已下单数量,将新数量叠加到当前数量和已下单数量上
+                Integer newQuantity = existingItem.getQuantity() + dto.getQuantity();
+                Integer newLockedQuantity = lockedQuantity + dto.getQuantity();
+                existingItem.setQuantity(newQuantity);
+                existingItem.setLockedQuantity(newLockedQuantity);
+                existingItem.setSubtotalAmount(existingItem.getUnitPrice()
+                        .multiply(BigDecimal.valueOf(newQuantity)));
+                log.info("商品已存在且有已下单数量,叠加数量, cuisineId={}, oldQuantity={}, newQuantity={}, oldOrderedQuantity={}, newOrderedQuantity={}", 
+                        dto.getCuisineId(), existingItem.getQuantity() - dto.getQuantity(), newQuantity, lockedQuantity, newLockedQuantity);
+            } else {
+                // 商品已存在但没有已下单数量,不允许重复添加
+                throw new RuntimeException("已添加过本商品");
+            }
+        } else {
+            // 添加新商品
+            CartItemDTO newItem = new CartItemDTO();
+            newItem.setCuisineId(cuisine.getId());
+            newItem.setCuisineName(cuisine.getName());
+            newItem.setCuisineType(cuisine.getCuisineType());
+            newItem.setCuisineImage(cuisine.getImages());
+            newItem.setUnitPrice(cuisine.getTotalPrice());
+            newItem.setQuantity(dto.getQuantity());
+            newItem.setSubtotalAmount(cuisine.getTotalPrice()
+                    .multiply(BigDecimal.valueOf(dto.getQuantity())));
+            newItem.setAddUserId(userId);
+            newItem.setAddUserPhone(userPhone);
+            newItem.setRemark(dto.getRemark());
+            items.add(newItem);
+        }
 
         // 重新计算总金额和总数量
         BigDecimal totalAmount = items.stream()
@@ -244,6 +269,14 @@ public class CartServiceImpl implements CartService {
                 .orElse(null);
 
         if (item != null) {
+            // 检查已下单数量:不允许将数量减少到小于已下单数量
+            Integer lockedQuantity = item.getLockedQuantity();
+            if (lockedQuantity != null && lockedQuantity > 0) {
+                if (quantity < lockedQuantity) {
+                    throw new RuntimeException("商品数量不能少于已下单数量(" + lockedQuantity + "),该数量已下单");
+                }
+            }
+            
             item.setQuantity(quantity);
             item.setSubtotalAmount(item.getUnitPrice()
                     .multiply(BigDecimal.valueOf(quantity)));
@@ -269,7 +302,21 @@ public class CartServiceImpl implements CartService {
         log.info("删除购物车商品, tableId={}, cuisineId={}", tableId, cuisineId);
         CartDTO cart = getCart(tableId);
         List<CartItemDTO> items = cart.getItems();
-        items.removeIf(item -> item.getCuisineId().equals(cuisineId));
+        
+        // 检查是否有已下单数量,如果有则不允许删除
+        CartItemDTO item = items.stream()
+                .filter(i -> i.getCuisineId().equals(cuisineId))
+                .findFirst()
+                .orElse(null);
+        
+        if (item != null) {
+            Integer lockedQuantity = item.getLockedQuantity();
+            if (lockedQuantity != null && lockedQuantity > 0) {
+                throw new RuntimeException("商品已下单,已下单数量为 " + lockedQuantity + ",不允许删除");
+            }
+        }
+        
+        items.removeIf(i -> i.getCuisineId().equals(cuisineId));
 
         // 重新计算总金额和总数量
         BigDecimal totalAmount = items.stream()
@@ -287,30 +334,107 @@ public class CartServiceImpl implements CartService {
 
     @Override
     public void clearCart(Integer tableId) {
-        log.info("清空购物车, tableId={}", tableId);
-        // 清空Redis
-        String cartKey = CART_KEY_PREFIX + tableId;
-        baseRedisService.delete(cartKey);
-
-        // 清空数据库(逻辑删除)
-        LambdaQueryWrapper<StoreCart> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(StoreCart::getTableId, tableId);
-        wrapper.eq(StoreCart::getDeleteFlag, 0);
-        List<StoreCart> cartList = storeCartMapper.selectList(wrapper);
-        if (cartList != null && !cartList.isEmpty()) {
-            for (StoreCart cart : cartList) {
-                cart.setDeleteFlag(1);
-                cart.setUpdatedTime(new Date());
-                storeCartMapper.updateById(cart);
+        log.info("清空购物车(保留已下单商品), tableId={}", tableId);
+        
+        // 获取购物车
+        CartDTO cart = getCart(tableId);
+        List<CartItemDTO> items = cart.getItems();
+        
+        if (items == null || items.isEmpty()) {
+            log.info("购物车为空,无需清空, tableId={}", tableId);
+            return;
+        }
+        
+        // 分离已下单的商品和未下单的商品
+        List<CartItemDTO> orderedItems = new ArrayList<>(); // 已下单的商品(保留,数量恢复为已下单数量)
+        List<Integer> orderedCuisineIds = new ArrayList<>(); // 已下单的商品ID列表
+        List<CartItemDTO> unorderedItems = new ArrayList<>(); // 未下单的商品(删除)
+        boolean hasChanges = false; // 是否有变化(需要更新)
+        
+        for (CartItemDTO item : items) {
+            Integer lockedQuantity = item.getLockedQuantity();
+            if (lockedQuantity != null && lockedQuantity > 0) {
+                // 有已下单数量,保留该商品,但将当前数量恢复为已下单数量
+                Integer currentQuantity = item.getQuantity();
+                if (currentQuantity != null && !currentQuantity.equals(lockedQuantity)) {
+                    // 当前数量不等于已下单数量,需要恢复
+                    item.setQuantity(lockedQuantity);
+                    item.setSubtotalAmount(item.getUnitPrice().multiply(BigDecimal.valueOf(lockedQuantity)));
+                    hasChanges = true;
+                    log.info("恢复已下单商品数量, cuisineId={}, oldQuantity={}, orderedQuantity={}", 
+                            item.getCuisineId(), currentQuantity, lockedQuantity);
+                }
+                orderedItems.add(item);
+                orderedCuisineIds.add(item.getCuisineId());
+            } else {
+                // 没有已下单数量,标记为删除
+                unorderedItems.add(item);
+                hasChanges = true;
             }
         }
-
-        // 更新桌号表的购物车统计
-        StoreTable table = storeTableMapper.selectById(tableId);
-        if (table != null) {
-            table.setCartItemCount(0);
-            table.setCartTotalAmount(BigDecimal.ZERO);
-            storeTableMapper.updateById(table);
+        
+        // 如果有变化(有未下单的商品需要删除,或者已下单商品数量需要恢复),进行更新
+        if (hasChanges) {
+            // 1. 更新购物车(删除未下单商品,已下单商品数量已恢复)
+            cart.setItems(orderedItems);
+            // 重新计算总金额和总数量(只计算保留的商品,数量已恢复为已下单数量)
+            BigDecimal totalAmount = orderedItems.stream()
+                    .map(CartItemDTO::getSubtotalAmount)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            Integer totalQuantity = orderedItems.stream()
+                    .mapToInt(CartItemDTO::getQuantity)
+                    .sum();
+            cart.setTotalAmount(totalAmount);
+            cart.setTotalQuantity(totalQuantity);
+            
+            // 更新Redis(保留已下单的商品,数量已恢复)
+            if (orderedItems.isEmpty()) {
+                // 如果所有商品都未下单,清空Redis
+                String cartKey = CART_KEY_PREFIX + tableId;
+                baseRedisService.delete(cartKey);
+            } else {
+                // 保存更新后的购物车到Redis(已下单商品数量已恢复为已下单数量)
+                saveCartToRedis(cart);
+            }
+            
+            // 2. 从数据库中逻辑删除未下单的商品
+            if (!unorderedItems.isEmpty()) {
+                LambdaQueryWrapper<StoreCart> wrapper = new LambdaQueryWrapper<>();
+                wrapper.eq(StoreCart::getTableId, tableId);
+                wrapper.eq(StoreCart::getDeleteFlag, 0);
+                if (!orderedCuisineIds.isEmpty()) {
+                    // 排除已下单的商品ID
+                    wrapper.notIn(StoreCart::getCuisineId, orderedCuisineIds);
+                }
+                List<StoreCart> cartListToDelete = storeCartMapper.selectList(wrapper);
+                if (cartListToDelete != null && !cartListToDelete.isEmpty()) {
+                    List<Integer> cartIds = cartListToDelete.stream()
+                            .map(StoreCart::getId)
+                            .collect(Collectors.toList());
+                    // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+                    storeCartMapper.deleteBatchIds(cartIds);
+                    log.info("删除未下单商品, tableId={}, count={}", tableId, cartIds.size());
+                }
+            }
+            
+            // 3. 更新数据库中已下单商品的数量(恢复为已下单数量)
+            if (!orderedItems.isEmpty()) {
+                // 保存更新后的购物车到数据库(会更新已下单商品的数量)
+                saveCartToDatabase(cart);
+            }
+            
+            // 4. 更新桌号表的购物车统计
+            StoreTable table = storeTableMapper.selectById(tableId);
+            if (table != null) {
+                table.setCartItemCount(totalQuantity);
+                table.setCartTotalAmount(totalAmount);
+                storeTableMapper.updateById(table);
+            }
+            
+            log.info("清空购物车完成(保留已下单商品,数量恢复为已下单数量), tableId={}, 删除商品数={}, 保留商品数={}", 
+                    tableId, unorderedItems.size(), orderedItems.size());
+        } else {
+            log.info("购物车无需更新, tableId={}", tableId);
         }
     }
 
@@ -446,18 +570,18 @@ public class CartServiceImpl implements CartService {
         String couponUsedKey = COUPON_USED_KEY_PREFIX + tableId;
         baseRedisService.delete(couponUsedKey);
 
-        // 更新数据库(逻辑删除未下单的记录)
+        // 更新数据库(逻辑删除未下单的记录,使用 MyBatis-Plus 的 deleteBatchIds
         LambdaQueryWrapper<StoreCouponUsage> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(StoreCouponUsage::getTableId, tableId);
         wrapper.eq(StoreCouponUsage::getDeleteFlag, 0);
         wrapper.eq(StoreCouponUsage::getUsageStatus, 0); // 只删除已标记使用但未下单的
         List<StoreCouponUsage> usageList = storeCouponUsageMapper.selectList(wrapper);
         if (usageList != null && !usageList.isEmpty()) {
-            for (StoreCouponUsage usage : usageList) {
-                usage.setDeleteFlag(1);
-                usage.setUpdatedTime(new Date());
-                storeCouponUsageMapper.updateById(usage);
-            }
+            List<Integer> usageIds = usageList.stream()
+                    .map(StoreCouponUsage::getId)
+                    .collect(Collectors.toList());
+            // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+            storeCouponUsageMapper.deleteBatchIds(usageIds);
         }
 
         // 更新桌号表的优惠券ID
@@ -640,15 +764,66 @@ public class CartServiceImpl implements CartService {
         return cart;
     }
 
+    @Override
+    public CartDTO lockCartItems(Integer tableId) {
+        log.info("锁定购物车商品数量(设置已下单数量), tableId={}", tableId);
+        
+        // 获取购物车
+        CartDTO cart = getCart(tableId);
+        List<CartItemDTO> items = cart.getItems();
+        
+        if (items == null || items.isEmpty()) {
+            log.warn("购物车为空,无需锁定, tableId={}", tableId);
+            return cart;
+        }
+        
+        // 遍历所有商品,将当前数量设置为已下单数量
+        boolean hasChanges = false;
+        for (CartItemDTO item : items) {
+            Integer currentQuantity = item.getQuantity();
+            Integer lockedQuantity = item.getLockedQuantity();
+            
+            if (currentQuantity != null && currentQuantity > 0) {
+                if (lockedQuantity == null || lockedQuantity == 0) {
+                    // 如果还没有已下单数量,将当前数量设置为已下单数量
+                    item.setLockedQuantity(currentQuantity);
+                    hasChanges = true;
+                    log.info("设置商品已下单数量, cuisineId={}, orderedQuantity={}", item.getCuisineId(), currentQuantity);
+                } else if (currentQuantity > lockedQuantity) {
+                    // 如果已有已下单数量,且当前数量大于已下单数量(再次下单的情况),将新增数量累加到已下单数量
+                    Integer newLockedQuantity = lockedQuantity + (currentQuantity - lockedQuantity);
+                    item.setLockedQuantity(newLockedQuantity);
+                    hasChanges = true;
+                    log.info("更新商品已下单数量, cuisineId={}, oldOrderedQuantity={}, newOrderedQuantity={}", 
+                            item.getCuisineId(), lockedQuantity, newLockedQuantity);
+                }
+            }
+        }
+        
+        // 如果有变化,保存购物车
+        if (hasChanges) {
+            saveCart(cart);
+        }
+        
+        return cart;
+    }
+
     /**
-     * 保存购物车到Redis和数据库(双写策略)
+     * 保存购物车到Redis和数据库(优化后的双写策略)
+     * Redis同步写入(保证实时性),数据库异步批量写入(提高性能)
      */
     private void saveCart(CartDTO cart) {
-        // 保存到Redis
+        // 1. 同步保存到Redis(保证实时性)
         saveCartToRedis(cart);
 
-        // 保存到数据库
-        saveCartToDatabase(cart);
+        // 2. 异步保存到数据库(不阻塞主流程,提高性能)
+        CompletableFuture.runAsync(() -> {
+            try {
+                saveCartToDatabase(cart);
+            } catch (Exception e) {
+                log.error("异步保存购物车到数据库失败, tableId={}, error={}", cart.getTableId(), e.getMessage(), e);
+            }
+        }, CART_DB_WRITE_EXECUTOR);
     }
 
     /**
@@ -661,29 +836,30 @@ public class CartServiceImpl implements CartService {
     }
 
     /**
-     * 保存购物车到数据库
+     * 保存购物车到数据库(优化后的批量操作版本)
+     * 使用批量逻辑删除和批量插入,提高性能
      */
     private void saveCartToDatabase(CartDTO cart) {
         try {
-            // 先删除该桌号的所有购物车记录(逻辑删除)
-            LambdaQueryWrapper<StoreCart> deleteWrapper = new LambdaQueryWrapper<>();
-            deleteWrapper.eq(StoreCart::getTableId, cart.getTableId());
-            deleteWrapper.eq(StoreCart::getDeleteFlag, 0);
-            List<StoreCart> existingCarts = storeCartMapper.selectList(deleteWrapper);
-            if (existingCarts != null && !existingCarts.isEmpty()) {
-                Date now = new Date();
-                for (StoreCart existing : existingCarts) {
-                    existing.setDeleteFlag(1);
-                    existing.setUpdatedTime(now);
-                    storeCartMapper.updateById(existing);
-                }
+            Date now = new Date();
+            Integer userId = TokenUtil.getCurrentUserId();
+
+            // 1. 批量逻辑删除该桌号的所有购物车记录(使用 MyBatis-Plus 的 deleteBatchIds)
+            LambdaQueryWrapper<StoreCart> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(StoreCart::getTableId, cart.getTableId())
+                    .eq(StoreCart::getDeleteFlag, 0);
+            List<StoreCart> existingCartList = storeCartMapper.selectList(queryWrapper);
+            if (existingCartList != null && !existingCartList.isEmpty()) {
+                List<Integer> cartIds = existingCartList.stream()
+                        .map(StoreCart::getId)
+                        .collect(Collectors.toList());
+                // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+                storeCartMapper.deleteBatchIds(cartIds);
             }
 
-            // 插入新的购物车记录
+            // 2. 批量插入新的购物车记录
             if (cart.getItems() != null && !cart.getItems().isEmpty()) {
-                Integer userId = TokenUtil.getCurrentUserId();
-                Date now = new Date();
-
+                List<StoreCart> cartList = new ArrayList<>(cart.getItems().size());
                 for (CartItemDTO item : cart.getItems()) {
                     StoreCart storeCart = new StoreCart();
                     storeCart.setTableId(cart.getTableId());
@@ -693,6 +869,7 @@ public class CartServiceImpl implements CartService {
                     storeCart.setCuisineImage(item.getCuisineImage());
                     storeCart.setUnitPrice(item.getUnitPrice());
                     storeCart.setQuantity(item.getQuantity());
+                    storeCart.setLockedQuantity(item.getLockedQuantity());
                     storeCart.setSubtotalAmount(item.getSubtotalAmount());
                     storeCart.setAddUserId(item.getAddUserId());
                     storeCart.setAddUserPhone(item.getAddUserPhone());
@@ -701,19 +878,40 @@ public class CartServiceImpl implements CartService {
                     storeCart.setCreatedTime(now);
                     storeCart.setCreatedUserId(userId);
                     storeCart.setUpdatedTime(now);
-                    storeCartMapper.insert(storeCart);
+                    cartList.add(storeCart);
+                }
+
+                // 批量插入(如果数量较少,直接循环插入;如果数量较多,可以考虑分批插入)
+                if (cartList.size() <= 50) {
+                    // 小批量直接插入
+                    for (StoreCart storeCart : cartList) {
+                        storeCartMapper.insert(storeCart);
+                    }
+                } else {
+                    // 大批量分批插入(每批50条)
+                    int batchSize = 50;
+                    for (int i = 0; i < cartList.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, cartList.size());
+                        List<StoreCart> batch = cartList.subList(i, end);
+                        for (StoreCart storeCart : batch) {
+                            storeCartMapper.insert(storeCart);
+                        }
+                    }
                 }
             }
 
-            // 更新桌号表的购物车统计
+            // 3. 更新桌号表的购物车统计
             StoreTable table = storeTableMapper.selectById(cart.getTableId());
             if (table != null) {
                 table.setCartItemCount(cart.getTotalQuantity());
                 table.setCartTotalAmount(cart.getTotalAmount());
                 storeTableMapper.updateById(table);
             }
+
+            log.debug("购物车数据已异步保存到数据库, tableId={}, itemCount={}", 
+                    cart.getTableId(), cart.getItems() != null ? cart.getItems().size() : 0);
         } catch (Exception e) {
-            log.error("保存购物车到数据库失败: {}", e.getMessage(), e);
+            log.error("保存购物车到数据库失败, tableId={}, error={}", cart.getTableId(), e.getMessage(), e);
             // 数据库保存失败不影响Redis,继续执行
         }
     }

+ 38 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreInfoServiceImpl.java

@@ -7,13 +7,17 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import shop.alien.entity.store.StoreCuisine;
 import shop.alien.entity.store.StoreCuisineCategory;
+import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StoreTable;
+import shop.alien.entity.store.dto.StoreInfoWithHomepageCuisinesDTO;
 import shop.alien.mapper.StoreCuisineCategoryMapper;
 import shop.alien.mapper.StoreCuisineMapper;
+import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.mapper.StoreTableMapper;
 import shop.alien.dining.service.StoreInfoService;
 import org.apache.commons.lang3.StringUtils;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -30,6 +34,7 @@ public class StoreInfoServiceImpl implements StoreInfoService {
     private final StoreTableMapper storeTableMapper;
     private final StoreCuisineCategoryMapper storeCuisineCategoryMapper;
     private final StoreCuisineMapper storeCuisineMapper;
+    private final StoreInfoMapper storeInfoMapper;
 
     @Override
     public List<StoreTable> getTablesByStoreId(Integer storeId) {
@@ -96,4 +101,37 @@ public class StoreInfoServiceImpl implements StoreInfoService {
                 })
                 .collect(java.util.stream.Collectors.toList());
     }
+
+    @Override
+    public StoreInfoWithHomepageCuisinesDTO getStoreInfoWithHomepageCuisines(Integer storeId) {
+        log.info("根据商铺ID查询店铺信息和首页展示美食价目表, storeId={}", storeId);
+        
+        // 1. 查询店铺信息
+        StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            throw new RuntimeException("店铺不存在");
+        }
+        
+        // 2. 查询首页展示的美食价目表(is_homepage_display = 1,上架状态 = 1,审核通过 = 1)
+        LambdaQueryWrapper<StoreCuisine> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreCuisine::getStoreId, storeId);
+        wrapper.eq(StoreCuisine::getDeleteFlag, 0);
+        wrapper.eq(StoreCuisine::getIsHomepageDisplay, 1); // 首页展示
+        wrapper.eq(StoreCuisine::getShelfStatus, 1); // 上架状态
+        wrapper.eq(StoreCuisine::getStatus, 1); // 审核通过
+        wrapper.orderByDesc(StoreCuisine::getCreatedTime); // 按创建时间倒序
+        
+        List<StoreCuisine> homepageCuisines = storeCuisineMapper.selectList(wrapper);
+        if (homepageCuisines == null) {
+            homepageCuisines = new ArrayList<>();
+        }
+        
+        // 3. 构建返回DTO
+        StoreInfoWithHomepageCuisinesDTO dto = new StoreInfoWithHomepageCuisinesDTO();
+        dto.setStoreInfo(storeInfo);
+        dto.setHomepageCuisines(homepageCuisines);
+        
+        log.info("查询完成, storeId={}, 首页展示美食数量={}", storeId, homepageCuisines.size());
+        return dto;
+    }
 }

+ 166 - 0
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -1,6 +1,7 @@
 package shop.alien.dining.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -9,12 +10,15 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
+import shop.alien.dining.config.BaseRedisService;
 import shop.alien.dining.service.CartService;
 import shop.alien.dining.service.StoreOrderService;
 import shop.alien.dining.util.TokenUtil;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.CartDTO;
+import shop.alien.entity.store.dto.CartItemDTO;
 import shop.alien.entity.store.dto.CreateOrderDTO;
+import shop.alien.entity.store.vo.OrderInfoVO;
 import shop.alien.mapper.*;
 
 import java.math.BigDecimal;
@@ -43,6 +47,9 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
     private final CartService cartService;
     private final StoreOrderLockMapper storeOrderLockMapper;
     private final StoreCouponUsageMapper storeCouponUsageMapper;
+    private final StoreCartMapper storeCartMapper;
+    private final BaseRedisService baseRedisService;
+    private final StoreInfoMapper storeInfoMapper;
 
     @Override
     public StoreOrder createOrder(CreateOrderDTO dto) {
@@ -203,6 +210,9 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         table.setStatus(1); // 就餐中
         storeTableMapper.updateById(table);
 
+        // 锁定购物车商品数量(禁止减少或删除已下单的商品)
+        cartService.lockCartItems(dto.getTableId());
+
         // 下单后不清空购物车,允许加餐(加餐时添加到同一订单)
         // 只有在支付完成后才清空购物车
 
@@ -578,6 +588,162 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         return order;
     }
 
+    @Override
+    public boolean resetTable(Integer tableId) {
+        log.info("管理员重置餐桌, tableId={}", tableId);
+        
+        // 验证餐桌是否存在
+        StoreTable table = storeTableMapper.selectById(tableId);
+        if (table == null) {
+            throw new RuntimeException("餐桌不存在");
+        }
+        
+        // 1. 删除购物车数据(逻辑删除,使用 MyBatis-Plus 的 removeBatchIds)
+        LambdaQueryWrapper<StoreCart> cartWrapper = new LambdaQueryWrapper<>();
+        cartWrapper.eq(StoreCart::getTableId, tableId);
+        cartWrapper.eq(StoreCart::getDeleteFlag, 0);
+        List<StoreCart> cartList = storeCartMapper.selectList(cartWrapper);
+        if (cartList != null && !cartList.isEmpty()) {
+            List<Integer> cartIds = cartList.stream()
+                    .map(StoreCart::getId)
+                    .collect(Collectors.toList());
+            // 使用 removeBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+            storeCartMapper.deleteBatchIds(cartIds);
+            log.info("删除购物车数据, tableId={}, count={}", tableId, cartList.size());
+        }
+        
+        // 2. 删除订单数据(逻辑删除,包括订单明细)
+        LambdaQueryWrapper<StoreOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(StoreOrder::getTableId, tableId);
+        orderWrapper.eq(StoreOrder::getDeleteFlag, 0);
+        List<StoreOrder> orderList = this.list(orderWrapper);
+        if (orderList != null && !orderList.isEmpty()) {
+            List<Integer> orderIds = orderList.stream()
+                    .map(StoreOrder::getId)
+                    .collect(Collectors.toList());
+            
+            // 删除订单明细(逻辑删除,使用 MyBatis-Plus 的 deleteBatchIds)
+            for (Integer orderId : orderIds) {
+                LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+                detailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
+                detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+                List<StoreOrderDetail> detailList = orderDetailMapper.selectList(detailWrapper);
+                if (detailList != null && !detailList.isEmpty()) {
+                    List<Integer> detailIds = detailList.stream()
+                            .map(StoreOrderDetail::getId)
+                            .collect(Collectors.toList());
+                    // 使用 deleteBatchIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+                    orderDetailMapper.deleteBatchIds(detailIds);
+                    log.info("删除订单明细, orderId={}, count={}", orderId, detailList.size());
+                }
+            }
+            
+            // 删除订单(逻辑删除,使用 MyBatis-Plus 的 removeByIds)
+            // 使用 removeByIds 进行逻辑删除(MyBatis-Plus 会自动处理 @TableLogic)
+            this.removeByIds(orderIds);
+            log.info("删除订单数据, tableId={}, count={}", tableId, orderList.size());
+        }
+        
+        // 3. 清空Redis中的购物车缓存
+        String cartKey = "cart:table:" + tableId;
+        baseRedisService.delete(cartKey);
+        log.info("清空Redis购物车缓存, tableId={}", tableId);
+        
+        // 4. 清除优惠券使用标记
+        cartService.clearCouponUsed(tableId);
+        log.info("清除优惠券使用标记, tableId={}", tableId);
+        
+        // 5. 重置餐桌表(使用 LambdaUpdateWrapper 来显式设置 null 值)
+        LambdaUpdateWrapper<StoreTable> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(StoreTable::getId, tableId)
+                .set(StoreTable::getCurrentOrderId, null)  // 显式设置 null
+                .set(StoreTable::getCurrentCouponId, null)  // 显式设置 null
+                .set(StoreTable::getCartItemCount, 0)
+                .set(StoreTable::getCartTotalAmount, BigDecimal.ZERO)
+                .set(StoreTable::getStatus, 0)  // 空闲
+                .set(StoreTable::getUpdatedTime, new Date());
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            updateWrapper.set(StoreTable::getUpdatedUserId, userId);
+        }
+        storeTableMapper.update(null, updateWrapper);
+        log.info("重置餐桌表完成, tableId={}", tableId);
+        
+        return true;
+    }
+
+    @Override
+    public OrderInfoVO getOrderInfo(Integer orderId) {
+        log.info("查询订单信息, orderId={}", orderId);
+        
+        // 1. 查询订单基本信息
+        StoreOrder order = this.getById(orderId);
+        if (order == null || order.getDeleteFlag() == 1) {
+            throw new RuntimeException("订单不存在");
+        }
+        
+        // 2. 查询订单明细(菜品清单)
+        LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
+        detailWrapper.eq(StoreOrderDetail::getOrderId, orderId);
+        detailWrapper.eq(StoreOrderDetail::getDeleteFlag, 0);
+        detailWrapper.orderByDesc(StoreOrderDetail::getCreatedTime);
+        List<StoreOrderDetail> details = orderDetailMapper.selectList(detailWrapper);
+        
+        // 转换为CartItemDTO
+        List<CartItemDTO> items = details.stream().map(detail -> {
+            CartItemDTO item = new CartItemDTO();
+            item.setCuisineId(detail.getCuisineId());
+            item.setCuisineName(detail.getCuisineName());
+            item.setCuisineType(detail.getCuisineType());
+            item.setCuisineImage(detail.getCuisineImage());
+            item.setUnitPrice(detail.getUnitPrice());
+            item.setQuantity(detail.getQuantity());
+            item.setSubtotalAmount(detail.getSubtotalAmount());
+            item.setAddUserId(detail.getAddUserId());
+            item.setAddUserPhone(detail.getAddUserPhone());
+            item.setRemark(detail.getRemark());
+            return item;
+        }).collect(Collectors.toList());
+        
+        // 3. 查询门店信息
+        StoreInfo storeInfo = storeInfoMapper.selectById(order.getStoreId());
+        String storeName = storeInfo != null ? storeInfo.getStoreName() : "";
+        
+        // 4. 查询优惠券信息(如果有)
+        String couponName = null;
+        if (order.getCouponId() != null) {
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            if (coupon != null) {
+                couponName = coupon.getName();
+            }
+        }
+        
+        // 5. 组装OrderInfoVO
+        OrderInfoVO vo = new OrderInfoVO();
+        vo.setOrderId(order.getId());
+        vo.setOrderNo(order.getOrderNo());
+        vo.setStoreName(storeName);
+        vo.setTableNumber(order.getTableNumber());
+        vo.setDinerCount(order.getDinerCount());
+        vo.setContactPhone(order.getContactPhone());
+        vo.setRemark(order.getRemark());
+        vo.setItems(items);
+        vo.setTotalAmount(order.getTotalAmount());
+        vo.setTablewareFee(order.getTablewareFee());
+        vo.setCouponId(order.getCouponId());
+        vo.setCouponName(couponName);
+        vo.setDiscountAmount(order.getDiscountAmount());
+        vo.setPayAmount(order.getPayAmount());
+        vo.setOrderStatus(order.getOrderStatus());
+        vo.setPayStatus(order.getPayStatus());
+        vo.setPayType(order.getPayType());
+        vo.setCreatedTime(order.getCreatedTime());
+        vo.setPayTime(order.getPayTime());
+        
+        log.info("查询订单信息完成, orderId={}, itemCount={}", orderId, items.size());
+        return vo;
+    }
+
     /**
      * 生成订单号
      */

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreCart.java

@@ -54,6 +54,10 @@ public class StoreCart {
     @TableField("quantity")
     private Integer quantity;
 
+    @ApiModelProperty(value = "已下单数量(下单时锁定的数量,不允许减少或删除)")
+    @TableField("locked_quantity")
+    private Integer lockedQuantity;
+
     @ApiModelProperty(value = "小计金额")
     @TableField("subtotal_amount")
     private BigDecimal subtotalAmount;

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/StorePlatformUserRole.java

@@ -55,4 +55,8 @@ public class StorePlatformUserRole implements Serializable {
     @TableField("account_name")
     private String accountName;
 
+    @ApiModelProperty(value = "禁用标志(0启用 1 禁用)")
+    @TableField("status")
+    private Integer status;
+
 }

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/CartItemDTO.java

@@ -34,6 +34,9 @@ public class CartItemDTO {
     @ApiModelProperty(value = "数量")
     private Integer quantity;
 
+    @ApiModelProperty(value = "已下单数量(下单时锁定的数量,不允许减少或删除)")
+    private Integer lockedQuantity;
+
     @ApiModelProperty(value = "小计金额")
     private BigDecimal subtotalAmount;
 

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreInfoWithHomepageCuisinesDTO.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.StoreCuisine;
+import shop.alien.entity.store.StoreInfo;
+
+import java.util.List;
+
+/**
+ * 店铺信息和首页展示美食价目表DTO
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@ApiModel(value = "StoreInfoWithHomepageCuisinesDTO对象", description = "店铺信息和首页展示美食价目表")
+public class StoreInfoWithHomepageCuisinesDTO {
+
+    @ApiModelProperty(value = "店铺信息")
+    private StoreInfo storeInfo;
+
+    @ApiModelProperty(value = "首页展示的美食价目表列表")
+    private List<StoreCuisine> homepageCuisines;
+}

+ 0 - 4
alien-entity/src/main/java/shop/alien/entity/store/excelVo/StoreUserExcelVo.java

@@ -23,10 +23,6 @@ public class StoreUserExcelVo {
     @ExcelHeader(value = "账号ID")
     private Integer id;
 
-//    @ApiModelProperty(value = "主账号手机号")
-//    @ExcelHeader(value = "主账号手机号码")
-//    @JsonInclude(JsonInclude.Include.NON_NULL)
-//    private String parentAccountPhone;
 
     @ApiModelProperty(value = "手机号码")
     @ExcelHeader(value = "手机号码")

+ 6 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackListVo.java

@@ -22,9 +22,15 @@ public class LifeFeedbackListVo implements Serializable {
     @ApiModelProperty(value = "用户昵称")
     private String nickName;
 
+    @ApiModelProperty(value = "用户昵称")
+    private String userNickName;
+
     @ApiModelProperty(value = "账号(手机号)")
     private String phone;
 
+    @ApiModelProperty(value = "用户手机号)")
+    private String userPhone;
+
     @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈")
     private Integer feedbackType;
 

+ 78 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/OrderInfoVO.java

@@ -0,0 +1,78 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.dto.CartItemDTO;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 订单信息VO(用于查询订单详情)
+ *
+ * @author system
+ * @since 2025-02-02
+ */
+@Data
+@ApiModel(value = "OrderInfoVO对象", description = "订单信息")
+public class OrderInfoVO {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "店铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "桌号")
+    private String tableNumber;
+
+    @ApiModelProperty(value = "就餐人数")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "菜品清单")
+    private List<CartItemDTO> items;
+
+    @ApiModelProperty(value = "菜品总价")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "餐具费")
+    private BigDecimal tablewareFee;
+
+    @ApiModelProperty(value = "优惠券ID")
+    private Integer couponId;
+
+    @ApiModelProperty(value = "优惠券名称")
+    private String couponName;
+
+    @ApiModelProperty(value = "优惠金额")
+    private BigDecimal discountAmount;
+
+    @ApiModelProperty(value = "应付金额")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "订单状态(0:待支付, 1:已支付, 2:已取消, 3:已完成)")
+    private Integer orderStatus;
+
+    @ApiModelProperty(value = "支付状态(0:未支付, 1:已支付, 2:已退款)")
+    private Integer payStatus;
+
+    @ApiModelProperty(value = "支付方式(1:微信, 2:支付宝, 3:现金)")
+    private Integer payType;
+
+    @ApiModelProperty(value = "创建时间")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "支付时间")
+    private Date payTime;
+}

+ 6 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/PerformerVo.java

@@ -43,5 +43,11 @@ public class PerformerVo implements Serializable {
      */
     @ApiModelProperty(value = "职位")
     private String style;
+
+    /**
+     * 标签(对应 store_staff_config.tag)
+     */
+    @ApiModelProperty(value = "标签")
+    private String tag;
 }
 

+ 7 - 3
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreSubExcelVo.java

@@ -15,14 +15,18 @@ public class StoreSubExcelVo {
     @ApiModelProperty(value = "序号")
     private Integer serialNumber;
 
-    @ApiModelProperty(value = "账号ID")
-    @ExcelHeader(value = "账号ID")
-    private Integer id;
+    @ApiModelProperty(value = "账号ID(主账号联系方式所对应的id)")
+    @ExcelHeader(value = "账号ID")
+    private Integer parentAccountId;
 
     @ApiModelProperty(value = "主账号手机号")
     @ExcelHeader(value = "主账号手机号码")
     private String parentAccountPhone;
 
+    @ApiModelProperty(value = "子账号ID")
+    @ExcelHeader(value = "子账号ID")
+    private Integer subAccountId;
+
     @ApiModelProperty(value = "手机号码")
     @ExcelHeader(value = "手机号码")
     private String phone;

+ 4 - 1
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreUserVo.java

@@ -20,9 +20,12 @@ import java.util.List;
 @JsonInclude
 @ApiModel(value = "StoreUserVo对象", description = "门店用户扩展")
 public class StoreUserVo extends StoreUser {
-     @ApiModelProperty(value = "父账号Id")
+     @ApiModelProperty(value = "父账号Id(主账号联系方式所对应的id)")
     private Integer parentAccountId;
 
+    @ApiModelProperty(value = "子账号ID(用于删除/禁用等操作,子账号列表时必填)")
+    private Integer subAccountId;
+
     @ApiModelProperty(value = "登录Token")
     private String token;
 

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/SubAccountStoreListVo.java

@@ -49,4 +49,7 @@ public class SubAccountStoreListVo implements Serializable {
 
     @ApiModelProperty(value = "手机号")
     private String phone;
+
+    @ApiModelProperty(value = "禁用标志(0启用 1禁用)")
+    private Integer status;
 }

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/storePlatform/StoreOperationalActivity.java

@@ -136,5 +136,9 @@ public class StoreOperationalActivity {
     @TableField("audit_time")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date auditTime;
+
+    @ApiModelProperty(value = "上传图片类型:0-本地上传, 1-AI生成")
+    @TableField("upload_img_type")
+    private Integer uploadImgType;
 }
 

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/storePlatform/vo/StoreOperationalActivityVO.java

@@ -60,5 +60,8 @@ public class StoreOperationalActivityVO extends StoreOperationalActivity {
 
     @ApiModelProperty(value = "活动类型名称")
     private String activityTypeName;
+
+    @ApiModelProperty(value = "上传图片类型:0-本地上传, 1-AI生成")
+    private Integer uploadImgType;
 }
 

+ 3 - 0
alien-entity/src/main/resources/mapper/LifeFeedbackMapper.xml

@@ -121,7 +121,9 @@
         SELECT
             f.id,
             u.nick_name AS nickName,
+            lu.user_name AS userNickName,
             u.phone AS phone,
+            lu.user_phone AS userPhone,
             f.feedback_type AS feedbackType,
             CASE f.feedback_type
                 WHEN 0 THEN 'bug反馈'
@@ -149,6 +151,7 @@
         FROM life_feedback f
         LEFT JOIN store_user u ON f.user_id = u.id
         LEFT JOIN life_sys s ON f.staff_id = s.id
+        LEFT JOIN life_user lu ON f.user_id = lu.id
         WHERE 1=1
         <if test="feedbackType != null">
             AND f.feedback_type = #{feedbackType}

+ 9 - 5
alien-entity/src/main/resources/mapper/SubAccountStoreMapper.xml

@@ -17,7 +17,8 @@
             spr.role_name COLLATE utf8mb4_unicode_ci AS roleName,
             spur.user_id AS userId,
             spur.account_name COLLATE utf8mb4_unicode_ci AS accountName,
-            su.phone COLLATE utf8mb4_unicode_ci AS phone
+            su.phone COLLATE utf8mb4_unicode_ci AS phone,
+            spur.status AS status
         FROM
             store_platform_user_role spur
         INNER JOIN store_user su ON spur.user_id = su.id
@@ -44,7 +45,8 @@
             NULL AS roleName,
             su_main.id AS userId,
             su_main.name COLLATE utf8mb4_unicode_ci AS accountName,
-            su_main.phone COLLATE utf8mb4_unicode_ci AS phone
+            su_main.phone COLLATE utf8mb4_unicode_ci AS phone,
+            NULL AS status
         FROM
             store_user su_main
         LEFT JOIN store_info si_main ON su_main.store_id = si_main.id 
@@ -85,7 +87,8 @@
             spr.role_name COLLATE utf8mb4_unicode_ci AS roleName,
             spur.user_id AS userId,
             spur.account_name COLLATE utf8mb4_unicode_ci AS accountName,
-            su.phone COLLATE utf8mb4_unicode_ci AS phone
+            su.phone COLLATE utf8mb4_unicode_ci AS phone,
+            spur.status AS status
         FROM
             store_platform_user_role spur
                 INNER JOIN store_user su ON spur.user_id = su.id
@@ -111,14 +114,15 @@
             NULL AS roleName,
             su_main.id AS userId,
             su_main.name COLLATE utf8mb4_unicode_ci AS accountName,
-            su_main.phone COLLATE utf8mb4_unicode_ci AS phone
+            su_main.phone COLLATE utf8mb4_unicode_ci AS phone,
+            NULL AS status
         FROM
             store_user su_main
                 INNER JOIN store_info si_main ON su_main.store_id = si_main.id
         WHERE
             (
                 -- 如果传入的是子账号,查询其主账号的门店
-                (su_main.id = (SELECT sub_account_id FROM store_user WHERE id = #{userId} AND account_type = 2 AND delete_flag = 0 LIMIT 1))
+                (su_main.id = (SELECT id FROM store_user WHERE id = #{userId} AND account_type = 2 AND delete_flag = 0 LIMIT 1))
            OR
            -- 如果传入的是主账号,查询自己的门店
             (su_main.id = #{userId} AND su_main.account_type = 1)

+ 6 - 1
alien-gateway/src/main/java/shop/alien/gateway/service/impl/StoreUserServiceImpl.java

@@ -146,6 +146,7 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserGatewayMapper, St
                 // 查询子账号关联的所有门店和角色
                 LambdaQueryWrapper<StorePlatformUserRole> roleWrapper = new LambdaQueryWrapper<>();
                 roleWrapper.eq(StorePlatformUserRole::getUserId, storeUser.getId())
+                        .eq(StorePlatformUserRole::getStatus, 0)
                         .eq(StorePlatformUserRole::getDeleteFlag, 0);
                 List<StorePlatformUserRole> userRoles = storePlatformUserRoleMapper.selectList(roleWrapper);
 
@@ -173,6 +174,9 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserGatewayMapper, St
                     }
 
                     if (maxPermissionRole != null) {
+                        if(maxPermissionRole.getStatus() == 1){
+                            return R.fail("子账号已被禁用无法登录");
+                        }
                         // 设置门店ID和角色ID
                         storeUserVo.setStoreId(maxPermissionRole.getStoreId());
                         storeUserVo.setRoleId(maxPermissionRole.getRoleId());
@@ -193,7 +197,8 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserGatewayMapper, St
                         log.warn("子账号关联的门店都没有角色ID - userId: {}", storeUser.getId());
                     }
                 } else {
-                    log.warn("子账号未关联任何门店 - userId: {}", storeUser.getId());
+                    log.warn("子账号未关联任何门店或子账号已被禁用 - userId: {}", storeUser.getId());
+                    return R.fail("子账号已被禁用无法登录");
                 }
             }
         }

+ 16 - 78
alien-gateway/src/main/resources/logback-spring.xml

@@ -1,40 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
-<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
-<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
-<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
-<!-- 该信息是由于设置了当配置文件变化时重新加载,所以每当达到扫描时间的时候就会检查配置文件是否错误。但是由于一般配置文件都放在了JAR包中,
-    而扫描的时候无法扫描JAR包内,因此会提示没有可以检查的文件,所以每隔一段时间就输出一次-->
-<configuration scan="false" scanPeriod="60 seconds" debug="true">
-    <contextName>logback-spring</contextName>
+<configuration scan="false" scanPeriod="60 seconds" debug="false">
 
-    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
     <!-- 定义全局参数常量 -->
     <property name="log.level" value="debug"/>
-    <property name="log.maxHistory" value="30"/><!-- 30表示30 -->
-    <springProperty scope="context" name="logging.path" source="logging.path"   defaultValue="C:/project/ext/log"/>
+    <property name="log.maxHistory" value="30"/><!-- 30表示30天 -->
+    <springProperty scope="context" name="logging.path" source="logging.path" defaultValue="C:/project/ext/log"/>
     <!--输出文件前缀-->
-    <property name="FILENAME" value="xiaokuihua_gateway"/>
-
-    <!--0. 日志格式和颜色渲染 -->
-    <!-- 彩色日志依赖的渲染类 -->
-    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
-    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
-    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
+    <property name="FILENAME" value="alien"/>
 
     <!-- 文件输出格式 -->
     <property name="FILE_LOG_PATTERN" value="[%d{MM/dd HH:mm:ss.SSS}][%-10.10thread][%-5level][%-40.40c{1}:%5line]:[%15method] || %m%n"/>
-    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+    
+    <!-- 控制台输出格式:纯文本,无颜色,适合 Docker/EFK -->
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n"/>
 
     <!--1. 输出到控制台-->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <!-- 【关键】控制台只输出 INFO 及以上,防止 SQL 刷屏 ES -->
         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-            <level>${log.level}</level>
+            <level>INFO</level>
         </filter>
         <encoder>
             <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
-            <!-- 设置字符集 -->
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -42,27 +30,17 @@
     <!--2. 输出到文档-->
     <!-- DEBUG 日志 -->
     <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 当前的日志文件存放路径 -->
         <file>${logging.path}/DEBUG.log</file>
-        <!-- 日志滚动策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 历史日志文件的存放路径和名称 -->
             <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
-            <!-- 日志文件最大的保存历史 数量-->
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
             <pattern>${FILE_LOG_PATTERN}</pattern>
         </encoder>
-        <!--日志文件最大的大小-->
-        <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
-        <!--            <MaxFileSize>10MB</MaxFileSize>-->
-        <!--        </triggeringPolicy>-->
-        <!-- 此日志文档只记录debug级别的 -->
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>DEBUG</level>
-            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMatch>ACCEPT</onMatch>
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
@@ -117,44 +95,14 @@
         </filter>
     </appender>
 
-    <!--
-      <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
-      以及指定<appender>。<logger>仅有一个name属性,
-      一个可选的level和一个可选的addtivity属性。
-      name:用来指定受此logger约束的某一个包或者具体的某一个类。
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-         还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
-         如果未设置此属性,那么当前logger将会继承上级的级别。
-      addtivity:是否向上级logger传递打印信息。默认是true。
-      <logger name="org.springframework.web" level="info"/>
-      <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-    -->
-
-    <!--
-      使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
-      第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
-      第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
-      【logging.level.org.mybatis=debug logging.level.dao=debug】
-     -->
-    <!-- mybatis显示sql,修改此处扫描包名 -->
-
-
-    <!--
-      root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-      不能设置为INHERITED或者同义词NULL。默认是DEBUG
-      可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-    -->
-
-    <!-- 关闭 Swagger/SpringDoc CachingOperationNameGenerator 的 INFO 日志 -->
-    <logger name="d.s.w.r.o.CachingOperationNameGenerator" level="WARN"/>
+    <!-- 降噪配置 -->
+    <logger name="springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator" level="WARN"/>
+    <logger name="org.springframework.security.web.DefaultSecurityFilterChain " level="WARN"/>
+    <logger name="com.netflix.config.sources.URLConfigurationSource " level="WARN"/>
+    <logger name="com.netflix.discovery" level="WARN"/>
+    <logger name="org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]" level="WARN"/>
 
     <!-- 4. 最终的策略 -->
-    <!-- 4.1 开发环境:打印控制台-->
-    <!--打印sql-->
-    <!--    <logger name="com.veryhappy.music.dao" level="debug"/>-->
-
-    <!--打印log-->
     <root level="info">
         <appender-ref ref="CONSOLE"/>
         <appender-ref ref="DEBUG_FILE"/>
@@ -163,14 +111,4 @@
         <appender-ref ref="ERROR_FILE"/>
     </root>
 
-    <!--   4.2 生产环境:输出到文档-->
-    <springProfile name="pro">
-        <root level="info">
-            <appender-ref ref="CONSOLE"/>
-            <appender-ref ref="DEBUG_FILE"/>
-            <appender-ref ref="INFO_FILE"/>
-            <appender-ref ref="ERROR_FILE"/>
-            <appender-ref ref="WARN_FILE"/>
-        </root>
-    </springProfile>
 </configuration>

+ 145 - 0
alien-job/src/main/java/shop/alien/job/store/StoreOperationalActivityJob.java

@@ -0,0 +1,145 @@
+package shop.alien.job.store;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.storePlatform.StoreOperationalActivity;
+import shop.alien.mapper.storePlantform.StoreOperationalActivityMapper;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 营销活动状态定时任务
+ * 根据活动的开始时间和结束时间自动更新活动状态
+ *
+ * @author system
+ * @date 2025/01/XX
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class StoreOperationalActivityJob {
+
+    private final StoreOperationalActivityMapper activityMapper;
+
+    /**
+     * 营销活动状态更新任务
+     * 定时更新活动状态:未开始 -> 进行中 -> 已结束
+     */
+    @XxlJob("operationalActivityStatusUpdateTask")
+    public void operationalActivityStatusUpdateTask() {
+        log.info("【定时任务】开始执行营销活动状态更新任务...");
+        
+        // 获取当前时间(零点)
+        Date now = new Date();
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(now);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        now = calendar.getTime();
+
+        try {
+            // 查询需要更新状态的活动:2-未开始, 5-进行中, 8-审核成功
+            List<Integer> statusList = new ArrayList<>();
+            statusList.add(2); // 未开始
+            statusList.add(5); // 进行中
+            statusList.add(8); // 审核成功
+
+            LambdaQueryWrapper<StoreOperationalActivity> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreOperationalActivity::getStatus, statusList)
+                    .isNotNull(StoreOperationalActivity::getStartTime)
+                    .isNotNull(StoreOperationalActivity::getEndTime);
+            
+            List<StoreOperationalActivity> activities = activityMapper.selectList(queryWrapper);
+            log.info("【定时任务】查询到需要更新状态的活动数量: {}", activities.size());
+
+            if (activities.isEmpty()) {
+                log.info("【定时任务】没有需要更新状态的活动");
+                return;
+            }
+
+            // 需要设置为"已结束"的活动ID列表
+            List<Integer> endActivityIds = new ArrayList<>();
+            // 需要设置为"进行中"的活动ID列表
+            List<Integer> ongoingActivityIds = new ArrayList<>();
+            // 需要设置为"未开始"的活动ID列表
+            List<Integer> notStartActivityIds = new ArrayList<>();
+
+            // 遍历活动,判断状态
+            for (StoreOperationalActivity activity : activities) {
+                Date startTime = activity.getStartTime();
+                Date endTime = activity.getEndTime();
+                Integer currentStatus = activity.getStatus();
+
+                if (startTime == null || endTime == null) {
+                    log.warn("【定时任务】活动ID: {} 的开始时间或结束时间为空,跳过处理", activity.getId());
+                    continue;
+                }
+
+                // 判断当前时间与活动时间的关系
+                if (now.compareTo(endTime) > 0) {
+                    // 当前时间 > 结束时间:应该设置为"已结束"(7)
+                    // 只处理状态为"未开始"(2)或"进行中"(5)的活动
+                    if (currentStatus == 2 || currentStatus == 5) {
+                        endActivityIds.add(activity.getId());
+                        log.debug("【定时任务】活动ID: {} 已过期,需要设置为已结束", activity.getId());
+                    }
+                } else if (now.compareTo(startTime) >= 0 && now.compareTo(endTime) <= 0) {
+                    // 当前时间在活动时间范围内:应该设置为"进行中"(5)
+                    // 只处理状态为"未开始"(2)或"审核成功"(8)的活动
+                    if (currentStatus == 2 || currentStatus == 8) {
+                        ongoingActivityIds.add(activity.getId());
+                        log.debug("【定时任务】活动ID: {} 正在进行中,需要设置为进行中", activity.getId());
+                    }
+                } else if (now.compareTo(startTime) < 0) {
+                    // 当前时间 < 开始时间:应该设置为"未开始"(2)
+                    // 只处理状态为"审核成功"(8)的活动
+                    if (currentStatus == 8) {
+                        notStartActivityIds.add(activity.getId());
+                        log.debug("【定时任务】活动ID: {} 未开始,需要设置为未开始", activity.getId());
+                    }
+                }
+            }
+
+            // 批量更新状态为"已结束"
+            if (!endActivityIds.isEmpty()) {
+                LambdaUpdateWrapper<StoreOperationalActivity> updateWrapper = new LambdaUpdateWrapper<>();
+                updateWrapper.in(StoreOperationalActivity::getId, endActivityIds)
+                        .set(StoreOperationalActivity::getStatus, 7); // 7-已结束
+                int updateCount = activityMapper.update(null, updateWrapper);
+                log.info("【定时任务】已结束活动更新完成,活动ID列表: {},更新数量: {}", endActivityIds, updateCount);
+            }
+
+            // 批量更新状态为"进行中"
+            if (!ongoingActivityIds.isEmpty()) {
+                LambdaUpdateWrapper<StoreOperationalActivity> updateWrapper = new LambdaUpdateWrapper<>();
+                updateWrapper.in(StoreOperationalActivity::getId, ongoingActivityIds)
+                        .set(StoreOperationalActivity::getStatus, 5); // 5-进行中
+                int updateCount = activityMapper.update(null, updateWrapper);
+                log.info("【定时任务】进行中活动更新完成,活动ID列表: {},更新数量: {}", ongoingActivityIds, updateCount);
+            }
+
+            // 批量更新状态为"未开始"
+            if (!notStartActivityIds.isEmpty()) {
+                LambdaUpdateWrapper<StoreOperationalActivity> updateWrapper = new LambdaUpdateWrapper<>();
+                updateWrapper.in(StoreOperationalActivity::getId, notStartActivityIds)
+                        .set(StoreOperationalActivity::getStatus, 2); // 2-未开始
+                int updateCount = activityMapper.update(null, updateWrapper);
+                log.info("【定时任务】未开始活动更新完成,活动ID列表: {},更新数量: {}", notStartActivityIds, updateCount);
+            }
+
+            log.info("【定时任务】营销活动状态更新任务执行完成");
+        } catch (Exception e) {
+            log.error("【定时任务】营销活动状态更新任务执行异常", e);
+        }
+    }
+}
+

+ 14 - 73
alien-job/src/main/resources/logback-spring.xml

@@ -1,40 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
-<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
-<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
-<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
-<!-- 该信息是由于设置了当配置文件变化时重新加载,所以每当达到扫描时间的时候就会检查配置文件是否错误。但是由于一般配置文件都放在了JAR包中,
-    而扫描的时候无法扫描JAR包内,因此会提示没有可以检查的文件,所以每隔一段时间就输出一次-->
-<configuration scan="false" scanPeriod="60 seconds" debug="true">
-    <contextName>logback-spring</contextName>
+<configuration scan="false" scanPeriod="60 seconds" debug="false">
 
-    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
     <!-- 定义全局参数常量 -->
     <property name="log.level" value="debug"/>
-    <property name="log.maxHistory" value="30"/><!-- 30表示30 -->
+    <property name="log.maxHistory" value="30"/><!-- 30表示30天 -->
     <springProperty scope="context" name="logging.path" source="logging.path" defaultValue="C:/project/ext/log"/>
     <!--输出文件前缀-->
     <property name="FILENAME" value="alien"/>
 
-    <!--0. 日志格式和颜色渲染 -->
-    <!-- 彩色日志依赖的渲染类 -->
-    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
-    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
-    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
-
     <!-- 文件输出格式 -->
     <property name="FILE_LOG_PATTERN" value="[%d{MM/dd HH:mm:ss.SSS}][%-10.10thread][%-5level][%-40.40c{1}:%5line]:[%15method] || %m%n"/>
-    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+    
+    <!-- 控制台输出格式:纯文本,无颜色,适合 Docker/EFK -->
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n"/>
 
     <!--1. 输出到控制台-->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <!-- 【关键】控制台只输出 INFO 及以上,防止 SQL 刷屏 ES -->
         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-            <level>${log.level}</level>
+            <level>INFO</level>
         </filter>
         <encoder>
             <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
-            <!-- 设置字符集 -->
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -42,27 +30,17 @@
     <!--2. 输出到文档-->
     <!-- DEBUG 日志 -->
     <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 当前的日志文件存放路径 -->
         <file>${logging.path}/DEBUG.log</file>
-        <!-- 日志滚动策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 历史日志文件的存放路径和名称 -->
             <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
-            <!-- 日志文件最大的保存历史 数量-->
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
             <pattern>${FILE_LOG_PATTERN}</pattern>
         </encoder>
-        <!--日志文件最大的大小-->
-    <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
-    <!--            <MaxFileSize>10MB</MaxFileSize>-->
-    <!--        </triggeringPolicy>-->
-        <!-- 此日志文档只记录debug级别的 -->
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>DEBUG</level>
-            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMatch>ACCEPT</onMatch>
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
@@ -117,41 +95,14 @@
         </filter>
     </appender>
 
-    <!--
-      <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
-      以及指定<appender>。<logger>仅有一个name属性,
-      一个可选的level和一个可选的addtivity属性。
-      name:用来指定受此logger约束的某一个包或者具体的某一个类。
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-         还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
-         如果未设置此属性,那么当前logger将会继承上级的级别。
-      addtivity:是否向上级logger传递打印信息。默认是true。
-      <logger name="org.springframework.web" level="info"/>
-      <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-    -->
-
-    <!--
-      使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
-      第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
-      第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
-      【logging.level.org.mybatis=debug logging.level.dao=debug】
-     -->
-    <!-- mybatis显示sql,修改此处扫描包名 -->
-
-
-    <!--
-      root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-      不能设置为INHERITED或者同义词NULL。默认是DEBUG
-      可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-    -->
+    <!-- 降噪配置 -->
+    <logger name="springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator" level="WARN"/>
+    <logger name="org.springframework.security.web.DefaultSecurityFilterChain " level="WARN"/>
+    <logger name="com.netflix.config.sources.URLConfigurationSource " level="WARN"/>
+    <logger name="com.netflix.discovery" level="WARN"/>
+    <logger name="org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]" level="WARN"/>
 
     <!-- 4. 最终的策略 -->
-    <!-- 4.1 开发环境:打印控制台-->
-    <!--打印sql-->
-    <!--    <logger name="com.veryhappy.music.dao" level="debug"/>-->
-
-    <!--打印log-->
     <root level="info">
         <appender-ref ref="CONSOLE"/>
         <appender-ref ref="DEBUG_FILE"/>
@@ -160,14 +111,4 @@
         <appender-ref ref="ERROR_FILE"/>
     </root>
 
-    <!--   4.2 生产环境:输出到文档-->
-    <springProfile name="pro">
-        <root level="info">
-            <appender-ref ref="CONSOLE"/>
-            <appender-ref ref="DEBUG_FILE"/>
-            <appender-ref ref="INFO_FILE"/>
-            <appender-ref ref="ERROR_FILE"/>
-            <appender-ref ref="WARN_FILE"/>
-        </root>
-    </springProfile>
 </configuration>

+ 15 - 74
alien-lawyer/src/main/resources/logback-spring.xml

@@ -1,40 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
-<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
-<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
-<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
-<!-- 该信息是由于设置了当配置文件变化时重新加载,所以每当达到扫描时间的时候就会检查配置文件是否错误。但是由于一般配置文件都放在了JAR包中,
-    而扫描的时候无法扫描JAR包内,因此会提示没有可以检查的文件,所以每隔一段时间就输出一次-->
-<configuration scan="false" scanPeriod="60 seconds" debug="true">
-<!--    <contextName>logback-spring</contextName>-->
+<configuration scan="false" scanPeriod="60 seconds" debug="false">
 
-    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
     <!-- 定义全局参数常量 -->
     <property name="log.level" value="debug"/>
-    <property name="log.maxHistory" value="30"/><!-- 30表示30 -->
-    <springProperty scope="context" name="logging.path" source="logging.path"  defaultValue="C:/project/ext/log"/>
+    <property name="log.maxHistory" value="30"/><!-- 30表示30天 -->
+    <springProperty scope="context" name="logging.path" source="logging.path" defaultValue="C:/project/ext/log"/>
     <!--输出文件前缀-->
     <property name="FILENAME" value="alien"/>
 
-    <!--0. 日志格式和颜色渲染 -->
-    <!-- 彩色日志依赖的渲染类 -->
-    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
-    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
-    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
-
     <!-- 文件输出格式 -->
     <property name="FILE_LOG_PATTERN" value="[%d{MM/dd HH:mm:ss.SSS}][%-10.10thread][%-5level][%-40.40c{1}:%5line]:[%15method] || %m%n"/>
-    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+    
+    <!-- 控制台输出格式:纯文本,无颜色,适合 Docker/EFK -->
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n"/>
 
     <!--1. 输出到控制台-->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <!-- 【关键】控制台只输出 INFO 及以上,防止 SQL 刷屏 ES -->
         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-            <level>${log.level}</level>
+            <level>INFO</level>
         </filter>
         <encoder>
             <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
-            <!-- 设置字符集 -->
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -42,27 +30,17 @@
     <!--2. 输出到文档-->
     <!-- DEBUG 日志 -->
     <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 当前的日志文件存放路径 -->
         <file>${logging.path}/DEBUG.log</file>
-        <!-- 日志滚动策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 历史日志文件的存放路径和名称 -->
             <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
-            <!-- 日志文件最大的保存历史 数量-->
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
             <pattern>${FILE_LOG_PATTERN}</pattern>
         </encoder>
-        <!--日志文件最大的大小-->
-        <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
-        <!--            <MaxFileSize>10MB</MaxFileSize>-->
-        <!--        </triggeringPolicy>-->
-        <!-- 此日志文档只记录debug级别的 -->
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>DEBUG</level>
-            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMatch>ACCEPT</onMatch>
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
@@ -117,41 +95,14 @@
         </filter>
     </appender>
 
-    <!--
-      <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
-      以及指定<appender>。<logger>仅有一个name属性,
-      一个可选的level和一个可选的addtivity属性。
-      name:用来指定受此logger约束的某一个包或者具体的某一个类。
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-         还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
-         如果未设置此属性,那么当前logger将会继承上级的级别。
-      addtivity:是否向上级logger传递打印信息。默认是true。
-      <logger name="org.springframework.web" level="info"/>
-      <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-    -->
-
-    <!--
-      使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
-      第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
-      第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
-      【logging.level.org.mybatis=debug logging.level.dao=debug】
-     -->
-    <!-- mybatis显示sql,修改此处扫描包名 -->
-
-
-    <!--
-      root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-      不能设置为INHERITED或者同义词NULL。默认是DEBUG
-      可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-    -->
+    <!-- 降噪配置 -->
+    <logger name="springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator" level="WARN"/>
+    <logger name="org.springframework.security.web.DefaultSecurityFilterChain " level="WARN"/>
+    <logger name="com.netflix.config.sources.URLConfigurationSource " level="WARN"/>
+    <logger name="com.netflix.discovery" level="WARN"/>
+    <logger name="org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]" level="WARN"/>
 
     <!-- 4. 最终的策略 -->
-    <!-- 4.1 开发环境:打印控制台-->
-    <!--打印sql-->
-    <!--    <logger name="com.veryhappy.music.dao" level="debug"/>-->
-
-    <!--打印log-->
     <root level="info">
         <appender-ref ref="CONSOLE"/>
         <appender-ref ref="DEBUG_FILE"/>
@@ -160,14 +111,4 @@
         <appender-ref ref="ERROR_FILE"/>
     </root>
 
-    <!--   4.2 生产环境:输出到文档-->
-    <springProfile name="pro">
-        <root level="info">
-            <appender-ref ref="CONSOLE"/>
-            <appender-ref ref="DEBUG_FILE"/>
-            <appender-ref ref="INFO_FILE"/>
-            <appender-ref ref="ERROR_FILE"/>
-            <appender-ref ref="WARN_FILE"/>
-        </root>
-    </springProfile>
 </configuration>

+ 15 - 75
alien-second/src/main/resources/logback-spring.xml

@@ -1,41 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
-<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
-<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
-<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
-<!-- 该信息是由于设置了当配置文件变化时重新加载,所以每当达到扫描时间的时候就会检查配置文件是否错误。但是由于一般配置文件都放在了JAR包中,
-    而扫描的时候无法扫描JAR包内,因此会提示没有可以检查的文件,所以每隔一段时间就输出一次-->
-<configuration scan="false" scanPeriod="60 seconds" debug="true">
-    <contextName>logback-spring</contextName>
+<configuration scan="false" scanPeriod="60 seconds" debug="false">
 
-    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
     <!-- 定义全局参数常量 -->
     <property name="log.level" value="debug"/>
-    <property name="log.maxHistory" value="30"/><!-- 30表示30个 -->
-    <springProperty scope="context" name="logging.path" source="logging.path"  defaultValue="C:/project/ext/log"/>
-
+    <property name="log.maxHistory" value="30"/><!-- 30表示30天 -->
+    <springProperty scope="context" name="logging.path" source="logging.path" defaultValue="C:/project/ext/log"/>
     <!--输出文件前缀-->
     <property name="FILENAME" value="alien"/>
 
-    <!--0. 日志格式和颜色渲染 -->
-    <!-- 彩色日志依赖的渲染类 -->
-    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
-    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
-    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
-
     <!-- 文件输出格式 -->
     <property name="FILE_LOG_PATTERN" value="[%d{MM/dd HH:mm:ss.SSS}][%-10.10thread][%-5level][%-40.40c{1}:%5line]:[%15method] || %m%n"/>
-    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+    
+    <!-- 控制台输出格式:纯文本,无颜色,适合 Docker/EFK -->
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n"/>
 
     <!--1. 输出到控制台-->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <!-- 【关键】控制台只输出 INFO 及以上,防止 SQL 刷屏 ES -->
         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-            <level>${log.level}</level>
+            <level>INFO</level>
         </filter>
         <encoder>
             <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
-            <!-- 设置字符集 -->
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -43,27 +30,17 @@
     <!--2. 输出到文档-->
     <!-- DEBUG 日志 -->
     <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 当前的日志文件存放路径 -->
         <file>${logging.path}/DEBUG.log</file>
-        <!-- 日志滚动策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 历史日志文件的存放路径和名称 -->
             <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
-            <!-- 日志文件最大的保存历史 数量-->
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
             <pattern>${FILE_LOG_PATTERN}</pattern>
         </encoder>
-        <!--日志文件最大的大小-->
-        <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
-        <!--            <MaxFileSize>10MB</MaxFileSize>-->
-        <!--        </triggeringPolicy>-->
-        <!-- 此日志文档只记录debug级别的 -->
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>DEBUG</level>
-            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMatch>ACCEPT</onMatch>
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
@@ -118,41 +95,14 @@
         </filter>
     </appender>
 
-    <!--
-      <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
-      以及指定<appender>。<logger>仅有一个name属性,
-      一个可选的level和一个可选的addtivity属性。
-      name:用来指定受此logger约束的某一个包或者具体的某一个类。
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-         还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
-         如果未设置此属性,那么当前logger将会继承上级的级别。
-      addtivity:是否向上级logger传递打印信息。默认是true。
-      <logger name="org.springframework.web" level="info"/>
-      <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-    -->
-
-    <!--
-      使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
-      第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
-      第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
-      【logging.level.org.mybatis=debug logging.level.dao=debug】
-     -->
-    <!-- mybatis显示sql,修改此处扫描包名 -->
-
-
-    <!--
-      root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-      不能设置为INHERITED或者同义词NULL。默认是DEBUG
-      可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-    -->
+    <!-- 降噪配置 -->
+    <logger name="springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator" level="WARN"/>
+    <logger name="org.springframework.security.web.DefaultSecurityFilterChain " level="WARN"/>
+    <logger name="com.netflix.config.sources.URLConfigurationSource " level="WARN"/>
+    <logger name="com.netflix.discovery" level="WARN"/>
+    <logger name="org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]" level="WARN"/>
 
     <!-- 4. 最终的策略 -->
-    <!-- 4.1 开发环境:打印控制台-->
-    <!--打印sql-->
-    <!--    <logger name="com.veryhappy.music.dao" level="debug"/>-->
-
-    <!--打印log-->
     <root level="info">
         <appender-ref ref="CONSOLE"/>
         <appender-ref ref="DEBUG_FILE"/>
@@ -161,14 +111,4 @@
         <appender-ref ref="ERROR_FILE"/>
     </root>
 
-    <!--   4.2 生产环境:输出到文档-->
-    <springProfile name="pro">
-        <root level="info">
-            <appender-ref ref="CONSOLE"/>
-            <appender-ref ref="DEBUG_FILE"/>
-            <appender-ref ref="INFO_FILE"/>
-            <appender-ref ref="ERROR_FILE"/>
-            <appender-ref ref="WARN_FILE"/>
-        </root>
-    </springProfile>
 </configuration>

+ 15 - 75
alien-store-platform/src/main/resources/logback-spring.xml

@@ -1,41 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
-<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
-<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
-<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
-<!-- 该信息是由于设置了当配置文件变化时重新加载,所以每当达到扫描时间的时候就会检查配置文件是否错误。但是由于一般配置文件都放在了JAR包中,
-    而扫描的时候无法扫描JAR包内,因此会提示没有可以检查的文件,所以每隔一段时间就输出一次-->
-<configuration scan="false" scanPeriod="60 seconds" debug="true">
-    <contextName>logback-spring</contextName>
+<configuration scan="false" scanPeriod="60 seconds" debug="false">
 
-    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
     <!-- 定义全局参数常量 -->
     <property name="log.level" value="debug"/>
-    <property name="log.maxHistory" value="30"/><!-- 30表示30个 -->
-    <springProperty scope="context" name="logging.path" source="logging.path"  defaultValue="C:/project/ext/log"/>
-
+    <property name="log.maxHistory" value="30"/><!-- 30表示30天 -->
+    <springProperty scope="context" name="logging.path" source="logging.path" defaultValue="C:/project/ext/log"/>
     <!--输出文件前缀-->
     <property name="FILENAME" value="alien"/>
 
-    <!--0. 日志格式和颜色渲染 -->
-    <!-- 彩色日志依赖的渲染类 -->
-    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
-    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
-    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
-
     <!-- 文件输出格式 -->
     <property name="FILE_LOG_PATTERN" value="[%d{MM/dd HH:mm:ss.SSS}][%-10.10thread][%-5level][%-40.40c{1}:%5line]:[%15method] || %m%n"/>
-    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+    
+    <!-- 控制台输出格式:纯文本,无颜色,适合 Docker/EFK -->
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n"/>
 
     <!--1. 输出到控制台-->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <!-- 【关键】控制台只输出 INFO 及以上,防止 SQL 刷屏 ES -->
         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-            <level>${log.level}</level>
+            <level>INFO</level>
         </filter>
         <encoder>
             <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
-            <!-- 设置字符集 -->
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -43,27 +30,17 @@
     <!--2. 输出到文档-->
     <!-- DEBUG 日志 -->
     <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 当前的日志文件存放路径 -->
         <file>${logging.path}/DEBUG.log</file>
-        <!-- 日志滚动策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 历史日志文件的存放路径和名称 -->
             <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
-            <!-- 日志文件最大的保存历史 数量-->
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
             <pattern>${FILE_LOG_PATTERN}</pattern>
         </encoder>
-        <!--日志文件最大的大小-->
-        <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
-        <!--            <MaxFileSize>10MB</MaxFileSize>-->
-        <!--        </triggeringPolicy>-->
-        <!-- 此日志文档只记录debug级别的 -->
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>DEBUG</level>
-            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMatch>ACCEPT</onMatch>
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
@@ -118,41 +95,14 @@
         </filter>
     </appender>
 
-    <!--
-      <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
-      以及指定<appender>。<logger>仅有一个name属性,
-      一个可选的level和一个可选的addtivity属性。
-      name:用来指定受此logger约束的某一个包或者具体的某一个类。
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-         还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
-         如果未设置此属性,那么当前logger将会继承上级的级别。
-      addtivity:是否向上级logger传递打印信息。默认是true。
-      <logger name="org.springframework.web" level="info"/>
-      <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-    -->
-
-    <!--
-      使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
-      第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
-      第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
-      【logging.level.org.mybatis=debug logging.level.dao=debug】
-     -->
-    <!-- mybatis显示sql,修改此处扫描包名 -->
-
-
-    <!--
-      root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-      不能设置为INHERITED或者同义词NULL。默认是DEBUG
-      可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-    -->
+    <!-- 降噪配置 -->
+    <logger name="springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator" level="WARN"/>
+    <logger name="org.springframework.security.web.DefaultSecurityFilterChain " level="WARN"/>
+    <logger name="com.netflix.config.sources.URLConfigurationSource " level="WARN"/>
+    <logger name="com.netflix.discovery" level="WARN"/>
+    <logger name="org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]" level="WARN"/>
 
     <!-- 4. 最终的策略 -->
-    <!-- 4.1 开发环境:打印控制台-->
-    <!--打印sql-->
-    <!--    <logger name="com.veryhappy.music.dao" level="debug"/>-->
-
-    <!--打印log-->
     <root level="info">
         <appender-ref ref="CONSOLE"/>
         <appender-ref ref="DEBUG_FILE"/>
@@ -161,14 +111,4 @@
         <appender-ref ref="ERROR_FILE"/>
     </root>
 
-    <!--   4.2 生产环境:输出到文档-->
-    <springProfile name="pro">
-        <root level="info">
-            <appender-ref ref="CONSOLE"/>
-            <appender-ref ref="DEBUG_FILE"/>
-            <appender-ref ref="INFO_FILE"/>
-            <appender-ref ref="ERROR_FILE"/>
-            <appender-ref ref="WARN_FILE"/>
-        </root>
-    </springProfile>
 </configuration>

+ 9 - 5
alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java

@@ -87,7 +87,11 @@ public class CommonRatingController {
     @PostMapping("/addRating")
     public R<Integer> add(@RequestBody CommonRating commonRating) {
         log.info("CommonRatingController.add?commonRating={}", commonRating);
-        return R.data(commonRatingService.saveCommonRating(commonRating));
+        try {
+            return R.data(commonRatingService.saveCommonRating(commonRating));
+        } catch (IllegalArgumentException e) {
+            return R.fail(e.getMessage());
+        }
     }
 
     @ApiOperation("获取评价详情,和所有回复")
@@ -113,7 +117,7 @@ public class CommonRatingController {
 
     /**
      * 删除评价(并重新统计店铺评分)
-     * 
+     *
      * @param ratingId 评价id
      * @return 0:成功, 1:失败
      */
@@ -121,16 +125,16 @@ public class CommonRatingController {
     @GetMapping("/deleteRating")
     public R deleteRating(@RequestParam Long ratingId) {
         log.info("删除评价,ratingId={}", ratingId);
-        
+
         // 先获取评价信息(用于后续更新评分)
         CommonRating rating = commonRatingService.getById(ratingId);
         if (rating == null) {
             return R.fail("评价不存在");
         }
-        
+
         // 删除评价
         boolean b = commonRatingService.removeById(ratingId);
-        
+
         if (b) {
             // 删除成功后,重新统计店铺评分(仅店铺评价类型)
             if (rating.getBusinessType() != null && rating.getBusinessType() == 1) {

+ 26 - 26
alien-store/src/main/java/shop/alien/store/controller/StoreUserController.java

@@ -225,15 +225,16 @@ public class StoreUserController {
     @ApiImplicitParams({
             @ApiImplicitParam(name = "pageNum", value = "页数", dataType = "int", paramType = "query", required = false),
             @ApiImplicitParam(name = "pageSize", value = "页容", dataType = "int", paramType = "query", required = false),
-            @ApiImplicitParam(name = "id", value = "账号ID", dataType = "String", paramType = "query", required = false),
-            @ApiImplicitParam(name = "phone", value = "联系电话", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "id", value = "账号ID(主账号为自身id,子账号为子账号id)", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "phone", value = "联系电话(主账号/子账号自身电话)", dataType = "String", paramType = "query", required = false),
             @ApiImplicitParam(name = "status", value = "状态", dataType = "int", paramType = "query", required = false),
-            @ApiImplicitParam(name = "accountType", value = "账号类型:1-主账号,2-子账号", dataType = "int", paramType = "query", required = true)
+            @ApiImplicitParam(name = "accountType", value = "账号类型:1-主账号,2-子账号", dataType = "int", paramType = "query", required = true),
+            @ApiImplicitParam(name = "mainAccountId", value = "主账号ID(子账号列表时支持模糊查询)", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "mainAccountPhone", value = "主账号联系电话(子账号列表时支持模糊查询)", dataType = "String", paramType = "query", required = false)
     })
-    public R<IPage<StoreUserVo>> getStoreUserList(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(required = false) String id, @RequestParam(required = false) String phone, @RequestParam(required = false) Integer status, @RequestParam(required = true) Integer accountType) {
-        log.info("StoreUserController.getStoreUserList?pageNum={},pageSize={},id={},phone={},status={},accountType={}", pageNum, pageSize, id, phone, status, accountType);
-        R<IPage<StoreUserVo>> storeUserVoR = storeUserService.getStoreUserList(pageNum, pageSize, id, phone, status, accountType);
-        return storeUserVoR;
+    public R<IPage<StoreUserVo>> getStoreUserList(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(required = false) String id, @RequestParam(required = false) String phone, @RequestParam(required = false) Integer status, @RequestParam(required = true) Integer accountType, @RequestParam(required = false) String mainAccountId, @RequestParam(required = false) String mainAccountPhone) {
+        log.info("StoreUserController.getStoreUserList?pageNum={},pageSize={},id={},phone={},status={},accountType={},mainAccountId={},mainAccountPhone={}", pageNum, pageSize, id, phone, status, accountType, mainAccountId, mainAccountPhone);
+        return storeUserService.getStoreUserList(pageNum, pageSize, id, phone, status, accountType, mainAccountId, mainAccountPhone);
     }
 
     /**
@@ -280,29 +281,25 @@ public class StoreUserController {
         return R.success("初始化成功");
     }
 
-    /**
-     * web端删除商家端用户
-     */
-    @ApiOperation("web端删除商家端用户")
-    @ApiOperationSupport(order = 7)
-    @DeleteMapping("/deleteStoreUser")
-    public R<StoreUserVo> deleteStoreUser(@RequestParam(value = "id") String id) {
-        log.info("StoreUserController.deleteStoreUser?id={}", id);
-        return storeUserService.deleteStoreUser(id);
-    }
+
 
     /**
-     * web端切换商家端用户
+     * web端切换商家端用户状态  (主账号有子账号时禁止禁用;子账号无限制;返回更新后状态供前端及时展示)
      */
-    @ApiOperation("web端切换商家端用户")
+    @ApiOperation("web端切换商家端用户状态(主账号有子账号时禁止禁用;返回更新后状态供前端及时展示)")
     @ApiOperationSupport(order = 7)
     @PutMapping("/switchingStates")
-    public R<StoreUserVo> switchingStates(@RequestBody StoreUser storeUser) {
+    public R<StoreUserVo> switchingStates(@RequestBody StoreUserVo storeUser) {
         log.info("StoreUserController.switchingStates?storeUser={}", storeUser);
-        storeUserService.switchingStates(storeUser);
-        return R.success("切换成功");
+        try {
+            return storeUserService.switchingStates(storeUser);
+        } catch (Exception e) {
+            log.warn("StoreUserController.switchingStates 校验或切换失败: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        }
     }
 
+
     @ApiOperation(value = "web端导出商家端账号相关信息")
     @ApiOperationSupport(order = 6)
     @GetMapping("/exportExcel")
@@ -423,10 +420,13 @@ public class StoreUserController {
      */
     @ApiOperation("手动删除商家账号及店铺")
     @PostMapping("/deleteStoreAccountInfo")
-    public R<StoreUserVo> deleteStoreAccountInfo(@RequestBody StoreUserVo storeUserVo) {
-        log.info("StoreUserController.deleteStoreAccountInfo?storeUserVo={}", storeUserVo);
-        storeUserService.deleteStoreAccountInfo(storeUserVo);
-        return R.success("删除成功");
+    public R<String> deleteStoreAccountInfo(@RequestBody StoreUserVo storeUserVo) {
+        log.info("StoreUserController.deleteStoreAccountInfo?phone={}, id={}", storeUserVo.getPhone(), storeUserVo.getSubAccountId());
+        String errMsg = storeUserService.deleteStoreAccountInfo(storeUserVo);
+        if (errMsg == null || errMsg.isEmpty()) {
+            return R.success("删除成功");
+        }
+        return R.fail(errMsg);
     }
 
     /**

+ 1 - 1
alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java

@@ -15,7 +15,7 @@ import shop.alien.entity.store.vo.CommonRatingVo;
 public interface CommonRatingService extends IService<CommonRating> {
 
     /**
-     * 新增评价
+     * 新增人员评价
      *
      * @param commonRating 评价信息
      * @return 是否成功

+ 9 - 0
alien-store/src/main/java/shop/alien/store/service/LifeDiscountCouponStoreFriendService.java

@@ -60,4 +60,13 @@ public interface LifeDiscountCouponStoreFriendService extends IService<LifeDisco
     LifeDiscountCouponFriendRuleVo getRuleById(String id);
 
     List<LifeDiscountCouponFriendRuleVo> getReceivedSendFriendCouponList(String storeUserId, String friendStoreUserId,String storeName);
+
+    /**
+     * 好评送券:用户对店铺好评且通过AI审核通过后,将店铺可用券发放到用户卡包
+     *
+     * @param userId  评价用户ID(life用户)
+     * @param storeId 店铺ID(businessId)
+     * @return 发放的优惠券数量,0表示未发放
+     */
+    int issueCouponForGoodRating(Integer userId, Integer storeId);
 }

+ 5 - 9
alien-store/src/main/java/shop/alien/store/service/StoreUserService.java

@@ -100,7 +100,7 @@ public interface StoreUserService extends IService<StoreUser> {
      *
      * @return boolean
      */
-    R<IPage<StoreUserVo>> getStoreUserList(int pageNum, int pageSize, String id, String phone, Integer status,Integer accountType);
+    R<IPage<StoreUserVo>> getStoreUserList(int pageNum, int pageSize, String id, String phone, Integer status, Integer accountType, String mainAccountId, String mainAccountPhone);
 
 
     /**
@@ -117,19 +117,14 @@ public interface StoreUserService extends IService<StoreUser> {
      */
     void resetStoreUserPassword(StoreUser storeUser);
 
-    /**
-     * web端编辑用户列表
-     *
-     * @return boolean
-     */
-    R<StoreUserVo> deleteStoreUser(String id);
+
 
     /**
      * web端切换商家端用户状态
      *
      * @return boolean
      */
-    void switchingStates(StoreUser storeUser);
+    R<StoreUserVo> switchingStates(StoreUserVo storeUser);
 
 
     /**
@@ -179,8 +174,9 @@ public interface StoreUserService extends IService<StoreUser> {
 
     /**
      * 手动删除商家账号及店铺 进行校验
+     * @return 成功返回 null;失败返回具体原因:当前账号下存在店铺 禁止删除 / 当前账号下存在子账号禁止删除 / 删除失败
      */
-    void deleteStoreAccountInfo(StoreUserVo storeUserVo);
+    String deleteStoreAccountInfo(StoreUserVo storeUserVo);
 
     /**
      * 获取主键集合

+ 1 - 0
alien-store/src/main/java/shop/alien/store/service/impl/BarPerformanceServiceImpl.java

@@ -534,6 +534,7 @@ public class BarPerformanceServiceImpl implements BarPerformanceService {
         performerVo.setAvatar(staffConfig.getStaffImage());
         performerVo.setName(staffConfig.getName());
         performerVo.setStyle(staffConfig.getStaffPosition());
+        performerVo.setTag(staffConfig.getTag());
         return performerVo;
     }
 

+ 18 - 1
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -28,6 +28,7 @@ import shop.alien.mapper.*;
 import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
+import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.store.util.CommonConstant;
 import shop.alien.store.util.ai.AiContentModerationUtil;
 import shop.alien.store.util.ai.AiVideoModerationUtil;
@@ -79,6 +80,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
     @Qualifier("commonVideoTaskExecutor")
     private final ExecutorService commonVideoTaskExecutor;
     private final AiVideoModerationUtil aiVideoModerationUtil;
+    private final LifeDiscountCouponStoreFriendService lifeDiscountCouponStoreFriendService;
 
     public static final List<String> SERVICES_LIST = ImmutableList.of(
             TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService(),
@@ -86,7 +88,11 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
     );
 
 
-
+    /**
+     *  新增评价ai审核接口通过后将券发到卡包中
+     * @param commonRating 评价信息
+     * @return
+     */
     @Override
     public Integer saveCommonRating(CommonRating commonRating) {
         // 1. 文本审核 + 视频审核(评价有图片和视频)
@@ -99,6 +105,17 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                 commonRating.setScoreThree(parse.getDouble("scoreThree"));
             }
             int i = this.save(commonRating) ? 0 : 1;
+            if (i == 0 && commonRating.getBusinessType() != null && commonRating.getBusinessType() == RatingBusinessTypeEnum.STORE_RATING.getBusinessType()) {
+                Double score = commonRating.getScore();
+                if (score != null && score >= 4.5 && commonRating.getUserId() != null && commonRating.getBusinessId() != null) {
+                    try {
+
+                        lifeDiscountCouponStoreFriendService.issueCouponForGoodRating(Math.toIntExact(commonRating.getUserId()), commonRating.getBusinessId());
+                    } catch (Exception ex) {
+                        log.warn("CommonRatingService.saveCommonRating 好评送券异常 userId={}, storeId={}, msg={}", commonRating.getUserId(), commonRating.getBusinessId(), ex.getMessage());
+                    }
+                }
+            }
             // 一次遍历完成分类,避免多次流式处理
             Map<String, List<String>> urlCategoryMap = StoreRenovationRequirementServiceImpl.classifyUrls(Arrays.asList(commonRating.getImageUrls().split(",")));
             AiContentModerationUtil.AuditResult auditResult = new AiContentModerationUtil.AuditResult(true, "");

+ 57 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponStoreFriendServiceImpl.java

@@ -524,4 +524,61 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
             return new ArrayList<>();
         }
     }
+
+    /**
+     *  商家优惠券活动
+     * 好评送券:用户对店铺好评且通过AI审核后,将店铺可用券发放到用户卡包
+     */
+    @Override
+    public int issueCouponForGoodRating(Integer userId, Integer storeId) {
+        if (userId == null || storeId == null) {
+            return 0;
+        }
+        LocalDate currentDate = LocalDate.now();
+        LambdaQueryWrapper<LifeDiscountCoupon> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(LifeDiscountCoupon::getStoreId, String.valueOf(storeId))
+                .eq(LifeDiscountCoupon::getGetStatus, DiscountCouponEnum.CAN_GET.getValue())
+                .ge(LifeDiscountCoupon::getValidDate, currentDate)
+                .gt(LifeDiscountCoupon::getSingleQty, 0);
+        List<LifeDiscountCoupon> coupons = lifeDiscountCouponMapper.selectList(queryWrapper);
+        if (CollectionUtils.isEmpty(coupons)) {
+            return 0;
+        }
+        int granted = 0;
+        int commenterUserId = userId.intValue();
+        StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        LifeUser lifeUser = lifeUserMapper.selectById(commenterUserId);
+        for (LifeDiscountCoupon coupon : coupons) {
+            try {
+                LifeDiscountCouponUser lifeDiscountCouponUser = new LifeDiscountCouponUser();
+                lifeDiscountCouponUser.setCouponId(coupon.getId());
+                lifeDiscountCouponUser.setUserId(commenterUserId);
+                lifeDiscountCouponUser.setReceiveTime(new Date());
+                lifeDiscountCouponUser.setExpirationTime(coupon.getValidDate());
+                lifeDiscountCouponUser.setStatus(Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
+                lifeDiscountCouponUser.setDeleteFlag(0);
+                lifeDiscountCouponUserMapper.insert(lifeDiscountCouponUser);
+                coupon.setSingleQty(coupon.getSingleQty() - 1);
+                lifeDiscountCouponMapper.updateById(coupon);
+                granted++;
+            } catch (Exception e) {
+                // 单张发放失败不影响其余
+            }
+        }
+        if (granted > 0 && lifeUser != null && storeInfo != null) {
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setSenderId("system");
+            lifeNotice.setReceiverId("user_" + lifeUser.getUserPhone());
+            String text = "您对好友店铺「" + storeInfo.getStoreName() + "」的好评已通过审核,已为您发放" + granted + "张优惠券,快去我的券包查看吧~";
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("message", text);
+            lifeNotice.setContext(jsonObject.toJSONString());
+            lifeNotice.setNoticeType(1);
+            lifeNotice.setTitle("好评送券");
+            lifeNotice.setIsRead(0);
+            lifeNotice.setDeleteFlag(0);
+            lifeNoticeMapper.insert(lifeNotice);
+        }
+        return granted;
+    }
 }

+ 457 - 236
alien-store/src/main/java/shop/alien/store/service/impl/StoreUserServiceImpl.java

@@ -252,12 +252,10 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserMapper, StoreUser
     public Map<String, String> changePhoneVerification(String phone, String oldPassword, String verificationCode) {
         Map<String, String> changePhoneMap = new HashMap<>();
         if (oldPassword != null && !oldPassword.equals("")) {
-            LambdaQueryWrapper<StoreUser> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
-            userLambdaQueryWrapper.eq(StoreUser::getPhone, phone);
-            StoreUser storeUser = this.getOne(userLambdaQueryWrapper);
-            // 由于password字段使用了EncryptTypeHandler,查询时密码会被自动解密
-            // 所以这里直接比较解密后的密码和用户输入的明文密码
-            if (storeUser != null && storeUser.getPassword() != null && storeUser.getPassword().equals(oldPassword)) {
+            LambdaUpdateWrapper<StoreUser> userLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
+            userLambdaUpdateWrapper.eq(StoreUser::getPhone, phone);
+            StoreUser storeUser = this.getOne(userLambdaUpdateWrapper);
+            if (storeUser.getPassword().equals(oldPassword)) {
                 changePhoneMap.put("passwordStatus", "1");
                 return changePhoneMap;
             } else {
@@ -405,151 +403,215 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserMapper, StoreUser
     }
 
     @Override
-    public R<IPage<StoreUserVo>> getStoreUserList(int pageNum, int pageSize, String id, String phone, Integer status,Integer accountType) {
-
+    public R<IPage<StoreUserVo>> getStoreUserList(int pageNum, int pageSize, String id, String phone, Integer status, Integer accountType, String mainAccountId, String mainAccountPhone) {
         IPage<StoreUser> page = new Page<>(pageNum, pageSize);
         IPage<StoreUserVo> storeUserVoIPage = new Page<>();
-        // 查询子账号(accountType == 2)
+        // 子账号分支:以 store_platform_user_role 每条记录为一行(不去重 userId),同一子账号在不同店铺展示多行,每行带主账号 id
         if (accountType == 2) {
-            // 构建子账号查询条件
-            LambdaQueryWrapper<StoreUser> subAccountWrapper = new LambdaQueryWrapper<>();
-            subAccountWrapper.eq(StoreUser::getAccountType, 2) // 子账号类型
-                    .eq(status != null, StoreUser::getStatus, status);
-
-            // 先通过中间表storePlatformUserRole获取所有子账号的userId列表
             LambdaQueryWrapper<StorePlatformUserRole> roleWrapper = new LambdaQueryWrapper<>();
+            roleWrapper.eq(StorePlatformUserRole::getDeleteFlag, 0);
             List<StorePlatformUserRole> userRoles = storePlatformUserRoleMapper.selectList(roleWrapper);
-            List<Integer> subAccountUserIds = null;
-            if (!CollectionUtils.isEmpty(userRoles)) {
-                // 提取所有在中间表中的子账号userId
-                subAccountUserIds = userRoles.stream()
-                        .map(StorePlatformUserRole::getUserId)
-                        .distinct()
-                        .collect(Collectors.toList());
+            if (CollectionUtils.isEmpty(userRoles)) {
+                storeUserVoIPage.setRecords(new ArrayList<>());
+                storeUserVoIPage.setTotal(0);
+                storeUserVoIPage.setCurrent(pageNum);
+                storeUserVoIPage.setSize(pageSize);
+                return R.data(storeUserVoIPage);
             }
 
-            // 当id不为空时,对ID进行模糊查询(将ID转换为字符串进行模糊匹配)
-            if (StringUtils.isNotEmpty(id)) {
-                // 使用apply方法,将ID转换为字符串进行模糊查询
-                subAccountWrapper.apply("CAST(id AS CHAR) LIKE {0}", "%" + id + "%");
+            // 主账号ID 模糊:只保留 platform 中 store_id 属于这些主账号店铺的 role 记录
+            if (StringUtils.isNotEmpty(mainAccountId)) {
+                LambdaQueryWrapper<StoreUser> mainById = new LambdaQueryWrapper<>();
+                mainById.eq(StoreUser::getAccountType, 1).eq(StoreUser::getDeleteFlag, 0)
+                        .apply("CAST(id AS CHAR) LIKE {0}", "%" + mainAccountId + "%");
+                List<StoreUser> mains = storeUserMapper.selectList(mainById);
+                List<Integer> storeIds = mains == null ? Collections.emptyList() : mains.stream().map(StoreUser::getStoreId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+                if (storeIds.isEmpty()) {
+                    userRoles = Collections.emptyList();
+                } else {
+                    userRoles = userRoles.stream().filter(r -> storeIds.contains(r.getStoreId())).collect(Collectors.toList());
+                }
             }
-
-            // 当phone不为空时,对子账号和主账号电话进行模糊查询,并关联storePlatformUserRole
-            if (StringUtils.isNotEmpty(phone)) {
-                if (subAccountUserIds != null && !subAccountUserIds.isEmpty()) {
-                    // 先查询主账号phone匹配的主账号ID列表
-                    LambdaQueryWrapper<StoreUser> mainAccountWrapper = new LambdaQueryWrapper<>();
-                    mainAccountWrapper.eq(StoreUser::getAccountType, 1)
-                            .like(StoreUser::getPhone, phone);
-                    List<StoreUser> mainAccounts = storeUserMapper.selectList(mainAccountWrapper);
-                    List<Integer> mainAccountIds = mainAccounts.stream()
-                            .map(StoreUser::getId)
-                            .collect(Collectors.toList());
-
-                    // 限制查询范围:只查询在中间表中存在的子账号
-                    subAccountWrapper.in(StoreUser::getId, subAccountUserIds);
-                    // 查询条件:在中间表范围内的子账号中,子账号phone匹配 OR 主账号phone匹配
-                    subAccountWrapper.and(wrapper -> {
-                        // 子账号本身的phone模糊查询
-                        wrapper.like(StoreUser::getPhone, phone);
-                        // 或者主账号phone匹配(通过subAccountId关联到主账号)
-                        if (!CollectionUtils.isEmpty(mainAccountIds)) {
-                            wrapper.or().in(StoreUser::getSubAccountId, mainAccountIds);
-                        }
-                    });
+            // 主账号联系电话 模糊:只保留 platform 中 store_id 属于这些主账号店铺的 role 记录
+            if (StringUtils.isNotEmpty(mainAccountPhone) && !userRoles.isEmpty()) {
+                LambdaQueryWrapper<StoreUser> mainByPhone = new LambdaQueryWrapper<>();
+                mainByPhone.eq(StoreUser::getAccountType, 1).eq(StoreUser::getDeleteFlag, 0).like(StoreUser::getPhone, mainAccountPhone);
+                List<StoreUser> mains = storeUserMapper.selectList(mainByPhone);
+                List<Integer> storeIds = mains == null ? Collections.emptyList() : mains.stream().map(StoreUser::getStoreId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+                if (storeIds.isEmpty()) {
+                    userRoles = Collections.emptyList();
                 } else {
-                    // 如果没有中间表数据,先查询主账号phone匹配的主账号ID
-                    LambdaQueryWrapper<StoreUser> mainAccountWrapper = new LambdaQueryWrapper<>();
-                    mainAccountWrapper.eq(StoreUser::getAccountType, 1)
-                            .like(StoreUser::getPhone, phone);
-                    List<StoreUser> mainAccounts = storeUserMapper.selectList(mainAccountWrapper);
-                    List<Integer> mainAccountIds = mainAccounts.stream()
-                            .map(StoreUser::getId)
-                            .collect(Collectors.toList());
-
-                    // 查询条件:子账号phone匹配 OR 主账号phone匹配
-                    if (!CollectionUtils.isEmpty(mainAccountIds)) {
-                        subAccountWrapper.and(wrapper -> {
-                            wrapper.like(StoreUser::getPhone, phone)
-                                    .or().in(StoreUser::getSubAccountId, mainAccountIds);
-                        });
-                    } else {
-                        // 如果没有主账号匹配,只查询子账号phone
-                        subAccountWrapper.like(StoreUser::getPhone, phone);
-                    }
+                    userRoles = userRoles.stream().filter(r -> storeIds.contains(r.getStoreId())).collect(Collectors.toList());
                 }
+            }
+
+            if (userRoles.isEmpty()) {
+                storeUserVoIPage.setRecords(new ArrayList<>());
+                storeUserVoIPage.setTotal(0);
+                storeUserVoIPage.setCurrent(pageNum);
+                storeUserVoIPage.setSize(pageSize);
+                return R.data(storeUserVoIPage);
+            }
+
+            // 按 (userId, role_id) 去重,同一用户同一角色只保留一条(保留 id 最小的一条)
+            userRoles = userRoles.stream()
+                    .sorted(Comparator.comparing(StorePlatformUserRole::getId, Comparator.nullsLast(Comparator.naturalOrder())))
+                    .collect(Collectors.toMap(
+                            r -> (r.getUserId() != null ? r.getUserId() : 0) + "_" + (r.getRoleId() != null ? r.getRoleId() : ""),
+                            r -> r,
+                            (a, b) -> a
+                    ))
+                    .values().stream()
+                    .sorted(Comparator.comparing(StorePlatformUserRole::getId, Comparator.nullsLast(Comparator.naturalOrder())))
+                    .collect(Collectors.toList());
+
+            // 按子账号 id/phone/status 过滤:批量查 store_user,只保留对应子账号满足条件的 role
+            List<Integer> distinctUserIds = userRoles.stream().map(StorePlatformUserRole::getUserId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+            List<StoreUser> subAccountList = distinctUserIds.isEmpty() ? Collections.emptyList() : storeUserMapper.selectBatchIds(distinctUserIds);
+            // 根据 status 参数决定子账号池:选「注销中/已注销」时只保留对应状态;否则排除注销中、已注销
+            Map<Integer, StoreUser> subAccountMap;
+            if (status != null && (status == -1 || status == 2)) {
+                final int filterStatus = status;
+                subAccountMap = subAccountList == null ? new HashMap<>() : subAccountList.stream()
+                        .filter(u -> u.getDeleteFlag() != null && u.getDeleteFlag() == 0)
+                        .filter(u -> {
+                            if (filterStatus == -1) return u.getStatus() != null && u.getStatus() == -1;
+                            return (u.getStatus() != null && u.getStatus() == 2) || (u.getLogoutFlag() != null && u.getLogoutFlag() == 1);
+                        })
+                        .collect(Collectors.toMap(StoreUser::getId, u -> u, (a, b) -> a));
             } else {
-                // phone为空时,如果中间表有数据,限制只查询中间表中的子账号
-                if (subAccountUserIds != null && !subAccountUserIds.isEmpty()) {
-                    subAccountWrapper.in(StoreUser::getId, subAccountUserIds);
+                subAccountMap = subAccountList == null ? new HashMap<>() : subAccountList.stream()
+                        .filter(u -> u.getDeleteFlag() != null && u.getDeleteFlag() == 0
+                                && (u.getLogoutFlag() == null || u.getLogoutFlag() == 0)
+                                && (u.getStatus() == null || (u.getStatus() != -1 && u.getStatus() != 2)))
+                        .collect(Collectors.toMap(StoreUser::getId, u -> u, (a, b) -> a));
+            }
+            List<StorePlatformUserRole> filteredRoles = new ArrayList<>();
+            for (StorePlatformUserRole role : userRoles) {
+                StoreUser subAccount = subAccountMap.get(role.getUserId());
+                if (subAccount == null) continue;
+                if (StringUtils.isNotEmpty(id) && (subAccount.getId() == null || !String.valueOf(subAccount.getId()).contains(id)))
+                    continue;
+                if (StringUtils.isNotEmpty(phone) && (subAccount.getPhone() == null || !subAccount.getPhone().contains(phone)))
+                    continue;
+                // 状态筛选:选「注销中/已注销」时以 store_user 为准(已由 subAccountMap 限定);否则以 role.status 为准
+                int rowStatus;
+                if (status != null && (status == -1 || status == 2)) {
+                    rowStatus = (subAccount.getStatus() != null && subAccount.getStatus() == -1) ? -1
+                            : ((subAccount.getStatus() != null && subAccount.getStatus() == 2) || (subAccount.getLogoutFlag() != null && subAccount.getLogoutFlag() == 1)) ? 2
+                            : (subAccount.getStatus() != null ? subAccount.getStatus() : 0);
+                } else {
+                    rowStatus = role.getStatus() != null ? role.getStatus() : (subAccount.getStatus() != null ? subAccount.getStatus() : 0);
                 }
+                if (status != null && !status.equals(rowStatus)) continue;
+                filteredRoles.add(role);
+            }
+            filteredRoles.sort(Comparator.comparing(StorePlatformUserRole::getId, Comparator.nullsLast(Comparator.naturalOrder())));
+
+            int total = filteredRoles.size();
+            storeUserVoIPage.setTotal(total);
+            storeUserVoIPage.setCurrent(pageNum);
+            storeUserVoIPage.setSize(pageSize);
+            int from = (pageNum - 1) * pageSize;
+            if (from >= total) {
+                storeUserVoIPage.setRecords(new ArrayList<>());
+                return R.data(storeUserVoIPage);
+            }
+            int to = Math.min(from + pageSize, total);
+            List<StorePlatformUserRole> pageRoles = filteredRoles.subList(from, to);
+
+            // 批量查本页主账号,避免 N+1
+            List<Integer> pageStoreIds = pageRoles.stream().map(StorePlatformUserRole::getStoreId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+            Map<Integer, StoreUser> mainAccountMap = new HashMap<>();
+            if (!pageStoreIds.isEmpty()) {
+                LambdaQueryWrapper<StoreUser> mainW = new LambdaQueryWrapper<>();
+                mainW.eq(StoreUser::getAccountType, 1).eq(StoreUser::getDeleteFlag, 0).in(StoreUser::getStoreId, pageStoreIds);
+                List<StoreUser> mains = storeUserMapper.selectList(mainW);
+                if (mains != null) mains.forEach(m -> mainAccountMap.put(m.getStoreId(), m));
             }
 
-            subAccountWrapper.orderByDesc(StoreUser::getCreatedTime);
-
-            IPage<StoreUser> subAccountsPage = storeUserMapper.selectPage(page, subAccountWrapper);
-            BeanUtils.copyProperties(subAccountsPage, storeUserVoIPage);
-            
             List<StoreUserVo> resultRecords = new ArrayList<>();
-            for (StoreUser subAccount : subAccountsPage.getRecords()) {
+            for (StorePlatformUserRole role : pageRoles) {
+                StoreUser subAccount = subAccountMap.get(role.getUserId());
+                if (subAccount == null) continue;
+                StoreUser mainAccount = role.getStoreId() == null ? null : mainAccountMap.get(role.getStoreId());
                 StoreUserVo storeUserVo = new StoreUserVo();
                 BeanUtils.copyProperties(subAccount, storeUserVo);
-                
-                // 查询主账号信息(通过 subAccountId 关联)
-                if (subAccount.getSubAccountId() != null) {
-                    StoreUser mainAccount = storeUserMapper.selectById(subAccount.getSubAccountId());
-                    if (mainAccount != null) {
-                        // 设置主账号联系电话
-                        storeUserVo.setParentAccountPhone(mainAccount.getPhone());
-                        storeUserVo.setParentAccountId(mainAccount.getId());
-                        storeUserVo.setParentAccountName(mainAccount.getName());
-                    }
+                // 分页列表:账号ID、parentAccountId 均为主账号 id(与主账号联系电话对应),子账号 id 放入 subAccountId
+                Integer mainId = mainAccount != null ? mainAccount.getId() : subAccount.getSubAccountId();
+                storeUserVo.setId(mainId != null ? mainId : subAccount.getId());
+                storeUserVo.setSubAccountId(subAccount.getId());
+                storeUserVo.setParentAccountId(mainId);
+                int rowStatus = role.getStatus() != null ? role.getStatus() : (subAccount.getStatus() != null ? subAccount.getStatus() : 0);
+                storeUserVo.setStatus(rowStatus);
+                storeUserVo.setSwitchStatus(rowStatus == 0);
+                storeUserVo.setPhone(subAccount.getPhone());
+                if (mainAccount != null) {
+                    storeUserVo.setParentAccountPhone(mainAccount.getPhone());
+                    storeUserVo.setParentAccountName(mainAccount.getName());
                 }
-                
-                // 不返回密码
                 storeUserVo.setPassword(null);
                 storeUserVo.setPayPassword(null);
-                
                 resultRecords.add(storeUserVo);
             }
-            
             storeUserVoIPage.setRecords(resultRecords);
             return R.data(storeUserVoIPage);
         }
 
-        // 查询主账号(accountType == 1)
+        // 主账号分支:直接查库返回最新 status,主账号页面展示子账号数量(按 store_platform_user_role 统计);排除注销中、已注销
         LambdaQueryWrapper<StoreUser> storeUserLambdaQueryWrapper = new LambdaQueryWrapper<>();
-        storeUserLambdaQueryWrapper.like(!StringUtils.isEmpty(id), StoreUser::getId, id)
-                .like(!StringUtils.isEmpty(phone), StoreUser::getPhone, phone)
+        storeUserLambdaQueryWrapper.eq(StoreUser::getDeleteFlag, 0)
+                .like(StringUtils.isNotEmpty(id), StoreUser::getId, id)
+                .like(StringUtils.isNotEmpty(phone), StoreUser::getPhone, phone)
                 .eq(status != null, StoreUser::getStatus, status)
-                .eq(StoreUser::getAccountType, accountType)
+                .eq(StoreUser::getAccountType, 1)
+                .and(w -> w.isNull(StoreUser::getLogoutFlag).or().eq(StoreUser::getLogoutFlag, 0))
+                .and(w -> w.isNull(StoreUser::getStatus).or().in(StoreUser::getStatus, 0, 1))
                 .orderByDesc(StoreUser::getCreatedTime);
-        
+
         IPage<StoreUser> storeUsers = storeUserMapper.selectPage(page, storeUserLambdaQueryWrapper);
         BeanUtils.copyProperties(storeUsers, storeUserVoIPage);
-        
-        for (StoreUser storeUser : storeUserVoIPage.getRecords()) {
-            // 不返回密码
-            storeUser.setPassword(null);
-            storeUser.setPayPassword(null);
-
-            // 如果是主账号,统计子账号数量
-            if (accountType == 1) {
-                List<StoreUser> childAccounts = getChildAccountsByParentId(String.valueOf(storeUser.getId()));
-                Integer childCount = childAccounts != null ? childAccounts.size() : 0;
-                storeUser.setChildAccountCount(childCount);
-                if (childAccounts != null && !childAccounts.isEmpty()) {
-                    List<String> childPhoneNumbers = childAccounts.stream()
-                            .map(StoreUser::getPhone)
-                            .collect(Collectors.toList());
-                    storeUser.setChildPhoneNumbers(childPhoneNumbers);
-                }
+
+        List<StoreUserVo> resultRecords = new ArrayList<>();
+        for (StoreUser storeUser : storeUsers.getRecords()) {
+            StoreUserVo vo = new StoreUserVo();
+            BeanUtils.copyProperties(storeUser, vo);
+            vo.setPassword(null);
+            vo.setPayPassword(null);
+            vo.setSwitchStatus(storeUser.getStatus() != null && storeUser.getStatus() == 0);
+            int childCount = countSubAccountUserIdsByStoreId(storeUser.getStoreId(), storeUser.getId());
+            vo.setChildAccountCount(childCount);
+            List<Integer> childUserIds = getSubAccountUserIdsByStoreId(storeUser.getStoreId(), storeUser.getId());
+            if (!childUserIds.isEmpty()) {
+                List<StoreUser> childUsers = storeUserMapper.selectBatchIds(childUserIds);
+                vo.setChildPhoneNumbers(childUsers == null ? Collections.emptyList() : childUsers.stream().map(StoreUser::getPhone).collect(Collectors.toList()));
             }
+            resultRecords.add(vo);
         }
-
+        storeUserVoIPage.setRecords(resultRecords);
         return R.data(storeUserVoIPage);
     }
 
+    /**
+     * 按 store_platform_user_role 统计该店铺下子账号数量(排除主账号自身,按 distinct user_id 计)
+     */
+    private int countSubAccountUserIdsByStoreId(Integer storeId, Integer excludeUserId) {
+        List<Integer> userIds = getSubAccountUserIdsByStoreId(storeId, excludeUserId);
+        return userIds == null ? 0 : userIds.size();
+    }
+
+    /**
+     * 按 store_platform_user_role 查询该店铺下子账号 userId 列表(排除主账号自身)
+     */
+    private List<Integer> getSubAccountUserIdsByStoreId(Integer storeId, Integer excludeUserId) {
+        if (storeId == null) return Collections.emptyList();
+        LambdaQueryWrapper<StorePlatformUserRole> w = new LambdaQueryWrapper<>();
+        w.eq(StorePlatformUserRole::getStoreId, storeId).eq(StorePlatformUserRole::getDeleteFlag, 0);
+        if (excludeUserId != null) w.ne(StorePlatformUserRole::getUserId, excludeUserId);
+        List<StorePlatformUserRole> list = storePlatformUserRoleMapper.selectList(w);
+        return list == null ? Collections.emptyList() : list.stream().map(StorePlatformUserRole::getUserId).distinct().collect(Collectors.toList());
+    }
+
     @Override
     public R<StoreUserVo> editStoreUser(StoreUser storeUser) {
         storeUserMapper.updateById(storeUser);
@@ -567,124 +629,226 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserMapper, StoreUser
         storeUserMapper.updateById(storeUserById);
     }
 
+
+    /**
+     * web端切换商家端用户状态(主账号底下有子账号时禁止禁用;子账号无限制)。
+     * 若同一账号既是主账号又是子账号:禁用时只禁用子账号角色,主账号保持启用。
+     */
     @Override
-    public R<StoreUserVo> deleteStoreUser(String id) {
-        StoreUser storeUser = storeUserMapper.selectById(id);
-        String phone = storeUser.getPhone();
-        Integer storeId = storeUser.getStoreId();
-
-        // 判断该账号是否关联店铺
-        if (ObjectUtils.isNotEmpty(storeId)) {
-            List<StoreInfo> storeInfos = storeInfoMapper.selectList(new LambdaQueryWrapper<StoreInfo>().eq(StoreInfo::getId, storeId).eq(StoreInfo::getDeleteFlag, 0).eq(StoreInfo::getLogoutFlag, 0));
-            if (ObjectUtils.isNotEmpty(storeInfos)) {
-                return R.fail("请删除店铺后再删除账号");
+    public R<StoreUserVo> switchingStates(StoreUserVo storeUserParam) {
+        // 默认传 0 表示执行禁用(状态改为 1),分页列表展示为禁用;传 1 表示执行启用(状态改为 0),分页列表展示为启用
+        int currentStatus = storeUserParam.getStatus() != null ? storeUserParam.getStatus() : 0;
+        int newStatus = (currentStatus == 0) ? 1 : 0;
+        boolean isMainAccount = storeUserParam.getAccountType() != null && storeUserParam.getAccountType() == 1;
+
+        if(storeUserParam.getAccountType()==2){
+            StoreUser storeUser = storeUserMapper.selectById(storeUserParam.getSubAccountId());
+            if (storeUser == null) {
+                throw new RuntimeException("用户不存在");
             }
-        }
+            // 统计该用户在 platform 中未删除的角色数,用于判断是否「既是主账号也是子账号」
+            long roleCount = storePlatformUserRoleMapper.selectCount(
+                    new LambdaQueryWrapper<StorePlatformUserRole>()
+                            .eq(StorePlatformUserRole::getUserId, storeUser.getId())
+                            .eq(StorePlatformUserRole::getDeleteFlag, 0));
+            log.info("员工状态的status roleCount={}",roleCount);
+            boolean isBothMainAndSub = isMainAccount && roleCount > 0;
+            // 1 既是主账号也是子账号且本次为禁用:只禁用子账号角色,不更新 store_user,主账号保持启用;子账号在列表上按 role.status 展示为禁用
+            if (isBothMainAndSub) {
+                LambdaUpdateWrapper<StorePlatformUserRole> roleUpdateWrapper = new LambdaUpdateWrapper<>();
+                roleUpdateWrapper.eq(StorePlatformUserRole::getUserId, storeUser.getId())
+                        .eq(StorePlatformUserRole::getDeleteFlag, 0)
+                        .set(StorePlatformUserRole::getStatus, currentStatus);
+                storePlatformUserRoleMapper.update(null, roleUpdateWrapper);
+                StoreUserVo vo = new StoreUserVo();
+                vo.setId(storeUser.getId());
+                vo.setStatus(newStatus);
+                vo.setSwitchStatus(newStatus == 0);
+                return R.data(vo);
+            }
+            LambdaUpdateWrapper<StorePlatformUserRole> roleUpdateWrapper = new LambdaUpdateWrapper<>();
+            roleUpdateWrapper.eq(StorePlatformUserRole::getUserId, storeUser.getId())
+                    .eq(StorePlatformUserRole::getDeleteFlag, 0)
+                    .set(StorePlatformUserRole::getStatus, currentStatus);
+            storePlatformUserRoleMapper.update(null, roleUpdateWrapper);
+            baseRedisService.delete("store_" + storeUser.getPhone());
+            LambdaUpdateWrapper<StoreUser> userUpdateWrapper = new LambdaUpdateWrapper<>();
+            userUpdateWrapper.eq(StoreUser::getId, storeUser.getId()).set(StoreUser::getStatus, currentStatus);
+            storeUserMapper.update(null, userUpdateWrapper);
+            StoreUserVo vo = new StoreUserVo();
+            vo.setId(storeUser.getId());
+            vo.setStatus(newStatus);
+            vo.setSwitchStatus(newStatus == 0);
+            return R.data(vo);
 
-        storeUserMapper.deleteById(id);
-        //删除用户redis中的token
-        baseRedisService.delete("store_" + storeUser.getPhone());
-        //删除该账号的店铺
-        storeInfoMapper.delete(new LambdaQueryWrapper<StoreInfo>().eq(StoreInfo::getId, storeId));
-        //删除该账号的动态
-        lifeUserDynamicsMapper.delete(new LambdaQueryWrapper<LifeUserDynamics>().eq(LifeUserDynamics::getPhoneId, "store_" + phone));
-        //删除该账号关注的信息
-        lifeFansMapper.delete(new LambdaQueryWrapper<LifeFans>().eq(LifeFans::getFansId, "store_" + phone));
-        //删除关注该账号的信息
-        lifeFansMapper.delete(new LambdaQueryWrapper<LifeFans>().eq(LifeFans::getFollowedId, "store_" + phone));
-        //删除该账号的通知信息
-        lifeNoticeMapper.delete(new LambdaQueryWrapper<LifeNotice>().eq(LifeNotice::getReceiverId, "store_" + phone));
-        //删除该账号的发送消息信息
-        lifeMessageMapper.delete(new LambdaQueryWrapper<LifeMessage>().eq(LifeMessage::getSenderId, "store_" + phone));
-        //删除该账号的接受消息信息
-        lifeMessageMapper.delete(new LambdaQueryWrapper<LifeMessage>().eq(LifeMessage::getReceiverId, "store_" + phone));
-
-        return R.success("删除成功");
-    }
+        }
 
-    @Override
-    public void switchingStates(StoreUser storeUserParam) {
-        StoreUser storeUser = storeUserMapper.selectById(storeUserParam.getId());
-        //如果当前为启用,则删除token
-        if (storeUser.getStatus() == 0) {
-            //删除用户redis中的token
-            baseRedisService.delete("store_" + storeUser.getPhone());
+        // 本次为禁用
+        if (newStatus == 0) {
+            // 2 对于主账号:有关联子账号则禁止禁用,请先删除主账号下的子账号
+            if (isMainAccount) {
+                List<StoreUser> subAccounts = getSubAccountsByMainAccountId(storeUserParam.getId());
+                if (subAccounts != null && !subAccounts.isEmpty()) {
+                    throw new RuntimeException("请先删除主账号下的子账号后再禁用");
+                }
+            }
         }
-        //根据当前状态切另一个状态
-        storeUser.setStatus(storeUser.getStatus() == 0 ? 1 : 0);
-        storeUserMapper.updateById(storeUser);
+        // 启用/禁用:更新 store_user 的 status(禁用 0→1,启用 1→0),列表即时展示用返回的 vo
+        LambdaUpdateWrapper<StoreUser> userUpdateWrapper = new LambdaUpdateWrapper<>();
+        userUpdateWrapper.eq(StoreUser::getId, storeUserParam.getId()).set(StoreUser::getStatus, newStatus);
+        storeUserMapper.update(null, userUpdateWrapper);
+        StoreUserVo vo = new StoreUserVo();
+        vo.setId(storeUserParam.getId());
+        vo.setStatus(newStatus);
+        vo.setSwitchStatus(newStatus == 0);
+        return R.data(vo);
     }
 
+    /**
+     * 分页查询
+     *
+     * @param
+     * @param
+     * @param id
+     * @param phone
+     * @param status
+     * @param accountType
+     * @return
+     */
     @Override
     public String exportExcel(String id, String phone, String status, Integer accountType) throws IOException {
-        // 定义格式化模式
         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-        if (accountType==1){
-        List<StoreUser> storeUsers = storeUserMapper.selectList(
-                new LambdaQueryWrapper<StoreUser>()
-                        .like(StringUtils.isNotEmpty(id), StoreUser::getId, id)
-                        .like(StringUtils.isNotEmpty(phone), StoreUser::getPhone, phone)
-                        .eq(StringUtils.isNotEmpty(status), StoreUser::getStatus, status)
-                        .eq(StoreUser::getAccountType, 1)
-                        .orderByDesc(StoreUser::getCreatedTime)
-        );
-
-        List<StoreUserExcelVo> storeUserExcelVoList = new ArrayList<>();
-        int serialNumber = 0;
-        for (StoreUser storeUser : storeUsers) {
-            StoreUserExcelVo storeUserExcelVo = new StoreUserExcelVo();
-            storeUserExcelVo.setSerialNumber(++serialNumber);
-
-            Integer currentUserId = storeUser.getId();
-            BeanUtils.copyProperties(storeUser, storeUserExcelVo);
-            Instant instant = storeUser.getCreatedTime().toInstant();
-            // 格式化时间
-            String formattedTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime().format(formatter);
-            storeUserExcelVo.setCreatedTime(formattedTime);
-            //格式化状态
-            storeUserExcelVo.setStatus(storeUser.getStatus() == 0 ? "启用" : "禁用");
-            List<StoreUser> childAccounts = getChildAccountsByParentId(String.valueOf(currentUserId));
-            Integer childCount = childAccounts != null ? childAccounts.size() : 0;
-            storeUserExcelVo.setChildAccountCount(childCount);
-            storeUserExcelVoList.add(storeUserExcelVo);
+        // 主账号导出:与 getStoreUserList 主账号分支一致,子账号数量按 store_platform_user_role 统计;排除注销中、已注销
+        if (accountType == 1) {
+            LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreUser::getDeleteFlag, 0)
+                    .like(StringUtils.isNotEmpty(id), StoreUser::getId, id)
+                    .like(StringUtils.isNotEmpty(phone), StoreUser::getPhone, phone)
+                    .eq(StringUtils.isNotEmpty(status), StoreUser::getStatus, status)
+                    .eq(StoreUser::getAccountType, 1)
+                    .and(w -> w.isNull(StoreUser::getLogoutFlag).or().eq(StoreUser::getLogoutFlag, 0))
+                    .and(w -> w.isNull(StoreUser::getStatus).or().in(StoreUser::getStatus, 0, 1))
+                    .orderByDesc(StoreUser::getCreatedTime);
+            List<StoreUser> storeUsers = storeUserMapper.selectList(wrapper);
+            List<StoreUserExcelVo> storeUserExcelVoList = new ArrayList<>();
+            int serialNumber = 0;
+            for (StoreUser storeUser : storeUsers) {
+                StoreUserExcelVo vo = new StoreUserExcelVo();
+                vo.setSerialNumber(++serialNumber);
+                BeanUtils.copyProperties(storeUser, vo);
+                vo.setCreatedTime(storeUser.getCreatedTime() != null ? storeUser.getCreatedTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().format(formatter) : null);
+                vo.setStatus(storeUser.getStatus() == null ? "" : (storeUser.getStatus() == 0 ? "启用" : "禁用"));
+                vo.setChildAccountCount(countSubAccountUserIdsByStoreId(storeUser.getStoreId(), storeUser.getId()));
+                storeUserExcelVoList.add(vo);
+            }
+            String fileName = UUID.randomUUID().toString().replace("-", "");
+            String filePath = ExcelGenerator.generateExcel(excelPath + excelGeneratePath + fileName + ".xlsx", storeUserExcelVoList, StoreUserExcelVo.class);
+            return aliOSSUtil.uploadFile(new File(filePath), "excel/" + fileName + ".xlsx");
         }
-        String fileName = UUID.randomUUID().toString().replace("-", "");
-        String filePath = ExcelGenerator.generateExcel(excelPath + excelGeneratePath + fileName + ".xlsx", storeUserExcelVoList, StoreUserExcelVo.class);
-        return aliOSSUtil.uploadFile(new File(filePath), "excel/" + fileName + ".xlsx");
+        // 子账号导出:与 getStoreUserList 子账号分页一致,每条 store_platform_user_role 为一行(不去重),带主账号 id
+        LambdaQueryWrapper<StorePlatformUserRole> roleWrapper = new LambdaQueryWrapper<>();
+        roleWrapper.eq(StorePlatformUserRole::getDeleteFlag, 0);
+        List<StorePlatformUserRole> userRoles = storePlatformUserRoleMapper.selectList(roleWrapper);
+        if (CollectionUtils.isEmpty(userRoles)) {
+            String fileName = UUID.randomUUID().toString().replace("-", "");
+            String filePath = ExcelGenerator.generateExcel(excelPath + excelGeneratePath + fileName + ".xlsx", new ArrayList<StoreSubExcelVo>(), StoreSubExcelVo.class);
+            return aliOSSUtil.uploadFile(new File(filePath), "excel/" + fileName + ".xlsx");
         }
-        List<StoreUser> storeUsers = storeUserMapper.selectList(
-                new LambdaQueryWrapper<StoreUser>()
-                        .like(StringUtils.isNotEmpty(id), StoreUser::getId, id)
-                        .like(StringUtils.isNotEmpty(phone), StoreUser::getPhone, phone)
-                        .eq(StringUtils.isNotEmpty(status), StoreUser::getStatus, status)
-                        .eq(StoreUser::getAccountType, 2)
-                        .orderByDesc(StoreUser::getCreatedTime)
-        );
-        List<StoreSubExcelVo> storeUserExcelVoList = new ArrayList<>();
-        int serialNumber = 0;
-        for (StoreUser subAccount : storeUsers) {
-            StoreSubExcelVo storeUserExcelVo = new StoreSubExcelVo();
-            storeUserExcelVo.setSerialNumber(++serialNumber);
-            BeanUtils.copyProperties(subAccount, storeUserExcelVo);
-            if (subAccount.getSubAccountId() != null){
-                StoreUser mainAccount = storeUserMapper.selectById(subAccount.getSubAccountId());
-                if (mainAccount != null){
-                    storeUserExcelVo.setId(mainAccount.getId());
-                    storeUserExcelVo.setParentAccountPhone(mainAccount.getPhone());
-                }
+        // 按 (userId, role_id) 去重,与 getStoreUserList 子账号分页一致
+        userRoles = userRoles.stream()
+                .sorted(Comparator.comparing(StorePlatformUserRole::getId, Comparator.nullsLast(Comparator.naturalOrder())))
+                .collect(Collectors.toMap(
+                        r -> (r.getUserId() != null ? r.getUserId() : 0) + "_" + (r.getRoleId() != null ? r.getRoleId() : ""),
+                        r -> r,
+                        (a, b) -> a
+                ))
+                .values().stream()
+                .sorted(Comparator.comparing(StorePlatformUserRole::getId, Comparator.nullsLast(Comparator.naturalOrder())))
+                .collect(Collectors.toList());
+        List<Integer> distinctUserIds = userRoles.stream().map(StorePlatformUserRole::getUserId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        List<StoreUser> subAccountList = distinctUserIds.isEmpty() ? Collections.emptyList() : storeUserMapper.selectBatchIds(distinctUserIds);
+        Integer statusInt = null;
+        if (StringUtils.isNotEmpty(status)) {
+            try {
+                statusInt = Integer.parseInt(status.trim());
+            } catch (NumberFormatException ignored) {
             }
-
-            Instant instant = subAccount.getCreatedTime().toInstant();
-            // 格式化时间
-            String formattedTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime().format(formatter);
-            storeUserExcelVo.setCreatedTime(formattedTime);
-            //格式化状态
-            storeUserExcelVo.setStatus(subAccount.getStatus() == 0 ? "启用" : "禁用");
-            storeUserExcelVoList.add(storeUserExcelVo);
+        }
+        // 与 getStoreUserList 一致:选「注销中/已注销」时只保留对应状态;否则排除注销中、已注销
+        Map<Integer, StoreUser> subAccountMap;
+        if (statusInt != null && (statusInt == -1 || statusInt == 2)) {
+            final int filterStatus = statusInt;
+            subAccountMap = subAccountList == null ? new HashMap<>() : subAccountList.stream()
+                    .filter(u -> u.getDeleteFlag() != null && u.getDeleteFlag() == 0)
+                    .filter(u -> {
+                        if (filterStatus == -1) return u.getStatus() != null && u.getStatus() == -1;
+                        return (u.getStatus() != null && u.getStatus() == 2) || (u.getLogoutFlag() != null && u.getLogoutFlag() == 1);
+                    })
+                    .collect(Collectors.toMap(StoreUser::getId, u -> u, (a, b) -> a));
+        } else {
+            subAccountMap = subAccountList == null ? new HashMap<>() : subAccountList.stream()
+                    .filter(u -> u.getDeleteFlag() != null && u.getDeleteFlag() == 0
+                            && (u.getLogoutFlag() == null || u.getLogoutFlag() == 0)
+                            && (u.getStatus() == null || (u.getStatus() != -1 && u.getStatus() != 2)))
+                    .collect(Collectors.toMap(StoreUser::getId, u -> u, (a, b) -> a));
+        }
+        List<StorePlatformUserRole> filteredRoles = new ArrayList<>();
+        for (StorePlatformUserRole role : userRoles) {
+            StoreUser subAccount = subAccountMap.get(role.getUserId());
+            if (subAccount == null) continue;
+            if (StringUtils.isNotEmpty(id) && (subAccount.getId() == null || !String.valueOf(subAccount.getId()).contains(id)))
+                continue;
+            if (StringUtils.isNotEmpty(phone) && (subAccount.getPhone() == null || !subAccount.getPhone().contains(phone)))
+                continue;
+            int rowStatus;
+            if (statusInt != null && (statusInt == -1 || statusInt == 2)) {
+                rowStatus = (subAccount.getStatus() != null && subAccount.getStatus() == -1) ? -1
+                        : ((subAccount.getStatus() != null && subAccount.getStatus() == 2) || (subAccount.getLogoutFlag() != null && subAccount.getLogoutFlag() == 1)) ? 2
+                        : (subAccount.getStatus() != null ? subAccount.getStatus() : 0);
+            } else {
+                rowStatus = role.getStatus() != null ? role.getStatus() : (subAccount.getStatus() != null ? subAccount.getStatus() : 0);
+            }
+            if (statusInt != null && !statusInt.equals(rowStatus)) continue;
+            filteredRoles.add(role);
+        }
+        // 子账号导出与列表一致:按创建时间从近到远(降序)
+        filteredRoles.sort(Comparator.comparing(
+                (StorePlatformUserRole r) -> {
+                    StoreUser sub = subAccountMap.get(r.getUserId());
+                    return sub != null && sub.getCreatedTime() != null ? sub.getCreatedTime() : new Date(0L);
+                },
+                Comparator.nullsLast(Comparator.reverseOrder())
+        ).thenComparing(StorePlatformUserRole::getId, Comparator.nullsLast(Comparator.reverseOrder())));
+        List<Integer> storeIds = filteredRoles.stream().map(StorePlatformUserRole::getStoreId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        Map<Integer, StoreUser> mainAccountMap = new HashMap<>();
+        if (!storeIds.isEmpty()) {
+            LambdaQueryWrapper<StoreUser> mainW = new LambdaQueryWrapper<>();
+            mainW.eq(StoreUser::getAccountType, 1).eq(StoreUser::getDeleteFlag, 0).in(StoreUser::getStoreId, storeIds);
+            List<StoreUser> mains = storeUserMapper.selectList(mainW);
+            if (mains != null) mains.forEach(m -> mainAccountMap.put(m.getStoreId(), m));
+        }
+        List<StoreSubExcelVo> storeSubExcelVoList = new ArrayList<>();
+        int serialNumber = 0;
+        for (StorePlatformUserRole role : filteredRoles) {
+            StoreUser subAccount = subAccountMap.get(role.getUserId());
+            if (subAccount == null) continue;
+            StoreUser mainAccount = role.getStoreId() == null ? null : mainAccountMap.get(role.getStoreId());
+            StoreSubExcelVo vo = new StoreSubExcelVo();
+            vo.setSerialNumber(++serialNumber);
+            vo.setParentAccountId(mainAccount != null ? mainAccount.getId() : null);
+            vo.setParentAccountPhone(mainAccount != null ? mainAccount.getPhone() : null);
+            vo.setPhone(subAccount.getPhone());
+            vo.setCreatedTime(subAccount.getCreatedTime() != null ? subAccount.getCreatedTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().format(formatter) : null);
+            int rowStatus = (subAccount.getStatus() != null && subAccount.getStatus() == -1) ? -1
+                    : ((subAccount.getStatus() != null && subAccount.getStatus() == 2) || (subAccount.getLogoutFlag() != null && subAccount.getLogoutFlag() == 1)) ? 2
+                    : (role.getStatus() != null ? role.getStatus() : (subAccount.getStatus() != null ? subAccount.getStatus() : 0));
+            vo.setStatus(rowStatus == -1 ? "注销中" : rowStatus == 2 ? "已注销" : (rowStatus == 0 ? "启用" : "禁用"));
+            storeSubExcelVoList.add(vo);
         }
         String fileName = UUID.randomUUID().toString().replace("-", "");
-        String filePath = ExcelGenerator.generateExcel(excelPath + excelGeneratePath + fileName + ".xlsx", storeUserExcelVoList, StoreSubExcelVo.class);
+        String filePath = ExcelGenerator.generateExcel(excelPath + excelGeneratePath + fileName + ".xlsx", storeSubExcelVoList, StoreSubExcelVo.class);
         return aliOSSUtil.uploadFile(new File(filePath), "excel/" + fileName + ".xlsx");
+
     }
 
 
@@ -910,33 +1074,90 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserMapper, StoreUser
     }
 
     /**
-     *  删除门店
+     * 删除门店
+     *
      * @param storeUserVo
      */
 
     @Override
-    public void deleteStoreAccountInfo(StoreUserVo storeUserVo) {
-        LambdaQueryWrapper<StoreUser> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(StoreUser::getId, storeUserVo.getId());
-        queryWrapper.eq(StoreUser::getDeleteFlag, 0);
-        StoreUser storeUser = storeUserMapper.selectOne(queryWrapper);
-        // 删除已过注销时间的商家
-        if (storeUser != null) {
-            storeUserMapper.deleteById(storeUserVo.getId());
-            nearMeService.removeGeolocation(Boolean.TRUE, storeUser.getId().toString());
-            String storePhone = "store_" + storeUserVo.getPhone();
-            String key = baseRedisService.getString(storePhone);
-            if (key != null) {
-                //删除用户redis中的token
-                baseRedisService.delete(key);
+    public String deleteStoreAccountInfo(StoreUserVo storeUserVo) {
+        // 解析用户:优先用子账号联系方式 phone 查;未传 phone 时用 id/subAccountId 查 store_user,后端从库中拿到子账号联系方式(phone)
+        StoreUser storeUser = null;
+        if ((storeUserVo.getId() != null || storeUserVo.getSubAccountId() != null)) {
+            Integer idParam = storeUserVo.getSubAccountId() != null ? storeUserVo.getSubAccountId() : storeUserVo.getId();
+            storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getId, idParam).eq(StoreUser::getDeleteFlag, 0));
+            if (storeUser != null) {
+                log.info("deleteStoreAccountInfo 通过 id 查到用户,后端拿到子账号联系方式: userId={}, phone={}", storeUser.getId(), storeUser.getPhone());
             }
-            LambdaQueryWrapper<LifeFans> lifeFansLambdaQueryWrapper = new LambdaQueryWrapper<LifeFans>().eq(LifeFans::getFollowedId, "store_" + storeUser.getPhone())
-                    .or().eq(LifeFans::getFansId, "store_" + storeUser.getPhone());
-            lifeFansMapper.delete(lifeFansLambdaQueryWrapper);
+        }
+        if (storeUserVo == null) {
+            log.warn("deleteStoreAccountInfo 用户不存在或已删除,请传子账号联系电话(phone)或用户id");
+            return "删除失败";
+        }
+        // 通过 phone 再尝试解析用户(前端可能只传 phone)
+        if (storeUser == null && storeUserVo.getPhone() != null) {
+            storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getPhone, storeUserVo.getPhone()).eq(StoreUser::getDeleteFlag, 0));
+        }
+        if (storeUser == null) {
+            log.warn("deleteStoreAccountInfo 用户不存在或已删除,请传子账号联系电话(phone)或用户id");
+            return "删除失败";
+        }
+        Integer userId = storeUser.getId();
+
+        // 删除主账号校验:该主账号下有店铺禁止删除、该主账号下有子账号禁止删除(主账号列表返回的 id 即主账号 id)
+        if (storeUserVo.getAccountType() != null && storeUserVo.getAccountType() == 1) {
             if (storeUser.getStoreId() != null) {
-                storeInfoMapper.deleteById(storeUser.getStoreId());
+                log.error("该主账号下存在店铺,禁止删除");
+                return "当前账号下存在店铺 禁止删除";
+            }
+            Integer mainIdForCheck = storeUserVo.getId();
+            if (mainIdForCheck != null) {
+                List<StoreUser> subAccounts = getSubAccountsByMainAccountId(mainIdForCheck);
+                if (subAccounts != null && !subAccounts.isEmpty()) {
+                    log.error("该主账号下存在关联子账号,禁止删除");
+                    return "当前账号下存在子账号禁止删除";
+                }
             }
         }
+
+        // 主账号 id:优先用子账号 store_user.sub_account_id;无则用前端列表返回的 parentAccountId(与分页接口 records 字段一致)
+        Integer mainAccountId = storeUser.getSubAccountId() != null ? storeUser.getSubAccountId() : storeUserVo.getParentAccountId();
+        if (mainAccountId == null) {
+            log.warn("deleteStoreAccountInfo 无法确定主账号,请传 parentAccountId 或保证子账号 store_user.sub_account_id 有值");
+            return "删除失败";
+        }
+        // 查询主账号下的子账号数量(按 store_user 统计:account_type=2 且 sub_account_id=主账号id)
+        List<StoreUser> subAccountsUnderMain = getSubAccountsByMainAccountId(mainAccountId);
+        int subAccountCount = subAccountsUnderMain != null ? subAccountsUnderMain.size() : 0;
+        log.info("主账号下子账号数量: mainAccountId={}, subAccountCount={}", mainAccountId, subAccountCount);
+
+        // 仅删除该主账号对应店铺下的角色:取主账号的 store_id,按 userId + storeId 逻辑删除 store_platform_user_role
+        StoreUser mainAccount = storeUserMapper.selectById(mainAccountId);
+        Integer storeId = mainAccount != null ? mainAccount.getStoreId() : null;
+        LambdaUpdateWrapper<StorePlatformUserRole> roleUpdateWrapper = new LambdaUpdateWrapper<>();
+        roleUpdateWrapper.eq(StorePlatformUserRole::getUserId, userId).eq(StorePlatformUserRole::getDeleteFlag, 0);
+        if (storeId != null) {
+            roleUpdateWrapper.eq(StorePlatformUserRole::getStoreId, storeId);
+        }
+        roleUpdateWrapper.set(StorePlatformUserRole::getDeleteFlag, 1);
+        storePlatformUserRoleMapper.update(null, roleUpdateWrapper);
+
+        // 主账号下子账号唯一:逻辑删除 store_platform_user_role 和 store_user;不唯一:只逻辑删除 store_platform_user_role
+        if (subAccountCount == 1) {
+            LambdaUpdateWrapper<StoreUser> userUpdateWrapper = new LambdaUpdateWrapper<>();
+            userUpdateWrapper.eq(StoreUser::getId, userId)
+                    .eq(StoreUser::getDeleteFlag, 0)
+                    .set(StoreUser::getDeleteFlag, 1);
+            storeUserMapper.update(null, userUpdateWrapper);
+            log.info("主账号下子账号唯一,已逻辑删除 store_platform_user_role 和 store_user: userId={}", userId);
+            String tokenKey = "store_" + storeUser.getPhone();
+            baseRedisService.delete(tokenKey);
+        } else {
+            log.info("主账号下子账号数不为1,仅逻辑删除 store_platform_user_role,不删 store_user: mainAccountId={}, subAccountCount={}", mainAccountId, subAccountCount);
+        }
+        return "删除失败";
     }
 
     @Override

+ 2 - 1
alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java

@@ -425,7 +425,8 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             wrapper.eq(LifeUserDynamics::getPhoneId, storePhoneId)
                     .eq(LifeUserDynamics::getType, "2") // 商家社区
                     .eq(LifeUserDynamics::getDraft, 0) // 非草稿
-                    .ge(LifeUserDynamics::getCreatedTime, startDate)
+                    // 统计发布动态数量,统计全部而不是当天的数据
+//                    .ge(LifeUserDynamics::getCreatedTime, startDate)
                     .lt(LifeUserDynamics::getCreatedTime, endDate)
                     .eq(LifeUserDynamics::getDeleteFlag, 0);
             return lifeUserDynamicsMapper.selectCount(wrapper);

+ 12 - 75
alien-store/src/main/resources/logback-spring.xml

@@ -1,40 +1,28 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
-<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
-<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
-<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
-<!-- 该信息是由于设置了当配置文件变化时重新加载,所以每当达到扫描时间的时候就会检查配置文件是否错误。但是由于一般配置文件都放在了JAR包中,
-    而扫描的时候无法扫描JAR包内,因此会提示没有可以检查的文件,所以每隔一段时间就输出一次-->
-<configuration scan="false" scanPeriod="60 seconds" debug="true">
-<!--    <contextName>logback-spring</contextName>-->
+<configuration scan="false" scanPeriod="60 seconds" debug="false">
 
-    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
     <!-- 定义全局参数常量 -->
     <property name="log.level" value="debug"/>
-    <property name="log.maxHistory" value="30"/><!-- 30表示30 -->
-    <springProperty scope="context" name="logging.path" source="logging.path"  defaultValue="C:/project/ext/log"/>
+    <property name="log.maxHistory" value="30"/><!-- 30表示30天 -->
+    <springProperty scope="context" name="logging.path" source="logging.path" defaultValue="C:/project/ext/log"/>
     <!--输出文件前缀-->
     <property name="FILENAME" value="alien"/>
 
-    <!--0. 日志格式和颜色渲染 -->
-    <!-- 彩色日志依赖的渲染类 -->
-    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
-    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
-    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
-
     <!-- 文件输出格式 -->
     <property name="FILE_LOG_PATTERN" value="[%d{MM/dd HH:mm:ss.SSS}][%-10.10thread][%-5level][%-40.40c{1}:%5line]:[%15method] || %m%n"/>
-    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+    
+    <!-- 控制台输出格式:纯文本,无颜色,适合 Docker/EFK -->
+    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n"/>
 
     <!--1. 输出到控制台-->
     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
+        <!-- 【关键】控制台只输出 INFO 及以上,防止 SQL 刷屏 ES -->
         <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-            <level>${log.level}</level>
+            <level>INFO</level>
         </filter>
         <encoder>
             <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
-            <!-- 设置字符集 -->
             <charset>UTF-8</charset>
         </encoder>
     </appender>
@@ -42,27 +30,17 @@
     <!--2. 输出到文档-->
     <!-- DEBUG 日志 -->
     <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 当前的日志文件存放路径 -->
         <file>${logging.path}/DEBUG.log</file>
-        <!-- 日志滚动策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-            <!-- 历史日志文件的存放路径和名称 -->
             <fileNamePattern>${logging.path}/%d{yyyy-MM-dd}_${FILENAME}_DEBUG.log.gz</fileNamePattern>
-            <!-- 日志文件最大的保存历史 数量-->
             <maxHistory>${log.maxHistory}</maxHistory>
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
             <pattern>${FILE_LOG_PATTERN}</pattern>
         </encoder>
-        <!--日志文件最大的大小-->
-        <!--        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
-        <!--            <MaxFileSize>10MB</MaxFileSize>-->
-        <!--        </triggeringPolicy>-->
-        <!-- 此日志文档只记录debug级别的 -->
         <filter class="ch.qos.logback.classic.filter.LevelFilter">
             <level>DEBUG</level>
-            <onMatch>ACCEPT</onMatch>  <!-- 用过滤器,只接受DEBUG级别的日志信息,其余全部过滤掉 -->
+            <onMatch>ACCEPT</onMatch>
             <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
@@ -117,45 +95,14 @@
         </filter>
     </appender>
 
-    <!--
-      <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
-      以及指定<appender>。<logger>仅有一个name属性,
-      一个可选的level和一个可选的addtivity属性。
-      name:用来指定受此logger约束的某一个包或者具体的某一个类。
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-         还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
-         如果未设置此属性,那么当前logger将会继承上级的级别。
-      addtivity:是否向上级logger传递打印信息。默认是true。
-      <logger name="org.springframework.web" level="info"/>
-      <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-    -->
-
-    <!--
-      使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
-      第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
-      第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
-      【logging.level.org.mybatis=debug logging.level.dao=debug】
-     -->
-    <!-- mybatis显示sql,修改此处扫描包名 -->
-
-
-    <!--
-      root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
-      level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
-      不能设置为INHERITED或者同义词NULL。默认是DEBUG
-      可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-    -->
-
+    <!-- 降噪配置 -->
     <logger name="springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator" level="WARN"/>
     <logger name="org.springframework.security.web.DefaultSecurityFilterChain " level="WARN"/>
     <logger name="com.netflix.config.sources.URLConfigurationSource " level="WARN"/>
+    <logger name="com.netflix.discovery" level="WARN"/>
+    <logger name="org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]" level="WARN"/>
 
     <!-- 4. 最终的策略 -->
-    <!-- 4.1 开发环境:打印控制台-->
-    <!--打印sql-->
-    <!--    <logger name="com.veryhappy.music.dao" level="debug"/>-->
-
-    <!--打印log-->
     <root level="info">
         <appender-ref ref="CONSOLE"/>
         <appender-ref ref="DEBUG_FILE"/>
@@ -164,14 +111,4 @@
         <appender-ref ref="ERROR_FILE"/>
     </root>
 
-    <!--   4.2 生产环境:输出到文档-->
-    <springProfile name="pro">
-        <root level="info">
-            <appender-ref ref="CONSOLE"/>
-            <appender-ref ref="DEBUG_FILE"/>
-            <appender-ref ref="INFO_FILE"/>
-            <appender-ref ref="ERROR_FILE"/>
-            <appender-ref ref="WARN_FILE"/>
-        </root>
-    </springProfile>
 </configuration>