From 5882ba8863d77fdcc168b176c3ffb30e8a20556a Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期一, 13 七月 2026 17:54:25 +0800
Subject: [PATCH] 缩短二维码原始参数

---
 backend/src/main/java/com/sinata/appointment/controller/AppointmentController.java            |   43 ++---
 yuyueMiniProgram/src/pages/index/index.vue                                                    |   34 ++--
 yuyueMiniProgram/src/pages/appointment/index.vue                                              |    2 
 .gitignore                                                                                    |    2 
 backend/src/main/java/com/sinata/appointment/service/StaffManageApplicationService.java       |    6 
 backend/src/main/java/com/sinata/appointment/service/VisitorUserManageApplicationService.java |   66 +++++++--
 yuyueMiniProgram/src/pages/visitorCode/index.vue                                              |   28 ++-
 backend/src/main/java/com/sinata/appointment/common/GlobalExceptionHandler.java               |   13 +
 backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java       |   93 +++++++-----
 backend/src/main/java/com/sinata/appointment/service/PostApprovalOrchestrationService.java    |   23 +-
 backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java         |    4 
 yuyueMiniProgram/src/pages/visitorCodeShare/index.vue                                         |   46 ++++-
 backend/src/main/java/com/sinata/appointment/controller/VisitorUserManageController.java      |   24 +--
 backend/src/main/java/com/sinata/appointment/service/LocalImageUploadService.java             |    5 
 14 files changed, 235 insertions(+), 154 deletions(-)

diff --git a/.gitignore b/.gitignore
index 3e5a369..b89b6ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@
 *.log
 .env
 .env.local
+.codegraph/
+**/.codegraph/
diff --git a/backend/src/main/java/com/sinata/appointment/common/GlobalExceptionHandler.java b/backend/src/main/java/com/sinata/appointment/common/GlobalExceptionHandler.java
index ed85b56..b90acca 100644
--- a/backend/src/main/java/com/sinata/appointment/common/GlobalExceptionHandler.java
+++ b/backend/src/main/java/com/sinata/appointment/common/GlobalExceptionHandler.java
@@ -35,17 +35,28 @@
     
     @ExceptionHandler(SaTokenException.class)
     public Result<Void> handleSaToken(SaTokenException e) {
-        log.error("satoken error", e);
         if (e instanceof NotPermissionException){
+            log.warn("satoken forbidden: {}", safeMessage(e));
             return ResultUtil.error(HttpBizCode.FORBIDDEN, "无权限访问");
         }
         if (e instanceof NotLoginException notLogin
                 && NotLoginException.KICK_OUT.equals(notLogin.getType())) {
+            log.warn("satoken unauthorized: type={} message={}", notLogin.getType(), safeMessage(e));
             return ResultUtil.error(HttpBizCode.UNAUTHORIZED, "账号已被禁用,请重新登录");
         }
+        if (e instanceof NotLoginException notLogin) {
+            log.warn("satoken unauthorized: type={} message={}", notLogin.getType(), safeMessage(e));
+            return ResultUtil.error(HttpBizCode.UNAUTHORIZED, "账号信息已过期,请重新登录");
+        }
+        log.warn("satoken error: {} message={}", e.getClass().getSimpleName(), safeMessage(e));
         return ResultUtil.error(HttpBizCode.UNAUTHORIZED, "账号信息已过期,请重新登录");
     }
 
