ソースを参照

feat(data-board): 新增数据看板模块及相关接口

- 新增数据看板Controller,提供数据概览、学校预约概览、月份统计及分页查询等接口
- 新增数据看板Service及其实现,完成数据统计、分页逻辑及履约率计算
- 新增对应Mapper及XML,实现SQL查询逻辑支持
- 新增数据看板相关VO定义,规范接口数据结构
- 优化家庭成员模块,增加实名认证校验接口与逻辑
- 家庭成员添加接口新增实名认证图片大小限制及数据校验流程
- 优化订单库存处理,新增原子扣减和恢复库存接口,防止并发库存超卖
- 订单服务中应用库存扣减的乐观锁校验及缓存更新逻辑
- AppSiteService实现修正商户名称括号格式问题
wzq 3 日 前
コミット
b58a9da331
15 ファイル変更842 行追加56 行削除
  1. 54 12
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/controller/my/AppFamilyMembersController.java
  2. 24 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/form/RealNameAuthenticationForm.java
  3. 51 35
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/impl/OrderServiceImpl.java
  4. 78 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/controller/AppDataBoardController.java
  5. 19 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/AppSitePriceRulesMapper.java
  6. 68 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/DataBoardMapper.java
  7. 1 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/AppOrderMapper.xml
  8. 159 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/DataBoardMapper.xml
  9. 38 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/IDataBoardService.java
  10. 3 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/IFamilyMembersService.java
  11. 1 1
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/AppSiteServiceImpl.java
  12. 187 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/DataBoardServiceImpl.java
  13. 35 7
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/FamilyMembersServiceImpl.java
  14. 123 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/vo/DataBoardVO.java
  15. 1 1
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/mapper/xml/SysUserMapper.xml

+ 54 - 12
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/controller/my/AppFamilyMembersController.java

@@ -2,16 +2,20 @@ package org.jeecg.modules.app.controller.my;
 
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.java.Log;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.compress.utils.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.shiro.SecurityUtils;
 import org.jeecg.common.api.vo.Result;
+import org.jeecg.common.aspect.annotation.AutoLog;
 import org.jeecg.common.system.vo.LoginUser;
 import org.jeecg.modules.app.dto.AddFamilyMembersDTO;
 import org.jeecg.modules.app.dto.FindFamilyMembersDTO;
+import org.jeecg.modules.app.form.RealNameAuthenticationForm;
 import org.jeecg.modules.system.app.entity.FamilyMembers;
 import org.jeecg.modules.system.app.service.IFamilyMembersService;
 import org.jeecg.modules.system.entity.SysUser;
@@ -49,6 +53,17 @@ public class AppFamilyMembersController {
         return iFamilyMembersService.findFamilyMembers(findFamilyMembersDTO);
     }
 
+    /**
+     * 实名认证校验
+     * @param realNameAuthenticationForm
+     * @return True 表示已实名认证 /False 表示未实名认证
+     */
+    @PostMapping("/realNameAuthentication")
+    @Operation(summary = "实名认证校验")
+    @AutoLog("实名认证校验")
+    public Result<Boolean> realNameAuthentication(@Validated @RequestBody RealNameAuthenticationForm realNameAuthenticationForm){
+        return Result.OK(iFamilyMembersService.realNameAuthentication(realNameAuthenticationForm));
+    }
 
     @PostMapping("/addFamilyMembers")
     @Operation(summary = "添加家庭成员")
@@ -57,20 +72,14 @@ public class AppFamilyMembersController {
         if (addFamilyMembersDTO.getRealNameStatus()!=1){
             return Result.error("该添加人员未实名,无法进行添加");
         }
-        if (StringUtils.isNotEmpty(addFamilyMembersDTO.getUserId())){
-            SysUser sysUser = iSysUserService.getById(addFamilyMembersDTO.getUserId());
-            if (sysUser==null||sysUser.getDelFlag()==1){
-                return Result.error("该添加人员未查询到所属用户,无法进行添加");
-            }
-            iFamilyMembersService.addFamilyMembers(addFamilyMembersDTO);
-        }
+        // 校验实名认证图片
         if (StringUtils.isNotEmpty(addFamilyMembersDTO.getRealNameImg())) {
             try {
-                byte[] imageBytes = downloadImage(addFamilyMembersDTO.getRealNameImg()); // 下载图片
+                byte[] imageBytes = downloadImage(addFamilyMembersDTO.getRealNameImg());
                 if (imageBytes.length < 60 * 1024) {
                     return Result.error("图片太小,请重新上传大于60KB的图片");
                 } else if (imageBytes.length > 200 * 1024) {
-                    return Result.error("图片太,请重新上传小于200KB的图片");
+                    return Result.error("图片太,请重新上传小于200KB的图片");
                 }
             } catch (Exception e) {
                 log.error("处理实名认证图片失败", e);
@@ -78,9 +87,42 @@ public class AppFamilyMembersController {
             }
         }
         LoginUser principal = (LoginUser) SecurityUtils.getSubject().getPrincipal();
-        String userId=principal.getId();
-        addFamilyMembersDTO.setUserId(userId);
-        return iFamilyMembersService.addFamilyMembers(addFamilyMembersDTO);
+        String currentUserId = principal.getId();
+        if (StringUtils.isNotEmpty(addFamilyMembersDTO.getUserId())){
+            // userId不为空:为该userId创建userType=1的家人记录,同时为当前登录用户创建userType=0的本人记录
+            SysUser sysUser = iSysUserService.getById(addFamilyMembersDTO.getUserId());
+            if (sysUser==null||sysUser.getDelFlag()==1){
+                return Result.error("该添加人员未查询到所属用户,无法进行添加");
+            }
+            // 为指定userId创建家人记录(userType=1)
+            addFamilyMembersDTO.setUserType(1);
+            Result<String> familyResult = iFamilyMembersService.addFamilyMembers(addFamilyMembersDTO);
+            if (!familyResult.isSuccess()){
+                return familyResult;
+            }
+            // 为当前登录用户创建本人记录(userType=0),若已存在则跳过
+            FamilyMembers selfRecord = iFamilyMembersService.getOne(Wrappers.<FamilyMembers>lambdaQuery()
+                    .eq(FamilyMembers::getUserId, currentUserId)
+                    .eq(FamilyMembers::getUserType, 0)
+                    .eq(FamilyMembers::getIdentityCard, addFamilyMembersDTO.getIdentityCard())
+            );
+            if (selfRecord != null) {
+                return Result.ok("添加成功");
+            }
+            addFamilyMembersDTO.setUserId(currentUserId);
+            addFamilyMembersDTO.setUserType(0);
+            return iFamilyMembersService.addFamilyMembers(addFamilyMembersDTO);
+        } else {
+            // userId为空:用当前登录用户userId创建家庭成员,若本人记录已存在则跳过
+            FamilyMembers selfRecord = iFamilyMembersService.getOne(Wrappers.<FamilyMembers>lambdaQuery()
+                    .eq(FamilyMembers::getUserId, currentUserId)
+                    .eq(FamilyMembers::getUserType, 0));
+            if (selfRecord != null) {
+                return Result.ok("本人记录已存在,无需重复添加");
+            }
+            addFamilyMembersDTO.setUserId(currentUserId);
+            return iFamilyMembersService.addFamilyMembers(addFamilyMembersDTO);
+        }
     }
     private byte[] downloadImage(String imageUrl) throws IOException {
         URL url = new URL(imageUrl);

+ 24 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/form/RealNameAuthenticationForm.java

@@ -0,0 +1,24 @@
+package org.jeecg.modules.app.form;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@Schema(description = "实名认证校验表单对象")
+public class RealNameAuthenticationForm implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "姓名")
+    private String name;
+
+    @Schema(description = "身份证号")
+    private String idCard;
+}