+    private String safeMessage(Exception e) {
+        String message = e.getMessage();
+        return message == null || message.isBlank() ? "-" : message;
+    }
+
     @ExceptionHandler(Exception.class)
     public Result<Void> handleOther(Exception e) {
         log.error("Unhandled error", e);
diff --git a/backend/src/main/java/com/sinata/appointment/controller/AppointmentController.java b/backend/src/main/java/com/sinata/appointment/controller/AppointmentController.java
index 9b2c36c..6953369 100644
--- a/backend/src/main/java/com/sinata/appointment/controller/AppointmentController.java
+++ b/backend/src/main/java/com/sinata/appointment/controller/AppointmentController.java
@@ -9,13 +9,12 @@
 import com.sinata.appointment.service.AppointmentApplicationService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.constraints.NotBlank;
+import java.io.IOException;
 import lombok.RequiredArgsConstructor;
 import java.time.LocalDate;
 import org.springframework.format.annotation.DateTimeFormat;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -67,7 +66,8 @@
     @Operation(summary = "导出 Excel")
     @GetMapping("/export")
     @SaCheckPermission("appointments:export")
-    public ResponseEntity<byte[]> export(
+    public void export(
+            HttpServletResponse response,
             @RequestParam(required = false) String visitedUserName,
             @RequestParam(required = false) Long visitedDeptId,
             @RequestParam(required = false) Long accessAreaId,
@@ -78,26 +78,21 @@
             @RequestParam(required = false) String displayStatus,
             @RequestParam(required = false) String approvalStatus,
             @RequestParam(required = false) String visitorKeyword,
-            @RequestParam(required = false) String visitReasonCode) {
-        byte[] body =
-                appointmentApplicationService.exportExcel(
-                        visitedUserName,
-                        visitedDeptId,
-                        accessAreaId,
-                        accessArea,
-                        localUnit,
-                        visitDateStart,
-                        visitDateEnd,
-                        displayStatus,
-                        approvalStatus,
-                        visitorKeyword,
-                        visitReasonCode);
-        return ResponseEntity.ok()
-                .contentType(
-                        MediaType.parseMediaType(
-                                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
-                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"appointments.xlsx\"")
-                .body(body);
+            @RequestParam(required = false) String visitReasonCode)
+            throws IOException {
+        appointmentApplicationService.exportExcel(
+                response,
+                visitedUserName,
+                visitedDeptId,
+                accessAreaId,
+                accessArea,
+                localUnit,
+                visitDateStart,
+                visitDateEnd,
+                displayStatus,
+                approvalStatus,
+                visitorKeyword,
+                visitReasonCode);
     }
 
     @Operation(summary = "详情含访客")
diff --git a/backend/src/main/java/com/sinata/appointment/controller/VisitorUserManageController.java b/backend/src/main/java/com/sinata/appointment/controller/VisitorUserManageController.java
index 9e7678e..b590e82 100644
--- a/backend/src/main/java/com/sinata/appointment/controller/VisitorUserManageController.java
+++ b/backend/src/main/java/com/sinata/appointment/controller/VisitorUserManageController.java
@@ -10,13 +10,12 @@
 import com.sinata.appointment.service.VisitorUserManageApplicationService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
+import java.io.IOException;
 import java.time.LocalDate;
 import lombok.RequiredArgsConstructor;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
 import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.DeleteMapping;
@@ -58,24 +57,17 @@
     @Operation(summary = "导出 Excel")
     @GetMapping("/export")
     @SaCheckPermission("sys:visitor-users:export")
-    public ResponseEntity<byte[]> export(
+    public void export(
+            HttpServletResponse response,
             @RequestParam(required = false) String userName,
             @RequestParam(required = false) String mobile,
             @RequestParam(required = false) String companyName,
             @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate registerStartDate,
             @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate registerEndDate,
-            @RequestParam(required = false) Integer status) {
-        byte[] body =
-                visitorUserManageApplicationService.exportExcel(
-                        userName, mobile, companyName, registerStartDate, registerEndDate, status);
-        return ResponseEntity.ok()
-                .contentType(
-                        MediaType.parseMediaType(
-                                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
-                .header(
-                        HttpHeaders.CONTENT_DISPOSITION,
-                        "attachment; filename=\"visitor-users.xlsx\"")
-                .body(body);
+            @RequestParam(required = false) Integer status)
+            throws IOException {
+        visitorUserManageApplicationService.exportExcel(
+                response, userName, mobile, companyName, registerStartDate, registerEndDate, status);
     }
 
     @Operation(summary = "详情")
diff --git a/backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java b/backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java
index d87666c..f2d6deb 100644
--- a/backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/AppointmentApplicationService.java
@@ -38,7 +38,10 @@
 import com.sinata.appointment.util.MobileMaskUtil;
 import com.sinata.appointment.util.SensitiveDataUtil;
 import com.sinata.appointment.util.UuidAssert;
-import java.io.ByteArrayOutputStream;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -110,7 +113,8 @@
         return buildAdminPage(pagedOrders);
     }
 
-    public byte[] exportExcel(
+    public void exportExcel(
+            HttpServletResponse response,
             String visitedUserName,
             Long visitedDeptId,
             Long accessAreaId,
@@ -121,49 +125,60 @@
             String displayStatus,
             String approvalStatus,
             String visitorKeyword,
-            String visitReasonCode) {
+            String visitReasonCode)
+            throws IOException {
+        prepareExcelResponse(response, "appointments.xlsx");
         List<Long> accessAreaIds = resolveAccessAreaFilterIds(accessAreaId);
         long pageNo = 1L;
         long pageSize = 200L;
-        List<AppointmentAdminExportRow> rows = new ArrayList<>();
-        while (true) {
-            Page<AppointmentOrder> orderPage = new Page<>(pageNo, pageSize);
-            IPage<AppointmentOrder> pagedOrders =
-                    appointmentOrderMapper.pageAdminOrders(
-                            orderPage,
-                            trimToNull(visitedUserName),
-                            visitedDeptId,
-                            accessAreaIds,
-                            trimToNull(accessArea),
-                            trimToNull(localUnit),
-                            visitDateStart,
-                            visitDateEnd,
-                            trimToNull(displayStatus),
-                            trimToNull(approvalStatus),
-                            trimToNull(visitorKeyword),
-                            trimToNull(visitReasonCode));
-            List<AppointmentOrder> orders = pagedOrders.getRecords();
-            if (orders == null || orders.isEmpty()) {
-                break;
+        boolean wroteAnyPage = false;
+        try (ExcelWriter writer =
+                EasyExcel.write(response.getOutputStream(), AppointmentAdminExportRow.class).build()) {
+            WriteSheet sheet = EasyExcel.writerSheet("预约记录").build();
+            while (true) {
+                Page<AppointmentOrder> orderPage = new Page<>(pageNo, pageSize);
+                IPage<AppointmentOrder> pagedOrders =
+                        appointmentOrderMapper.pageAdminOrders(
+                                orderPage,
+                                trimToNull(visitedUserName),
+                                visitedDeptId,
+                                accessAreaIds,
+                                trimToNull(accessArea),
+                                trimToNull(localUnit),
+                                visitDateStart,
+                                visitDateEnd,
+                                trimToNull(displayStatus),
+                                trimToNull(approvalStatus),
+                                trimToNull(visitorKeyword),
+                                trimToNull(visitReasonCode));
+                List<AppointmentOrder> orders = pagedOrders.getRecords();
+                if (orders == null || orders.isEmpty()) {
+                    break;
+                }
+                AdminPageData pageData = loadAdminPageData(orders);
+                writer.write(
+                        buildExportRows(
+                                orders,
+                                pageData.visitorRowsMap(),
+                                pageData.approvalInstanceMap(),
+                                pageData.reasonNameMap()),
+                        sheet);
+                wroteAnyPage = true;
+                if (pageNo * pageSize >= pagedOrders.getTotal()) {
+                    break;
+                }
+                pageNo++;
             }
-            AdminPageData pageData = loadAdminPageData(orders);
-            rows.addAll(
-                    buildExportRows(
-                            orders,
-                            pageData.visitorRowsMap(),
-                            pageData.approvalInstanceMap(),
-                            pageData.reasonNameMap()));
-            if (pageNo * pageSize >= pagedOrders.getTotal()) {
-                break;
+            if (!wroteAnyPage) {
+                writer.write(List.of(), sheet);
             }
-            pageNo++;
         }
-        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
-            EasyExcel.write(bos, AppointmentAdminExportRow.class).sheet("预约记录").doWrite(rows);
-            return bos.toByteArray();
-        } catch (Exception e) {
-            return new byte[0];
-        }
+    }
+
+    private void prepareExcelResponse(HttpServletResponse response, String filename) {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("UTF-8");
+        response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
     }
 
     public AppointmentOrder getById(Long id, String uuid) {
diff --git a/backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java b/backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java
index e4d548d..2f6cade 100644
--- a/backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/IntegrationHikvisionService.java
@@ -95,7 +95,6 @@
         payload.put("workUnit", firstNonBlank(valueOf(visitorProfile, MpVisitor::getVisitorCompany), order.getVisitorOrg()));
         payload.put("carNumber", trimToEmpty(visitor.getPlateNo()));
         payload.put("visitPurpose", trimToEmpty(visitPurpose));
-        payload.put("jobNo", valueOf(visitedUser, MpUser::getStaffNo));
         payload.put("beVisitorPhone", valueOf(visitedUser, MpUser::getMobile));
         payload.put("startTime", formatDateTime(startTime, QR_TIME_FORMATTER));
         payload.put("endTime", formatDateTime(endTime, QR_TIME_FORMATTER));
@@ -104,9 +103,6 @@
         payload.put("date", formatDateTime(qrGenerateTime, QR_DATE_FORMATTER));
         payload.put("validTime", VALID_TIME_PREFIX + Math.max(validMinutes, 1));
         payload.put("codeId", trimToEmpty(codeId));
-        payload.put("customa", EMPTY);
-        payload.put("customb", EMPTY);
-        payload.put("customc", EMPTY);
         try {
             String json = objectMapper.writeValueAsString(payload);
             log.info(
diff --git a/backend/src/main/java/com/sinata/appointment/service/LocalImageUploadService.java b/backend/src/main/java/com/sinata/appointment/service/LocalImageUploadService.java
index 1c713e2..ee503d7 100644
--- a/backend/src/main/java/com/sinata/appointment/service/LocalImageUploadService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/LocalImageUploadService.java
@@ -4,6 +4,7 @@
 import com.sinata.appointment.constants.BizErrorCode;
 import com.sinata.appointment.exception.BizException;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
@@ -41,9 +42,9 @@
         if (!target.getParent().equals(uploadDir)) {
             throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "图片上传路径非法");
         }
-        try {
+        try (InputStream in = file.getInputStream()) {
             Files.createDirectories(uploadDir);
-            Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
+            Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
         } catch (IOException ex) {
             throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "图片上传失败");
         }
diff --git a/backend/src/main/java/com/sinata/appointment/service/PostApprovalOrchestrationService.java b/backend/src/main/java/com/sinata/appointment/service/PostApprovalOrchestrationService.java
index a59f36b..2603679 100644
--- a/backend/src/main/java/com/sinata/appointment/service/PostApprovalOrchestrationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/PostApprovalOrchestrationService.java
@@ -120,6 +120,15 @@
             visitor.setPlateNo(driverPlateMap.get(visitor.getMpVisitorId()));
             String ticketUuid = UUID.randomUUID().toString().replace("-", "");
             String visitorIdCardPlain = resolveVisitorIdCardPlain(visitor, visitorProfile);
+            QrcodeTicket ticket = new QrcodeTicket();
+            ticket.setUuid(ticketUuid);
+            ticket.setOrderId(order.getId());
+            ticket.setMpVisitorId(visitor.getMpVisitorId());
+            ticket.setPayload("{}");
+            ticket.setTicketStatus(QrcodeTicketStatus.UNUSED);
+            ticket.setGenerateTime(generateTime);
+            ticket.setExpireTime(expireTime);
+            qrcodeTicketMapper.insert(ticket);
             String payload =
                     integrationHikvisionService.requestQrcodePayload(
                             order,
@@ -131,24 +140,18 @@
                             generateTime,
                             expireTime,
                             validMinutes,
-                            ticketUuid,
+                            String.valueOf(ticket.getId()),
                             visitorIdCardPlain);
-            QrcodeTicket ticket = new QrcodeTicket();
-            ticket.setUuid(ticketUuid);
-            ticket.setOrderId(order.getId());
-            ticket.setMpVisitorId(visitor.getMpVisitorId());
             ticket.setPayload(payload);
-            ticket.setTicketStatus(QrcodeTicketStatus.UNUSED);
-            ticket.setGenerateTime(generateTime);
-            ticket.setExpireTime(expireTime);
-            qrcodeTicketMapper.insert(ticket);
+            qrcodeTicketMapper.updateById(ticket);
             existingTicketMap.put(visitor.getMpVisitorId(), ticket);
             createdCount++;
             log.info(
-                    "{} raw-payload orderId={} mpVisitorId={} ticketUuid={} payloadLength={}",
+                    "{} raw-payload orderId={} mpVisitorId={} ticketId={} ticketUuid={} payloadLength={}",
                     IntegrationConstant.HIKVISION_QRCODE_EVENT,
                     order.getId(),
                     visitor.getMpVisitorId(),
+                    ticket.getId(),
                     ticketUuid,
                     payload == null ? 0 : payload.length());
         }
diff --git a/backend/src/main/java/com/sinata/appointment/service/StaffManageApplicationService.java b/backend/src/main/java/com/sinata/appointment/service/StaffManageApplicationService.java
index 7e8acfc..47c1d72 100644
--- a/backend/src/main/java/com/sinata/appointment/service/StaffManageApplicationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/StaffManageApplicationService.java
@@ -99,9 +99,9 @@
         Path dir = resolveAvatarUploadDir();
         String fileName = UUID.randomUUID().toString().replace("-", "") + ext;
         Path target = dir.resolve(fileName);
-        try {
+        try (InputStream in = file.getInputStream()) {
             Files.createDirectories(dir);
-            Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
+            Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
         } catch (IOException ex) {
             throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "头像上传失败");
         }
@@ -156,7 +156,7 @@
                 q.notIn(MpUser::getId, inGroup);
             }
         }
-        q.orderByDesc(MpUser::getCreateTime);
+        q.orderByAsc(MpUser::getNickName).orderByAsc(MpUser::getId);
         Page<MpUser> p = mpUserMapper.selectPage(new Page<>(page, size), q);
         Set<Long> deptIds = new HashSet<>();
         Set<Long> userIds = new HashSet<>();
diff --git a/backend/src/main/java/com/sinata/appointment/service/VisitorUserManageApplicationService.java b/backend/src/main/java/com/sinata/appointment/service/VisitorUserManageApplicationService.java
index af0883c..261a561 100644
--- a/backend/src/main/java/com/sinata/appointment/service/VisitorUserManageApplicationService.java
+++ b/backend/src/main/java/com/sinata/appointment/service/VisitorUserManageApplicationService.java
@@ -1,6 +1,8 @@
 package com.sinata.appointment.service;
 
+import com.alibaba.excel.ExcelWriter;
 import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.write.metadata.WriteSheet;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -20,7 +22,8 @@
 import com.sinata.appointment.util.MobileMaskUtil;
 import com.sinata.appointment.util.PhoneNormalize;
 import com.sinata.appointment.util.UuidAssert;
-import java.io.ByteArrayOutputStream;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -62,29 +65,60 @@
         return result.convert(user -> toListVo(user, bookingCountMap));
     }
 
-    public byte[] exportExcel(
+    public void exportExcel(
+            HttpServletResponse response,
             String userName,
             String mobile,
             String companyName,
             LocalDate registerStartDate,
             LocalDate registerEndDate,
-            Integer status) {
-        List<MpUser> users =
-                mpUserMapper.selectList(
-                        buildVisitorQuery(
-                                userName, mobile, companyName, registerStartDate, registerEndDate, status));
-        List<Long> userIds = users.stream().map(MpUser::getId).filter(Objects::nonNull).toList();
-        Map<Long, Long> bookingCountMap = loadBookingCountMap(userIds);
-        List<VisitorUserExportRow> rows =
-                users.stream().map(user -> toExportRow(user, bookingCountMap)).toList();
-        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
-            EasyExcel.write(bos, VisitorUserExportRow.class).sheet("访客信息").doWrite(rows);
-            return bos.toByteArray();
-        } catch (Exception e) {
-            return new byte[0];
+            Integer status)
+            throws IOException {
+        prepareExcelResponse(response, "visitor-users.xlsx");
+        long pageNo = 1L;
+        long pageSize = 500L;
+        boolean wroteAnyPage = false;
+        try (ExcelWriter writer =
+                EasyExcel.write(response.getOutputStream(), VisitorUserExportRow.class).build()) {
+            WriteSheet sheet = EasyExcel.writerSheet("访客信息").build();
+            while (true) {
+                IPage<MpUser> page =
+                        mpUserMapper.selectPage(
+                                new Page<>(pageNo, pageSize),
+                                buildVisitorQuery(
+                                        userName,
+                                        mobile,
+                                        companyName,
+                                        registerStartDate,
+                                        registerEndDate,
+                                        status));
+                List<MpUser> users = page.getRecords();
+                if (users == null || users.isEmpty()) {
+                    break;
+                }
+                List<Long> userIds = users.stream().map(MpUser::getId).filter(Objects::nonNull).toList();
+                Map<Long, Long> bookingCountMap = loadBookingCountMap(userIds);
+                List<VisitorUserExportRow> rows =
+                        users.stream().map(user -> toExportRow(user, bookingCountMap)).toList();
+                writer.write(rows, sheet);
+                wroteAnyPage = true;
+                if (pageNo * pageSize >= page.getTotal()) {
+                    break;
+                }
+                pageNo++;
+            }
+            if (!wroteAnyPage) {
+                writer.write(List.of(), sheet);
+            }
         }
     }
 
+    private void prepareExcelResponse(HttpServletResponse response, String filename) {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("UTF-8");
+        response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
+    }
+
     public VisitorUserDetailVo detail(Long id, String uuid) {
         MpUser user = loadVisitorUserWithUuid(id, uuid);
         VisitorUserDetailVo vo = new VisitorUserDetailVo();
diff --git a/yuyueMiniProgram/src/pages/appointment/index.vue b/yuyueMiniProgram/src/pages/appointment/index.vue
index d58af96..270c74a 100644
--- a/yuyueMiniProgram/src/pages/appointment/index.vue
+++ b/yuyueMiniProgram/src/pages/appointment/index.vue
@@ -1563,7 +1563,7 @@
                   <view v-if="formData.vehicleEntry" class="w-[16rpx] h-[16rpx] rounded-full bg-[#1677FF]">
                   </view>
                 </view>
-                <text class="ml-[12rpx] text-[28rpx]" :class="formData.vehicleEntry ? 'text-[#333333]' : 'text-[#999999]'">车辆进场</text>
+                <text class="ml-[12rpx] text-[28rpx]" :class="formData.vehicleEntry ? 'text-[#333333]' : 'text-[#999999]'">接送货</text>
               </view>
             </view>
           </view>
diff --git a/yuyueMiniProgram/src/pages/index/index.vue b/yuyueMiniProgram/src/pages/index/index.vue
index 25fe871..dbc8af2 100644
--- a/yuyueMiniProgram/src/pages/index/index.vue
+++ b/yuyueMiniProgram/src/pages/index/index.vue
@@ -14,6 +14,10 @@
   qrCodes: VisitorCodeDisplayTicket[]
 }
 
+function wait(ms: number) {
+  return new Promise(resolve => setTimeout(resolve, ms))
+}
+
 const systemInfo = uni.getSystemInfoSync()
 const { safeAreaInsets } = systemInfo
 const parkFlowBottomPx = Math.max(24, Math.round(systemInfo.windowHeight / 8))
@@ -72,9 +76,9 @@
 )
 const visitorCodeCardMinHeightClass = computed(() => {
   if (hasLongAccessAreaText.value) {
-    return currentOrderHasMultipleTickets.value ? 'min-h-[900rpx]' : 'min-h-[860rpx]'
+    return currentOrderHasMultipleTickets.value ? 'min-h-[940rpx]' : 'min-h-[900rpx]'
   }
-  return currentOrderHasMultipleTickets.value ? 'min-h-[804rpx]' : 'min-h-[760rpx]'
+  return currentOrderHasMultipleTickets.value ? 'min-h-[844rpx]' : 'min-h-[800rpx]'
 })
 const currentTicket = computed(() => {
   if (!currentOrder.value) {
@@ -259,7 +263,7 @@
     errorCorrectionLevel: 'M',
     margin: 1,
   })
-  const size = 300
+  const size = 400
   const moduleCount = qr.modules.size
   const tileSize = size / moduleCount
   const canvasId = 'visitorQrCanvas'
@@ -285,14 +289,16 @@
     }
 
     context.draw(false, () => {
-      uni.canvasToTempFilePath({
-        canvasId,
-        width: size,
-        height: size,
-        destWidth: size * 2,
-        destHeight: size * 2,
-        success: res => resolve(res.tempFilePath),
-        fail: reject,
+      void wait(500).then(() => {
+        uni.canvasToTempFilePath({
+          canvasId,
+          width: size,
+          height: size,
+          destWidth: size * 2,
+          destHeight: size * 2,
+          success: res => resolve(res.tempFilePath),
+          fail: reject,
+        })
       })
     })
   })
@@ -671,7 +677,7 @@
                 />
                 <image
                   :src="currentOrder?.orderId === order.orderId ? currentTicket?.qrCode : order.qrCodes[0]?.qrCode"
-                  class="h-[360rpx] w-[360rpx]"
+                  class="h-[400rpx] w-[400rpx]"
                   mode="aspectFit"
                 />
                 <image
@@ -795,8 +801,8 @@
   position: fixed;
   left: -9999px;
   top: -9999px;
-  width: 300px;
-  height: 300px;
+  width: 400px;
+  height: 400px;
   pointer-events: none;
 }
 </style>
diff --git a/yuyueMiniProgram/src/pages/visitorCode/index.vue b/yuyueMiniProgram/src/pages/visitorCode/index.vue
index 2282eae..afe3fd9 100644
--- a/yuyueMiniProgram/src/pages/visitorCode/index.vue
+++ b/yuyueMiniProgram/src/pages/visitorCode/index.vue
@@ -7,6 +7,10 @@
   qrCode: string
 }
 
+function wait(ms: number) {
+  return new Promise(resolve => setTimeout(resolve, ms))
+}
+
 definePage({
   name: 'visitorCode',
   layout: 'default',
@@ -56,7 +60,7 @@
     errorCorrectionLevel: 'M',
     margin: 1,
   })
-  const size = 400
+  const size = 448
   const moduleCount = qr.modules.size
   const tileSize = size / moduleCount
   const canvasId = 'sharedVisitorQrCanvas'
@@ -82,14 +86,16 @@
     }
 
     context.draw(false, () => {
-      uni.canvasToTempFilePath({
-        canvasId,
-        width: size,
-        height: size,
-        destWidth: size * 2,
-        destHeight: size * 2,
-        success: res => resolve(res.tempFilePath),
-        fail: reject,
+      void wait(500).then(() => {
+        uni.canvasToTempFilePath({
+          canvasId,
+          width: size,
+          height: size,
+          destWidth: size * 2,
+          destHeight: size * 2,
+          success: res => resolve(res.tempFilePath),
+          fail: reject,
+        })
       })
     })
   })
@@ -332,8 +338,8 @@
   position: fixed;
   left: -9999px;
   top: -9999px;
-  width: 400px;
-  height: 400px;
+  width: 448px;
+  height: 448px;
   opacity: 0;
   pointer-events: none;
 }
diff --git a/yuyueMiniProgram/src/pages/visitorCodeShare/index.vue b/yuyueMiniProgram/src/pages/visitorCodeShare/index.vue
index edd467f..c18ffd6 100644
--- a/yuyueMiniProgram/src/pages/visitorCodeShare/index.vue
+++ b/yuyueMiniProgram/src/pages/visitorCodeShare/index.vue
@@ -7,6 +7,10 @@
   qrCode: string
 }
 
+function wait(ms: number) {
+  return new Promise(resolve => setTimeout(resolve, ms))
+}
+
 interface VisitorCodeDisplayOrder extends Omit<MiniMyVisitorCodeOrderItem, 'qrCodes'> {
   qrCodes: VisitorCodeDisplayTicket[]
 }
@@ -19,7 +23,6 @@
   },
 })
 