+ 51 - 35
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/impl/OrderServiceImpl.java

@@ -40,6 +40,7 @@ import org.jeecg.modules.pay.config.RatiosUtil;
 import org.jeecg.modules.pay.config.WechatConstants;
 import org.jeecg.modules.pay.config.WechatUrlConstants;
 import org.jeecg.modules.rabbitmq.DelayedMessageService;
+import org.jeecg.modules.redission.RedisLockUtils;
 import org.jeecg.modules.system.app.dto.ClassGroupingDTO;
 import org.jeecg.modules.system.app.dto.receiptPaymentDetails.ReceiptPaymentDetailsInfoVo;
 import org.jeecg.modules.system.app.entity.*;
@@ -594,37 +595,50 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
                     productKey = "ORDER_TYPE_1_PRODUCT_" + productId; // ORDER_TYPE_1_PRODUCT_N001
                     stockKey = "ORDER_TYPE_1_PRODUCT_STOCK_" + productId; // ORDER_TYPE_1_PRODUCT_STOCK_N001
 
-                    // 查询库存
-                    Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
                     //使用人
                     List<String> ids = Arrays.stream(createOrderForm.getFamilyIds().split(",")).collect(Collectors.toList());
-                    log.info("学校场地预约商品库存数量:{}", stock);
-                    // 缓存没有商品库存,查询数据库
-                    if (stock == null || stock == 0 || stock < ids.size()) {
-                        AppSitePriceRules product = appSitePriceRulesMapper.selectById(productId);
-                        if (Objects.isNull(product)) {
-                            throw new JeecgBootException("订单提交失败,商品已下架");
+
+                    // 加分布式锁,防止并发超卖
+                    String stockLockKey = "LOCK_ORDER_STOCK_" + productId;
+                    boolean locked = RedisLockUtils.tryLock(stockLockKey, TimeUnit.SECONDS, 3, 10);
+                    if (!locked) {
+                        throw new JeecgBootException("系统正忙,请稍后再试");
+                    }
+                    try {
+                        // 查询库存(优先从Redis读)
+                        Object stockObj = redisTemplate.opsForValue().get(stockKey);
+                        Integer stock = stockObj != null ? Integer.parseInt(stockObj.toString()) : null;
+                        log.info("学校场地预约商品库存数量:{}", stock);
+
+                        // 缓存没有商品库存,查询数据库
+                        if (stock == null) {
+                            AppSitePriceRules product = appSitePriceRulesMapper.selectById(productId);
+                            if (Objects.isNull(product)) {
+                                throw new JeecgBootException("订单提交失败,商品已下架");
+                            }
+                            redisTemplate.opsForValue().set(productKey, JSON.toJSONString(product), 60 * 60 * 24, TimeUnit.SECONDS);
+                            if (product.getTicketNum() == null) {
+                                throw new JeecgBootException("订单提交失败,当前商品库存为空");
+                            }
+                            stock = product.getTicketNum();
+                            redisTemplate.opsForValue().set(stockKey, stock, 60 * 60 * 24, TimeUnit.SECONDS);
                         }
-                        redisTemplate.opsForValue().set(productKey, JSON.toJSONString(product), 60 * 60 * 24, TimeUnit.SECONDS);
-                        // 数据库的库存信息要根据实际业务来获取,如果商品有规格信息,库存应该根据规格来获取
-                        if (product.getTicketNum() == null) {
-                            throw new JeecgBootException("订单提交失败,当前商品库存为空");
+
+                        // 检查库存是否足够
+                        if (stock < ids.size()) {
+                            throw new JeecgBootException("订单提交失败,库存不足");
+                        }
+
+                        // 原子扣减数据库库存(防并发覆盖写)
+                        log.info("更新学校场地数据库中的库存数据,扣减数量:{}", ids.size());
+                        int row = appSitePriceRulesMapper.deductStock(productId, ids.size());
+                        if (row <= 0) {
+                            throw new JeecgBootException("订单提交失败,库存不足");
                         }
-                        stock = product.getTicketNum();
-                        redisTemplate.opsForValue().set(stockKey, stock, 60 * 60 * 24, TimeUnit.SECONDS);
-                    }
-                    // 检查库存是否足够
-                    if (stock < ids.size()) {
-                        throw new JeecgBootException("订单提交失败,库存不足");
-                    }
-                    // 更新数据库中的库存数据
-                    log.info("更新学校场地数据库中的库存数据:{}", stock - ids.size());
-                    int row = appSitePriceRulesMapper.update(null, Wrappers.<AppSitePriceRules>lambdaUpdate()
-                            .eq(AppSitePriceRules::getId, productId)
-                            .set(AppSitePriceRules::getTicketNum, (stock - ids.size())));
-                    if (row > 0) {
                         // 更新Redis中缓存的商品库存数据
                         redisTemplate.opsForValue().decrement(stockKey, ids.size());
+                    } finally {
+                        RedisLockUtils.unlock(stockLockKey);
                     }
                     // 库存扣减完,创建订单
                     appOrder
@@ -2568,6 +2582,9 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
         if (!Objects.equals(appOrder.getOrderType(), CommonConstant.ORDER_PRO_INFO_TYPE_0)){
             throw new JeecgBootException("只能主动取消学校场地预约订单!");
         }
+        if (Objects.equals(appOrder.getOrderStatus(), CommonConstant.ORDER_STATUS_4)) {
+            throw new JeecgBootException("当前订单已取消,请勿重复操作!");
+        }
         List<AppOrderProInfo> orderProInfoList = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery().eq(AppOrderProInfo::getOrderId, orderId));
         if (CollUtil.isNotEmpty(orderProInfoList)) {
             // 按productId分组统计需要恢复的库存数量(排除保险类型的子订单)
@@ -2580,20 +2597,19 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
                 String productId = entry.getKey();
                 int restoreCount = entry.getValue().intValue();
 
-                // 恢复数据库库存
-                AppSitePriceRules priceRules = appSitePriceRulesMapper.selectById(productId);
-                if (ObjectUtil.isNotEmpty(priceRules)) {
-                    int newTicketNum = (priceRules.getTicketNum() == null ? 0 : priceRules.getTicketNum()) + restoreCount;
-                    appSitePriceRulesMapper.update(null, Wrappers.<AppSitePriceRules>lambdaUpdate()
-                            .eq(AppSitePriceRules::getId, productId)
-                            .set(AppSitePriceRules::getTicketNum, newTicketNum));
-                    log.info("取消学校订单恢复库存,productId={},恢复数量={},恢复后库存={}", productId, restoreCount, newTicketNum);
+                // 恢复数据库库存(原子操作,使用原生SQL不受@TableLogic影响)
+                int restored = appSitePriceRulesMapper.restoreStock(productId, restoreCount);
+                if (restored > 0) {
+                    log.info("取消学校订单恢复库存,productId={},恢复数量={}", productId, restoreCount);
 
                     // 恢复Redis缓存库存
                     String stockKey = "ORDER_TYPE_1_PRODUCT_STOCK_" + productId;
-                    Integer cachedStock = (Integer) redisTemplate.opsForValue().get(stockKey);
-                    if (cachedStock != null) {
+                    Object stockObj = redisTemplate.opsForValue().get(stockKey);
+                    if (stockObj != null) {
                         redisTemplate.opsForValue().increment(stockKey, restoreCount);
+                    } else {
+                        // 缓存不存在时直接删除key,下次下单时会从已提交的DB重新加载
+                        redisTemplate.delete(stockKey);
                     }
                 }
             }

+ 78 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/controller/AppDataBoardController.java

@@ -0,0 +1,78 @@
+package org.jeecg.modules.system.app.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.common.api.vo.Result;
+import org.jeecg.modules.system.app.service.IDataBoardService;
+import org.jeecg.modules.system.app.vo.DataBoardVO;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Tag(name = "数据看板")
+@RestController
+@RequestMapping("/app/dataBoard")
+@Slf4j
+public class AppDataBoardController {
+
+    @Resource
+    private IDataBoardService dataBoardService;
+
+    /**
+     * 数据概览(注册用户、累计/今年/本月/今日 预约人数与入场人数)
+     */
+    @GetMapping("/overview")
+    @Operation(summary = "数据概览")
+    public Result<DataBoardVO.OverviewVO> overview() {
+        return Result.OK(dataBoardService.overview());
+    }
+
+    /**
+     * 学校预约概览-柱状图(各学校累计预约人数及入场人数)
+     */
+    @GetMapping("/schoolOverview")
+    @Operation(summary = "学校预约概览-柱状图")
+    public Result<List<DataBoardVO.SchoolOverviewVO>> schoolOverview() {
+        return Result.OK(dataBoardService.schoolOverview());
+    }
+
+    /**
+     * 学校预约月份统计-折线图(指定学校本年度各月份数据)
+     */
+    @GetMapping("/schoolMonthly")
+    @Operation(summary = "学校预约月份统计-折线图")
+    public Result<List<DataBoardVO.SchoolMonthlyVO>> schoolMonthly(
+            @RequestParam(name = "siteId", required = false) String siteId,
+            @RequestParam(name = "year", required = false) Integer year) {
+        return Result.OK(dataBoardService.schoolMonthly(siteId, year));
+    }
+
+    /**
+     * 学校预约列表-分页(含合计)
+     */
+    @GetMapping("/schoolReservationPage")
+    @Operation(summary = "学校预约列表-分页")
+    public Result<DataBoardVO.SchoolReservationPageVO> schoolReservationPage(
+            @RequestParam(name = "siteIds", required = false) String siteIds,
+            @RequestParam(name = "timeRange", required = false, defaultValue = "0") Integer timeRange,
+            @RequestParam(name = "startDate", required = false) String startDate,
+            @RequestParam(name = "endDate", required = false) String endDate,
+            @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
+            @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
+        return Result.OK(dataBoardService.schoolReservationPage(siteIds, timeRange, startDate, endDate, pageNo, pageSize));
+    }
+
+    /**
+     * 获取全部学校下拉列表
+     */
+    @GetMapping("/schools")
+    @Operation(summary = "获取全部学校下拉列表")
+    public Result<List<DataBoardVO.SchoolOptionVO>> listSchools() {
+        return Result.OK(dataBoardService.listSchools());
+    }
+}

+ 19 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/AppSitePriceRulesMapper.java

@@ -3,6 +3,7 @@ package org.jeecg.modules.system.app.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
 import org.jeecg.modules.app.vo.OrderVO;
 import org.jeecg.modules.app.vo.PlaceInfoVO;
 import org.jeecg.modules.system.app.entity.AppSitePriceRules;
@@ -52,4 +53,22 @@ public interface AppSitePriceRulesMapper extends BaseMapper<AppSitePriceRules> {
      */
     @Select("select * from nm_site_price_rules where id = #{id}")
     AppSitePriceRules selectAllById(String id);
+
+    /**
+     * 原子扣减库存(防并发覆盖写)
+     * @param id 规则ID
+     * @param count 扣减数量
+     * @return 影响行数(0表示库存不足)
+     */
+    @Update("UPDATE nm_site_price_rules SET ticket_num = ticket_num - #{count} WHERE id = #{id} AND ticket_num >= #{count}")
+    int deductStock(@Param("id") String id, @Param("count") int count);
+
+    /**
+     * 原子恢复库存
+     * @param id 规则ID
+     * @param count 恢复数量
+     * @return 影响行数
+     */
+    @Update("UPDATE nm_site_price_rules SET ticket_num = IFNULL(ticket_num, 0) + #{count} WHERE id = #{id}")
+    int restoreStock(@Param("id") String id, @Param("count") int count);
 }

+ 68 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/DataBoardMapper.java

@@ -0,0 +1,68 @@
+package org.jeecg.modules.system.app.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.jeecg.modules.system.app.vo.DataBoardVO;
+
+import java.util.List;
+
+/**
+ * 数据看板Mapper
+ */
+@Mapper
+public interface DataBoardMapper {
+
+    /**
+     * 统计注册C端用户数
+     */
+    Long countRegisteredUsers();
+
+    /**
+     * 统计预约人数
+     */
+    Long countReservations(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    /**
+     * 统计入场人数
+     */
+    Long countEntries(@Param("startDate") String startDate, @Param("endDate") String endDate);
+
+    /**
+     * 学校预约概览(各学校累计预约人数与入场人数)
+     */
+    List<DataBoardVO.SchoolOverviewVO> schoolOverview();
+
+    /**
+     * 学校预约月份统计(指定学校本年度各月份)
+     */
+    List<DataBoardVO.SchoolMonthlyVO> schoolMonthly(@Param("siteId") String siteId, @Param("year") int year);
+
+    /**
+     * 学校预约列表-分页
+     */
+    List<DataBoardVO.SchoolReservationVO> schoolReservationPage(
+            @Param("siteIds") List<String> siteIds,
+            @Param("startDate") String startDate,
+            @Param("endDate") String endDate);
+
+    /**
+     * 学校预约列表-合计
+     */
+    DataBoardVO.StatsVO schoolReservationTotal(
+            @Param("siteIds") List<String> siteIds,
+            @Param("startDate") String startDate,
+            @Param("endDate") String endDate);
+
+    /**
+     * 学校预约列表-总数
+     */
+    Long schoolReservationCount(
+            @Param("siteIds") List<String> siteIds,
+            @Param("startDate") String startDate,
+            @Param("endDate") String endDate);
+
+    /**
+     * 获取全部学校下拉列表
+     */
+    List<DataBoardVO.SchoolOptionVO> listSchools();
+}

+ 1 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/AppOrderMapper.xml

@@ -100,6 +100,7 @@
           AND b.start_time = #{startTime}
           AND b.end_time = #{endTime}
           AND opi.type <![CDATA[!=]]> 6
+          AND a.order_status <![CDATA[!=]]> 4
           AND b.del_flag = 0;
     </select>
     <select id="pageOrders" resultType="org.jeecg.modules.app.vo.PageOrdersVO">

+ 159 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/DataBoardMapper.xml

@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.jeecg.modules.system.app.mapper.DataBoardMapper">
+
+    <!-- 统计C端注册用户数 (user_identity=3 且 org_code IS NULL) -->
+    <select id="countRegisteredUsers" resultType="java.lang.Long">
+        SELECT COUNT(DISTINCT u.id)
+        FROM nm_family_members u
+        WHERE u.del_flag = 0
+    </select>
+
+    <!-- 统计学校预约人数 (order_pro_info type=0, order_status IN (1,2,3)) -->
+    <select id="countReservations" resultType="java.lang.Long">
+        SELECT COUNT(opi.id)
+        FROM nm_order_pro_info opi
+                 INNER JOIN nm_order o ON opi.order_id = o.id AND o.del_flag = 0
+        WHERE opi.del_flag = 0
+          AND opi.type = 0
+          AND opi.order_status IN (1, 2, 3)
+        <if test="startDate != null and startDate != ''">
+            AND opi.create_time &gt;= #{startDate}
+        </if>
+        <if test="endDate != null and endDate != ''">
+            AND opi.create_time &lt;= #{endDate}
+        </if>
+    </select>
+
+    <!-- 统计学校入场人数 (order_pro_info type=0, order_status=2 已使用) -->
+    <select id="countEntries" resultType="java.lang.Long">
+        SELECT COUNT(opi.id)
+        FROM nm_order_pro_info opi
+                 INNER JOIN nm_order o ON opi.order_id = o.id AND o.del_flag = 0
+        WHERE opi.del_flag = 0
+          AND opi.type = 0
+          AND opi.order_status = 2
+        <if test="startDate != null and startDate != ''">
+            AND opi.create_time &gt;= #{startDate}
+        </if>
+        <if test="endDate != null and endDate != ''">
+            AND opi.create_time &lt;= #{endDate}
+        </if>
+    </select>
+
+    <!-- 各学校累计预约人数与入场人数 (柱状图) -->
+    <select id="schoolOverview" resultType="org.jeecg.modules.system.app.vo.DataBoardVO$SchoolOverviewVO">
+        SELECT
+            s.id AS siteId,
+            s.name AS schoolName,
+            COUNT(CASE WHEN opi.order_status IN (1, 2, 3) THEN opi.id END) AS reservationCount,
+            COUNT(CASE WHEN opi.order_status = 2 THEN opi.id END) AS entryCount
+        FROM nm_site s
+                 LEFT JOIN nm_order_pro_info opi ON opi.site_id = s.id AND opi.del_flag = 0 AND opi.type = 0
+        WHERE s.del_flag = 0
+          AND s.type = 0
+        GROUP BY s.id, s.name
+        ORDER BY reservationCount DESC
+    </select>
+
+    <!-- 指定学校本年度各月份预约与入场统计 (折线图) -->
+    <select id="schoolMonthly" resultType="org.jeecg.modules.system.app.vo.DataBoardVO$SchoolMonthlyVO">
+        SELECT
+            m.month,
+            COALESCE(t.reservationCount, 0) AS reservationCount,
+            COALESCE(t.entryCount, 0) AS entryCount
+        FROM (
+            SELECT 1 AS month UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
+            UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
+            UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
+        ) m
+        LEFT JOIN (
+            SELECT
+                MONTH(opi.create_time) AS month,
+                COUNT(CASE WHEN opi.order_status IN (1, 2, 3) THEN opi.id END) AS reservationCount,
+                COUNT(CASE WHEN opi.order_status = 2 THEN opi.id END) AS entryCount
+            FROM nm_order_pro_info opi
+                     INNER JOIN nm_order o ON opi.order_id = o.id AND o.del_flag = 0
+            WHERE opi.del_flag = 0
+              AND opi.type = 0
+              AND opi.site_id = #{siteId}
+              AND YEAR(opi.create_time) = #{year}
+            GROUP BY MONTH(opi.create_time)
+        ) t ON m.month = t.month
+        ORDER BY m.month
+    </select>
+
+    <!-- 学校预约列表-分页查询 -->
+    <select id="schoolReservationPage" resultType="org.jeecg.modules.system.app.vo.DataBoardVO$SchoolReservationVO">
+        SELECT
+            s.id AS siteId,
+            s.name AS schoolName,
+            COUNT(CASE WHEN opi.order_status IN (1, 2, 3) THEN opi.id END) AS reservationCount,
+            COUNT(CASE WHEN opi.order_status = 2 THEN opi.id END) AS entryCount
+        FROM nm_site s
+                 LEFT JOIN nm_order_pro_info opi ON opi.site_id = s.id AND opi.del_flag = 0 AND opi.type = 0
+            <if test="startDate != null and startDate != ''">
+                AND opi.create_time &gt;= #{startDate}
+            </if>
+            <if test="endDate != null and endDate != ''">
+                AND opi.create_time &lt;= #{endDate}
+            </if>
+        WHERE s.del_flag = 0
+          AND s.type = 0
+        <if test="siteIds != null and siteIds.size() > 0">
+            AND s.id IN
+            <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+                #{siteId}
+            </foreach>
+        </if>
+        GROUP BY s.id, s.name
+        ORDER BY reservationCount DESC
+    </select>
+
+    <!-- 学校预约列表-合计统计 -->
+    <select id="schoolReservationTotal" resultType="org.jeecg.modules.system.app.vo.DataBoardVO$StatsVO">
+        SELECT
+            COUNT(CASE WHEN opi.order_status IN (1, 2, 3) THEN opi.id END) AS reservationCount,
+            COUNT(CASE WHEN opi.order_status = 2 THEN opi.id END) AS entryCount
+        FROM nm_order_pro_info opi
+                 INNER JOIN nm_site s ON opi.site_id = s.id AND s.del_flag = 0 AND s.type = 0
+        WHERE opi.del_flag = 0
+          AND opi.type = 0
+        <if test="siteIds != null and siteIds.size() > 0">
+            AND s.id IN
+            <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+                #{siteId}
+            </foreach>
+        </if>
+        <if test="startDate != null and startDate != ''">
+            AND opi.create_time &gt;= #{startDate}
+        </if>
+        <if test="endDate != null and endDate != ''">
+            AND opi.create_time &lt;= #{endDate}
+        </if>
+    </select>
+
+    <!-- 学校预约列表-总学校数 -->
+    <select id="schoolReservationCount" resultType="java.lang.Long">
+        SELECT COUNT(*)
+        FROM nm_site s
+        WHERE s.del_flag = 0
+          AND s.type = 0
+        <if test="siteIds != null and siteIds.size() > 0">
+            AND s.id IN
+            <foreach collection="siteIds" item="siteId" open="(" separator="," close=")">
+                #{siteId}
+            </foreach>
+        </if>
+    </select>
+
+    <!-- 获取全部学校列表(下拉选项) -->
+    <select id="listSchools" resultType="org.jeecg.modules.system.app.vo.DataBoardVO$SchoolOptionVO">
+        SELECT s.id, s.name
+        FROM nm_site s
+        WHERE s.del_flag = 0
+          AND s.type = 0
+        ORDER BY s.sort ASC, s.create_time ASC
+    </select>
+
+</mapper>

+ 38 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/IDataBoardService.java

@@ -0,0 +1,38 @@
+package org.jeecg.modules.system.app.service;
+
+import org.jeecg.modules.system.app.vo.DataBoardVO;
+
+import java.util.List;
+
+/**
+ * 数据看板Service
+ */
+public interface IDataBoardService {
+
+    /**
+     * 数据概览
+     */
+    DataBoardVO.OverviewVO overview();
+
+    /**
+     * 学校预约概览(柱状图)
+     */
+    List<DataBoardVO.SchoolOverviewVO> schoolOverview();
+
+    /**
+     * 学校预约月份统计(折线图)
+     */
+    List<DataBoardVO.SchoolMonthlyVO> schoolMonthly(String siteId, Integer year);
+
+    /**
+     * 学校预约列表(分页+合计)
+     */
+    DataBoardVO.SchoolReservationPageVO schoolReservationPage(
+            String siteIds, Integer timeRange, String startDate, String endDate,
+            Integer pageNo, Integer pageSize);
+
+    /**
+     * 获取全部学校下拉列表
+     */
+    List<DataBoardVO.SchoolOptionVO> listSchools();
+}

+ 3 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/IFamilyMembersService.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import org.jeecg.common.api.vo.Result;
 import org.jeecg.modules.app.dto.AddFamilyMembersDTO;
 import org.jeecg.modules.app.dto.FindFamilyMembersDTO;
+import org.jeecg.modules.app.form.RealNameAuthenticationForm;
 import org.jeecg.modules.system.app.dto.FindFamilyMembersResponseDTO;
 import org.jeecg.modules.system.app.entity.FamilyMembers;
 
@@ -27,4 +28,6 @@ public interface IFamilyMembersService extends IService<FamilyMembers> {
 
 
     Result<IPage<FamilyMembers>> findFamilyMembers(FindFamilyMembersDTO findFamilyMembersDTO);
+
+    Boolean realNameAuthentication(RealNameAuthenticationForm realNameAuthenticationForm);
 }

+ 1 - 1
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/AppSiteServiceImpl.java

@@ -209,7 +209,7 @@ public class AppSiteServiceImpl extends ServiceImpl<AppSiteMapper, AppSite> impl
         EditSitePayInfoDTO editSitePayInfoDTO = new EditSitePayInfoDTO();
         SysDepart sysDepart = sysDepartMapper.selectById(id);
         // 转换括号
-        String mchName = editSitePayInfoDTO.getMchName();
+        String mchName = sysDepart.getMchName();
         if (mchName != null) {
             mchName = mchName.replace("(", "(").replace(")", ")");
         }

+ 187 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/DataBoardServiceImpl.java

@@ -0,0 +1,187 @@
+package org.jeecg.modules.system.app.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.modules.system.app.mapper.DataBoardMapper;
+import org.jeecg.modules.system.app.service.IDataBoardService;
+import org.jeecg.modules.system.app.vo.DataBoardVO;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.Year;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 数据看板Service实现
+ */
+@Slf4j
+@Service
+public class DataBoardServiceImpl implements IDataBoardService {
+
+    @Resource
+    private DataBoardMapper dataBoardMapper;
+
+    @Override
+    public DataBoardVO.OverviewVO overview() {
+        DataBoardVO.OverviewVO vo = new DataBoardVO.OverviewVO();
+        // 注册用户数
+        vo.setRegisteredUsers(dataBoardMapper.countRegisteredUsers());
+
+        LocalDate now = LocalDate.now();
+        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+        // 累计(不传时间)
+        vo.setCumulative(buildStats(null, null));
+
+        // 今年
+        String yearStart = now.withDayOfYear(1).format(fmt) + " 00:00:00";
+        String yearEnd = now.format(fmt) + " 23:59:59";
+        vo.setThisYear(buildStats(yearStart, yearEnd));
+
+        // 本月
+        String monthStart = now.withDayOfMonth(1).format(fmt) + " 00:00:00";
+        String monthEnd = now.format(fmt) + " 23:59:59";
+        vo.setThisMonth(buildStats(monthStart, monthEnd));
+
+        // 今日
+        String todayStart = now.format(fmt) + " 00:00:00";
+        String todayEnd = now.format(fmt) + " 23:59:59";
+        vo.setToday(buildStats(todayStart, todayEnd));
+
+        return vo;
+    }
+
+    @Override
+    public List<DataBoardVO.SchoolOverviewVO> schoolOverview() {
+        return dataBoardMapper.schoolOverview();
+    }
+
+    @Override
+    public List<DataBoardVO.SchoolMonthlyVO> schoolMonthly(String siteId, Integer year) {
+        if (StrUtil.isEmpty(siteId)) {
+            return Collections.emptyList();
+        }
+        int resolvedYear = (year != null) ? year : Year.now().getValue();
+        return dataBoardMapper.schoolMonthly(siteId, resolvedYear);
+    }
+
+    @Override
+    public DataBoardVO.SchoolReservationPageVO schoolReservationPage(
+            String siteIds, Integer timeRange, String startDate, String endDate,
+            Integer pageNo, Integer pageSize) {
+
+        // 解析学校IDs
+        List<String> siteIdList = null;
+        if (siteIds != null && !siteIds.isEmpty()) {
+            siteIdList = Arrays.stream(siteIds.split(","))
+                    .map(String::trim)
+                    .filter(s -> !s.isEmpty())
+                    .collect(Collectors.toList());
+        }
+
+        // 根据timeRange计算日期范围
+        String[] dateRange = resolveDateRange(timeRange, startDate, endDate);
+        String resolvedStart = dateRange[0];
+        String resolvedEnd = dateRange[1];
+
+        // 查询总学校数
+        Long total = dataBoardMapper.schoolReservationCount(siteIdList, resolvedStart, resolvedEnd);
+
+        // 分页查询
+        List<DataBoardVO.SchoolReservationVO> allRecords = dataBoardMapper.schoolReservationPage(
+                siteIdList, resolvedStart, resolvedEnd);
+
+        // 手动分页
+        int fromIndex = (pageNo - 1) * pageSize;
+        int toIndex = Math.min(fromIndex + pageSize, allRecords.size());
+        List<DataBoardVO.SchoolReservationVO> pagedRecords;
+        if (fromIndex >= allRecords.size()) {
+            pagedRecords = Collections.emptyList();
+        } else {
+            pagedRecords = allRecords.subList(fromIndex, toIndex);
+        }
+
+        // 计算履约率
+        for (DataBoardVO.SchoolReservationVO record : pagedRecords) {
+            record.setFulfillmentRate(calcFulfillmentRate(record.getReservationCount(), record.getEntryCount()));
+        }
+
+        // 合计
+        DataBoardVO.StatsVO totalStats = dataBoardMapper.schoolReservationTotal(
+                siteIdList, resolvedStart, resolvedEnd);
+
+        DataBoardVO.SchoolReservationPageVO result = new DataBoardVO.SchoolReservationPageVO();
+        result.setRecords(pagedRecords);
+        result.setTotal(total);
+        result.setCurrent((long) pageNo);
+        result.setSize((long) pageSize);
+        result.setTotalReservationCount(totalStats != null ? totalStats.getReservationCount() : 0L);
+        result.setTotalEntryCount(totalStats != null ? totalStats.getEntryCount() : 0L);
+        return result;
+    }
+
+    @Override
+    public List<DataBoardVO.SchoolOptionVO> listSchools() {
+        return dataBoardMapper.listSchools();
+    }
+
+    /**
+     * 构建预约/入场统计
+     */
+    private DataBoardVO.StatsVO buildStats(String startDate, String endDate) {
+        DataBoardVO.StatsVO stats = new DataBoardVO.StatsVO();
+        stats.setReservationCount(dataBoardMapper.countReservations(startDate, endDate));
+        stats.setEntryCount(dataBoardMapper.countEntries(startDate, endDate));
+        return stats;
+    }
+
+    /**
+     * 根据时间范围类型解析日期区间
+     * @param timeRange 0-不限 1-今年 2-本月 3-自定义
+     */
+    private String[] resolveDateRange(Integer timeRange, String startDate, String endDate) {
+        if (timeRange == null || timeRange == 0) {
+            return new String[]{null, null};
+        }
+        LocalDate now = LocalDate.now();
+        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        switch (timeRange) {
+            case 1: // 今年
+                return new String[]{
+                        now.withDayOfYear(1).format(fmt) + " 00:00:00",
+                        now.format(fmt) + " 23:59:59"
+                };
+            case 2: // 本月
+                return new String[]{
+                        now.withDayOfMonth(1).format(fmt) + " 00:00:00",
+                        now.format(fmt) + " 23:59:59"
+                };
+            case 3: // 自定义
+                String resolvedStart = (startDate != null && !startDate.isEmpty()) ? startDate + " 00:00:00" : null;
+                String resolvedEnd = (endDate != null && !endDate.isEmpty()) ? endDate + " 23:59:59" : null;
+                return new String[]{resolvedStart, resolvedEnd};
+            default:
+                return new String[]{null, null};
+        }
+    }
+
+    /**
+     * 计算履约率 = 入场人数 / 预约人数 * 100%
+     */
+    private String calcFulfillmentRate(Long reservationCount, Long entryCount) {
+        if (reservationCount == null || reservationCount == 0) {
+            return "0.00%";
+        }
+        BigDecimal rate = BigDecimal.valueOf(entryCount)
+                .multiply(BigDecimal.valueOf(100))
+                .divide(BigDecimal.valueOf(reservationCount), 2, RoundingMode.HALF_UP);
+        return rate + "%";
+    }
+}

+ 35 - 7
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/FamilyMembersServiceImpl.java

@@ -1,15 +1,19 @@
 package org.jeecg.modules.system.app.service.impl;
 
 
+import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import me.zhyd.oauth.utils.UuidUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.shiro.SecurityUtils;
 import org.jeecg.common.api.vo.Result;
+import org.jeecg.common.system.vo.LoginUser;
 import org.jeecg.modules.app.dto.AddFamilyMembersDTO;
 import org.jeecg.modules.app.dto.FindFamilyMembersDTO;
+import org.jeecg.modules.app.form.RealNameAuthenticationForm;
 import org.jeecg.modules.system.app.dto.FindFamilyMembersResponseDTO;
 import org.jeecg.modules.system.app.dto.FindResponseDTO;
 import org.jeecg.modules.system.app.entity.FamilyMembers;
@@ -73,14 +77,9 @@ public class FamilyMembersServiceImpl extends ServiceImpl<FamilyMembersMapper, F
                 .eq(FamilyMembers::getIdentityCard,addFamilyMembersDTO.getIdentityCard())).isEmpty()){
             return Result.error("请勿重复添加,该身份证已填写!");
         }
-        FamilyMembers familyMembers = familyMembersMapper.findByUserIdAndPhone(addFamilyMembersDTO.getUserId(),addFamilyMembersDTO.getPhone());
-        if (familyMembers==null){
-            familyMembers = new FamilyMembers();
-            familyMembers.setId(UuidUtils.getUUID());
-        }else {
-            addFamilyMembersDTO.setId(familyMembers.getId());
-        }
+        FamilyMembers familyMembers = new FamilyMembers();
         BeanUtils.copyProperties(addFamilyMembersDTO,familyMembers);
+        familyMembers.setId(UuidUtils.getUUID());
 
         familyMembers.setCreateTime(new Date());
         if (familyMembersMapper.insert(familyMembers)<1){
@@ -126,4 +125,33 @@ public class FamilyMembersServiceImpl extends ServiceImpl<FamilyMembersMapper, F
         Page<FindResponseDTO> page = new Page<FindResponseDTO>(findFamilyMembersDTO.getPageNo(), findFamilyMembersDTO.getPageSize());
         return Result.ok(familyMembersMapper.findByPage(page,findFamilyMembersDTO.getUserId()));
     }
+
+    @Override
+    public Boolean realNameAuthentication(RealNameAuthenticationForm realNameAuthenticationForm) {
+        //获取当前用户
+        LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
+        //是否本人认证
+        FamilyMembers familyMembers = familyMembersMapper
+                .selectOne(Wrappers.<FamilyMembers>lambdaQuery()
+                        .eq(FamilyMembers::getUserId, user.getId())
+                        .eq(FamilyMembers::getIdentityCard, realNameAuthenticationForm.getIdCard())
+                        .eq(FamilyMembers::getFullName, realNameAuthenticationForm.getName())
+                        .last("limit 1")
+                );
+        if (ObjectUtil.isNotEmpty(familyMembers)){
+            throw new RuntimeException("本人已实名认证,请勿重复操作!");
+        }
+        //家庭成员是否实名认证
+        List<FamilyMembers> familyMembersList = familyMembersMapper
+                .selectList(Wrappers.<FamilyMembers>lambdaQuery()
+                        .eq(FamilyMembers::getIdentityCard, realNameAuthenticationForm.getIdCard())
+                        .eq(FamilyMembers::getFullName, realNameAuthenticationForm.getName())
+                        .eq(FamilyMembers::getRealNameStatus, 1)
+                );
+        if (!familyMembersList.isEmpty()){
+            return Boolean.TRUE;
+        }
+
+        return Boolean.FALSE;
+    }
 }

+ 123 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/vo/DataBoardVO.java

@@ -0,0 +1,123 @@
+package org.jeecg.modules.system.app.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 数据看板VO
+ */
+public class DataBoardVO {
+
+    /**
+     * 数据概览
+     */
+    @Data
+    @Schema(description = "数据概览")
+    public static class OverviewVO implements Serializable {
+        @Schema(description = "注册用户数")
+        private Long registeredUsers;
+        @Schema(description = "累计统计")
+        private StatsVO cumulative;
+        @Schema(description = "今年统计")
+        private StatsVO thisYear;
+        @Schema(description = "本月统计")
+        private StatsVO thisMonth;
+        @Schema(description = "今日统计")
+        private StatsVO today;
+    }
+
+    /**
+     * 预约/入场统计
+     */
+    @Data
+    @Schema(description = "预约/入场统计")
+    public static class StatsVO implements Serializable {
+        @Schema(description = "预约人数")
+        private Long reservationCount;
+        @Schema(description = "入场人数")
+        private Long entryCount;
+    }
+
+    /**
+     * 学校预约概览(柱状图)
+     */
+    @Data
+    @Schema(description = "学校预约概览")
+    public static class SchoolOverviewVO implements Serializable {
+        @Schema(description = "学校ID")
+        private String siteId;
+        @Schema(description = "学校名称")
+        private String schoolName;
+        @Schema(description = "预约人数")
+        private Long reservationCount;
+        @Schema(description = "入场人数")
+        private Long entryCount;
+    }
+
+    /**
+     * 学校预约月份统计(折线图)
+     */
+    @Data
+    @Schema(description = "学校预约月份统计")
+    public static class SchoolMonthlyVO implements Serializable {
+        @Schema(description = "月份")
+        private Integer month;
+        @Schema(description = "预约人数")
+        private Long reservationCount;
+        @Schema(description = "入场人数")
+        private Long entryCount;
+    }
+
+    /**
+     * 学校预约列表(表格行)
+     */
+    @Data
+    @Schema(description = "学校预约列表")
+    public static class SchoolReservationVO implements Serializable {
+        @Schema(description = "学校ID")
+        private String siteId;
+        @Schema(description = "学校名称")
+        private String schoolName;
+        @Schema(description = "预约人数")
+        private Long reservationCount;
+        @Schema(description = "入场人数")
+        private Long entryCount;
+        @Schema(description = "履约率")
+        private String fulfillmentRate;
+    }
+
+    /**
+     * 学校预约分页结果(含合计)
+     */
+    @Data
+    @Schema(description = "学校预约分页结果")
+    public static class SchoolReservationPageVO implements Serializable {
+        @Schema(description = "分页数据")
+        private List<SchoolReservationVO> records;
+        @Schema(description = "总记录数")
+        private Long total;
+        @Schema(description = "当前页")
+        private Long current;
+        @Schema(description = "每页条数")
+        private Long size;
+        @Schema(description = "合计-预约人数")
+        private Long totalReservationCount;
+        @Schema(description = "合计-入场人数")
+        private Long totalEntryCount;
+    }
+
+    /**
+     * 学校下拉选项
+     */
+    @Data
+    @Schema(description = "学校下拉选项")
+    public static class SchoolOptionVO implements Serializable {
+        @Schema(description = "学校ID")
+        private String id;
+        @Schema(description = "学校名称")
+        private String name;
+    }
+}

+ 1 - 1
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/mapper/xml/SysUserMapper.xml

@@ -350,7 +350,7 @@
                 LEFT JOIN ( SELECT fm.user_id, COUNT(*) AS num FROM nm_family_members fm WHERE fm.del_flag = 0 GROUP BY fm.user_id ) AS tem ON u.id = tem.user_id
         <where>
             nfm.del_flag = 0
-            AND nfm.user_type = 1
+            -- AND nfm.user_type = 1
             <if test="findPageCUserInfoRequestDTO.username!=null  and findPageCUserInfoRequestDTO.username!=''">
                 and nfm.full_name like CONCAT('%',#{findPageCUserInfoRequestDTO.username},'%')
             </if>