-const router = useRouter()
 const sysInfo = uni.getSystemInfoSync()
 const statusBarHeight = sysInfo.statusBarHeight || 0
 const navBarHeight = 44
@@ -30,7 +33,22 @@
 const currentTicket = computed(() => order.value?.qrCodes?.[0] || null)
 
 function handleBack() {
-  router.back()
+  const pages = getCurrentPages()
+  if (pages.length > 1) {
+    uni.navigateBack({
+      delta: 1,
+      fail: () => {
+        uni.switchTab({ url: '/pages/index/index' })
+      },
+    })
+    return
+  }
+  uni.switchTab({
+    url: '/pages/index/index',
+    fail: () => {
+      uni.reLaunch({ url: '/pages/index/index' })
+    },
+  })
 }
 
 function ticketTimeText() {
@@ -49,7 +67,7 @@
     errorCorrectionLevel: 'M',
     margin: 1,
   })
-  const size = 400
+  const size = 448
   const moduleCount = qr.modules.size
   const tileSize = size / moduleCount
   const canvasId = 'sharedSingleVisitorQrCanvas'
@@ -75,14 +93,16 @@
     }
 
     context.draw(false, () => {
-      uni.canvasToTempFilePath({
-        canvasId,
-        width: size,
-        height: size,
-        destWidth: size * 2,
-        destHeight: size * 2,
-        success: res => resolve(res.tempFilePath),
-        fail: reject,
+      void wait(500).then(() => {
+        uni.canvasToTempFilePath({
+          canvasId,
+          width: size,
+          height: size,
+          destWidth: size * 2,
+          destHeight: size * 2,
+          success: res => resolve(res.tempFilePath),
+          fail: reject,
+        })
       })
     })
   })
@@ -286,8 +306,8 @@
   position: fixed;
   left: -9999px;
   top: -9999px;
-  width: 400px;
-  height: 400px;
+  width: 448px;
+  height: 448px;
   opacity: 0;
   pointer-events: none;
 }

--
Gitblit v1.7.1