diff --git a/config/env.ts b/config/env.ts
index de8e281..c3f95e2 100644
--- a/config/env.ts
+++ b/config/env.ts
@@ -2,7 +2,7 @@
export const ENV_CONFIG = {
// 开发环境
development: {
- API_BASE_URL: 'https://cms-api.websoft.top/api',
+ API_BASE_URL: 'http://127.0.0.1:9200/api',
APP_NAME: '开发环境',
DEBUG: 'true',
},
diff --git a/docs/统一扫码功能说明.md b/docs/统一扫码功能说明.md
new file mode 100644
index 0000000..c4036e5
--- /dev/null
+++ b/docs/统一扫码功能说明.md
@@ -0,0 +1,182 @@
+# 统一扫码功能说明
+
+## 📋 功能概述
+
+本项目实现了统一扫码入口,支持多种类型的二维码识别和处理:
+
+1. **二维码登录** - 扫码确认后台管理系统登录
+2. **礼品卡核销** - 管理员扫码核销礼品卡
+3. **礼品卡兑换** - 用户扫码兑换礼品卡
+4. **车辆查询** - 扫码查询车辆信息
+5. **未知类型** - 提供选择处理方式
+
+## 🔧 技术实现
+
+### 核心组件
+
+#### 1. API接口层 (`src/api/qrLogin/`)
+- `model/index.ts` - 类型定义
+- `index.ts` - API接口和扫码结果解析逻辑
+
+#### 2. 统一扫码组件 (`src/components/UniversalScanner.tsx`)
+- `useUniversalScanner` Hook - 提供扫码功能
+- 智能识别不同类型的二维码
+- 权限控制和错误处理
+
+#### 3. 用户界面集成
+- `UserCard.tsx` - 移除管理员权限限制,所有登录用户可见扫码按钮
+- `verification.tsx` - 支持从统一扫码传递参数
+
+## 🎯 使用方法
+
+### 在组件中使用统一扫码
+
+```typescript
+import { useUniversalScanner } from '@/components/UniversalScanner';
+
+function MyComponent() {
+ const { startScan } = useUniversalScanner({
+ onScanSuccess: (result) => {
+ console.log('扫码成功:', result);
+ },
+ onScanError: (error) => {
+ console.error('扫码失败:', error);
+ }
+ });
+
+ return (
+
+ );
+}
+```
+
+### 扫码结果类型
+
+```typescript
+export type ScanResultType =
+ | 'qr-login' // 二维码登录
+ | 'gift-verification' // 礼品卡核销
+ | 'gift-redeem' // 礼品卡兑换
+ | 'vehicle-query' // 车辆查询
+ | 'unknown'; // 未知类型
+```
+
+## 🔍 二维码格式识别规则
+
+### 1. 二维码登录
+- 格式:`qr-login:token` 或纯token(32位以上字符串)
+- 示例:`qr-login:abc123def456...` 或 `abc123def456ghi789...`
+
+### 2. 礼品卡核销
+- 格式:JSON格式,包含 `businessType: 'gift'`
+- 示例:`{"businessType":"gift","token":"xxx","data":"yyy"}`
+
+### 3. 礼品卡兑换
+- 格式:6位字母数字组合
+- 示例:`ABC123`、`XYZ789`
+
+### 4. 车辆查询
+- 格式:以 `vehicle-` 或 `car-` 开头
+- 示例:`vehicle-12345`、`car-abc123`
+
+### 5. URL格式
+- 支持包含特定参数的URL
+- 二维码登录:包含 `qr-login-token` 参数
+- 礼品卡:包含 `gift-code` 参数
+
+## 🛡️ 权限控制
+
+### 用户权限
+- **所有已登录用户**:可以看到扫码按钮
+- **二维码登录**:需要登录状态
+- **礼品卡兑换**:无需特殊权限
+- **车辆查询**:无需特殊权限
+
+### 管理员权限
+- **礼品卡核销**:仅管理员可用
+
+## 🔄 处理流程
+
+### 二维码登录流程
+1. 扫码获取token
+2. 调用 `scanQrCode(token)` 更新状态为已扫码
+3. 调用 `wechatMiniProgramConfirm()` 确认登录
+4. 显示成功提示
+
+### 礼品卡核销流程
+1. 检查管理员权限
+2. 跳转到核销页面并传递扫码数据
+3. 在核销页面处理解密和验证
+
+### 礼品卡兑换流程
+1. 直接跳转到兑换页面
+2. 传递兑换码参数
+
+## 🚨 错误处理
+
+### 常见错误类型
+- **权限不足**:非管理员尝试核销
+- **未登录**:需要登录的功能
+- **二维码过期**:登录token过期
+- **无效二维码**:格式不正确
+- **网络错误**:API调用失败
+
+### 错误提示
+- 自动显示Toast提示
+- 根据错误类型显示不同消息
+- 支持自定义错误处理回调
+
+## 📱 用户体验
+
+### 成功提示
+- **二维码登录**:显示确认弹窗,提示在电脑端查看
+- **其他功能**:显示成功Toast并跳转相应页面
+
+### 未知类型处理
+- 显示操作选择弹窗
+- 提供复制、作为礼品卡码、作为车辆码等选项
+
+## 🔧 配置选项
+
+### useUniversalScanner 参数
+```typescript
+interface UniversalScannerProps {
+ /** 扫码成功回调 */
+ onScanSuccess?: (result: ScanResultParsed) => void;
+ /** 扫码失败回调 */
+ onScanError?: (error: string) => void;
+ /** 是否显示处理结果提示 */
+ showToast?: boolean;
+}
+```
+
+## 🧪 测试建议
+
+### 测试场景
+1. **不同用户权限**:普通用户、管理员
+2. **不同二维码类型**:登录、核销、兑换、车辆查询
+3. **错误情况**:过期、无效、网络错误
+4. **边界情况**:未登录、权限不足
+
+### 测试数据
+- 有效的登录token
+- 有效的礼品卡核销JSON
+- 有效的兑换码(6位)
+- 有效的车辆查询码
+- 无效/过期的各种码
+
+## 📝 注意事项
+
+1. **安全性**:登录token应该有过期时间
+2. **性能**:扫码解析逻辑应该高效
+3. **用户体验**:错误提示应该清晰明确
+4. **扩展性**:新增扫码类型时只需修改解析逻辑
+
+## 🔄 后续优化
+
+1. **缓存机制**:缓存扫码结果避免重复处理
+2. **统计功能**:记录扫码使用情况
+3. **批量处理**:支持连续扫码
+4. **离线支持**:部分功能支持离线处理
diff --git a/java/auto/controller/QrLoginController.java b/java/auto/controller/QrLoginController.java
new file mode 100644
index 0000000..d296d82
--- /dev/null
+++ b/java/auto/controller/QrLoginController.java
@@ -0,0 +1,104 @@
+package com.gxwebsoft.auto.controller;
+
+import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
+import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
+import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
+import com.gxwebsoft.auto.service.QrLoginService;
+import com.gxwebsoft.common.core.web.BaseController;
+import com.gxwebsoft.common.core.web.ApiResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+/**
+ * 认证模块
+ *
+ * @author 科技小王子
+ * @since 2025-03-06 22:50:25
+ */
+@Tag(name = "认证模块")
+@RestController
+@RequestMapping("/api/qr-login")
+public class QrLoginController extends BaseController {
+
+ @Autowired
+ private QrLoginService qrLoginService;
+
+ /**
+ * 生成扫码登录token
+ */
+ @Operation(summary = "生成扫码登录token")
+ @PostMapping("/generate")
+ public ApiResult> generateQrLoginToken() {
+ try {
+ QrLoginGenerateResponse response = qrLoginService.generateQrLoginToken();
+ return success("生成成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 检查扫码登录状态
+ */
+ @Operation(summary = "检查扫码登录状态")
+ @GetMapping("/status/{token}")
+ public ApiResult> checkQrLoginStatus(
+ @Parameter(description = "扫码登录token") @PathVariable String token) {
+ try {
+ QrLoginStatusResponse response = qrLoginService.checkQrLoginStatus(token);
+ return success("查询成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 确认扫码登录
+ */
+ @Operation(summary = "确认扫码登录")
+ @PostMapping("/confirm")
+ public ApiResult> confirmQrLogin(@Valid @RequestBody QrLoginConfirmRequest request) {
+ try {
+ QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
+ return success("确认成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 扫码操作(可选接口,用于移动端扫码后更新状态)
+ */
+ @Operation(summary = "扫码操作")
+ @PostMapping("/scan/{token}")
+ public ApiResult> scanQrCode(@Parameter(description = "扫码登录token") @PathVariable String token) {
+ try {
+ boolean result = qrLoginService.scanQrCode(token);
+ return success("操作成功", result);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 微信小程序扫码登录确认(便捷接口)
+ */
+ @Operation(summary = "微信小程序扫码登录确认")
+ @PostMapping("/wechat-confirm")
+ public ApiResult> wechatMiniProgramConfirm(@Valid @RequestBody QrLoginConfirmRequest request) {
+ try {
+ // 设置平台为微信小程序
+ request.setPlatform("miniprogram");
+ QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
+ return success("微信小程序登录确认成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+}
diff --git a/java/auto/dto/QrLoginConfirmRequest.java b/java/auto/dto/QrLoginConfirmRequest.java
new file mode 100644
index 0000000..f3b423e
--- /dev/null
+++ b/java/auto/dto/QrLoginConfirmRequest.java
@@ -0,0 +1,50 @@
+package com.gxwebsoft.auto.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 扫码登录确认请求
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Data
+@Schema(description = "扫码登录确认请求")
+public class QrLoginConfirmRequest {
+
+ @Schema(description = "扫码登录token")
+ @NotBlank(message = "token不能为空")
+ private String token;
+
+ @Schema(description = "用户ID")
+ private Integer userId;
+
+ @Schema(description = "登录平台: web-网页端, app-移动应用, miniprogram-微信小程序")
+ private String platform;
+
+ @Schema(description = "微信小程序相关信息")
+ private WechatMiniProgramInfo wechatInfo;
+
+ /**
+ * 微信小程序信息
+ */
+ @Data
+ @Schema(description = "微信小程序信息")
+ public static class WechatMiniProgramInfo {
+ @Schema(description = "微信openid")
+ private String openid;
+
+ @Schema(description = "微信unionid")
+ private String unionid;
+
+ @Schema(description = "微信昵称")
+ private String nickname;
+
+ @Schema(description = "微信头像")
+ private String avatar;
+ }
+
+}
diff --git a/java/auto/dto/QrLoginData.java b/java/auto/dto/QrLoginData.java
new file mode 100644
index 0000000..563bf1d
--- /dev/null
+++ b/java/auto/dto/QrLoginData.java
@@ -0,0 +1,55 @@
+package com.gxwebsoft.auto.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 扫码登录数据模型
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class QrLoginData {
+
+ /**
+ * 扫码登录token
+ */
+ private String token;
+
+ /**
+ * 状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期
+ */
+ private String status;
+
+ /**
+ * 用户ID(扫码确认后设置)
+ */
+ private Integer userId;
+
+ /**
+ * 用户名(扫码确认后设置)
+ */
+ private String username;
+
+ /**
+ * 创建时间
+ */
+ private LocalDateTime createTime;
+
+ /**
+ * 过期时间
+ */
+ private LocalDateTime expireTime;
+
+ /**
+ * JWT访问令牌(确认后生成)
+ */
+ private String accessToken;
+
+}
diff --git a/java/auto/dto/QrLoginGenerateResponse.java b/java/auto/dto/QrLoginGenerateResponse.java
new file mode 100644
index 0000000..f0b69e5
--- /dev/null
+++ b/java/auto/dto/QrLoginGenerateResponse.java
@@ -0,0 +1,29 @@
+package com.gxwebsoft.auto.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 扫码登录生成响应
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(description = "扫码登录生成响应")
+public class QrLoginGenerateResponse {
+
+ @Schema(description = "扫码登录token")
+ private String token;
+
+ @Schema(description = "二维码内容")
+ private String qrCode;
+
+ @Schema(description = "过期时间(秒)")
+ private Long expiresIn;
+
+}
diff --git a/java/auto/dto/QrLoginStatusResponse.java b/java/auto/dto/QrLoginStatusResponse.java
new file mode 100644
index 0000000..1eb0d4a
--- /dev/null
+++ b/java/auto/dto/QrLoginStatusResponse.java
@@ -0,0 +1,32 @@
+package com.gxwebsoft.auto.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 扫码登录状态响应
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(description = "扫码登录状态响应")
+public class QrLoginStatusResponse {
+
+ @Schema(description = "状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期")
+ private String status;
+
+ @Schema(description = "JWT访问令牌(仅在confirmed状态时返回)")
+ private String accessToken;
+
+ @Schema(description = "用户信息(仅在confirmed状态时返回)")
+ private Object userInfo;
+
+ @Schema(description = "剩余过期时间(秒)")
+ private Long expiresIn;
+
+}
diff --git a/java/auto/service/QrLoginService.java b/java/auto/service/QrLoginService.java
new file mode 100644
index 0000000..85ed28f
--- /dev/null
+++ b/java/auto/service/QrLoginService.java
@@ -0,0 +1,46 @@
+package com.gxwebsoft.auto.service;
+
+import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
+import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
+import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
+
+/**
+ * 扫码登录服务接口
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+public interface QrLoginService {
+
+ /**
+ * 生成扫码登录token
+ *
+ * @return QrLoginGenerateResponse
+ */
+ QrLoginGenerateResponse generateQrLoginToken();
+
+ /**
+ * 检查扫码登录状态
+ *
+ * @param token 扫码登录token
+ * @return QrLoginStatusResponse
+ */
+ QrLoginStatusResponse checkQrLoginStatus(String token);
+
+ /**
+ * 确认扫码登录
+ *
+ * @param request 确认请求
+ * @return QrLoginStatusResponse
+ */
+ QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request);
+
+ /**
+ * 扫码操作(更新状态为已扫码)
+ *
+ * @param token 扫码登录token
+ * @return boolean
+ */
+ boolean scanQrCode(String token);
+
+}
diff --git a/java/auto/service/impl/QrLoginServiceImpl.java b/java/auto/service/impl/QrLoginServiceImpl.java
new file mode 100644
index 0000000..34658e8
--- /dev/null
+++ b/java/auto/service/impl/QrLoginServiceImpl.java
@@ -0,0 +1,239 @@
+package com.gxwebsoft.auto.service.impl;
+
+import cn.hutool.core.lang.UUID;
+import cn.hutool.core.util.StrUtil;
+import com.gxwebsoft.auto.dto.*;
+import com.gxwebsoft.auto.service.QrLoginService;
+import com.gxwebsoft.common.core.security.JwtSubject;
+import com.gxwebsoft.common.core.security.JwtUtil;
+import com.gxwebsoft.common.core.utils.JSONUtil;
+import com.gxwebsoft.common.core.utils.RedisUtil;
+import com.gxwebsoft.common.system.entity.User;
+import com.gxwebsoft.common.system.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.TimeUnit;
+
+import static com.gxwebsoft.common.core.constants.RedisConstants.*;
+
+/**
+ * 扫码登录服务实现
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Slf4j
+@Service
+public class QrLoginServiceImpl implements QrLoginService {
+
+ @Autowired
+ private RedisUtil redisUtil;
+
+ @Autowired
+ private UserService userService;
+
+ @Value("${config.jwt.secret:websoft-jwt-secret-key-2025}")
+ private String jwtSecret;
+
+ @Value("${config.jwt.expire:86400}")
+ private Long jwtExpire;
+
+ @Override
+ public QrLoginGenerateResponse generateQrLoginToken() {
+ // 生成唯一的扫码登录token
+ String token = UUID.randomUUID().toString(true);
+
+ // 创建扫码登录数据
+ QrLoginData qrLoginData = new QrLoginData();
+ qrLoginData.setToken(token);
+ qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
+ qrLoginData.setCreateTime(LocalDateTime.now());
+ qrLoginData.setExpireTime(LocalDateTime.now().plusSeconds(QR_LOGIN_TOKEN_TTL));
+
+ // 存储到Redis,设置过期时间
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ redisUtil.set(redisKey, qrLoginData, QR_LOGIN_TOKEN_TTL, TimeUnit.SECONDS);
+
+ log.info("生成扫码登录token: {}", token);
+
+ // 构造二维码内容(这里可以是前端登录页面的URL + token参数)
+ String qrCodeContent = "qr-login:" + token;
+
+ return new QrLoginGenerateResponse(token, qrCodeContent, QR_LOGIN_TOKEN_TTL);
+ }
+
+ @Override
+ public QrLoginStatusResponse checkQrLoginStatus(String token) {
+ if (StrUtil.isBlank(token)) {
+ return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
+ }
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
+
+ if (qrLoginData == null) {
+ return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
+ }
+
+ // 检查是否过期
+ if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
+ // 删除过期的token
+ redisUtil.delete(redisKey);
+ return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
+ }
+
+ // 计算剩余过期时间
+ long expiresIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
+
+ QrLoginStatusResponse response = new QrLoginStatusResponse();
+ response.setStatus(qrLoginData.getStatus());
+ response.setExpiresIn(expiresIn);
+
+ // 如果已确认,返回token和用户信息
+ if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())) {
+ response.setAccessToken(qrLoginData.getAccessToken());
+
+ // 获取用户信息
+ if (qrLoginData.getUserId() != null) {
+ User user = userService.getByIdRel(qrLoginData.getUserId());
+ if (user != null) {
+ // 清除敏感信息
+ user.setPassword(null);
+ response.setUserInfo(user);
+ }
+ }
+
+ // 确认后删除token,防止重复使用
+ redisUtil.delete(redisKey);
+ }
+
+ return response;
+ }
+
+ @Override
+ public QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request) {
+ String token = request.getToken();
+ Integer userId = request.getUserId();
+ String platform = request.getPlatform();
+
+ if (StrUtil.isBlank(token) || userId == null) {
+ throw new RuntimeException("参数不能为空");
+ }
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
+
+ if (qrLoginData == null) {
+ throw new RuntimeException("扫码登录token不存在或已过期");
+ }
+
+ // 检查是否过期
+ if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
+ redisUtil.delete(redisKey);
+ throw new RuntimeException("扫码登录token已过期");
+ }
+
+ // 获取用户信息
+ User user = userService.getByIdRel(userId);
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+
+ // 检查用户状态
+ if (user.getStatus() != null && user.getStatus() != 0) {
+ throw new RuntimeException("用户已被冻结");
+ }
+
+ // 如果是微信小程序登录,处理微信相关信息
+ if ("miniprogram".equals(platform) && request.getWechatInfo() != null) {
+ handleWechatMiniProgramLogin(user, request.getWechatInfo());
+ }
+
+ // 生成JWT token
+ JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
+ String accessToken = JwtUtil.buildToken(jwtSubject, jwtExpire, jwtSecret);
+
+ // 更新扫码登录数据
+ qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
+ qrLoginData.setUserId(userId);
+ qrLoginData.setUsername(user.getUsername());
+ qrLoginData.setAccessToken(accessToken);
+
+ // 更新Redis中的数据
+ redisUtil.set(redisKey, qrLoginData, 60L, TimeUnit.SECONDS); // 给前端60秒时间获取token
+
+ log.info("用户 {} 通过 {} 平台确认扫码登录,token: {}", user.getUsername(),
+ platform != null ? platform : "unknown", token);
+
+ // 清除敏感信息
+ user.setPassword(null);
+
+ return new QrLoginStatusResponse(QR_LOGIN_STATUS_CONFIRMED, accessToken, user, 60L);
+ }
+
+ /**
+ * 处理微信小程序登录相关逻辑
+ */
+ private void handleWechatMiniProgramLogin(User user, QrLoginConfirmRequest.WechatMiniProgramInfo wechatInfo) {
+ // 更新用户的微信信息
+ if (StrUtil.isNotBlank(wechatInfo.getOpenid())) {
+ user.setOpenid(wechatInfo.getOpenid());
+ }
+ if (StrUtil.isNotBlank(wechatInfo.getUnionid())) {
+ user.setUnionid(wechatInfo.getUnionid());
+ }
+ if (StrUtil.isNotBlank(wechatInfo.getNickname()) && StrUtil.isBlank(user.getNickname())) {
+ user.setNickname(wechatInfo.getNickname());
+ }
+ if (StrUtil.isNotBlank(wechatInfo.getAvatar()) && StrUtil.isBlank(user.getAvatar())) {
+ user.setAvatar(wechatInfo.getAvatar());
+ }
+
+ // 更新用户信息到数据库
+ try {
+ userService.updateById(user);
+ log.info("更新用户 {} 的微信小程序信息成功", user.getUsername());
+ } catch (Exception e) {
+ log.warn("更新用户 {} 的微信小程序信息失败: {}", user.getUsername(), e.getMessage());
+ }
+ }
+
+ @Override
+ public boolean scanQrCode(String token) {
+ if (StrUtil.isBlank(token)) {
+ return false;
+ }
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
+
+ if (qrLoginData == null) {
+ return false;
+ }
+
+ // 检查是否过期
+ if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
+ redisUtil.delete(redisKey);
+ return false;
+ }
+
+ // 只有pending状态才能更新为scanned
+ if (QR_LOGIN_STATUS_PENDING.equals(qrLoginData.getStatus())) {
+ qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
+
+ // 计算剩余过期时间
+ long remainingSeconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
+ redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS);
+
+ log.info("扫码登录token {} 状态更新为已扫码", token);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/api/qrLogin/index.ts b/src/api/qrLogin/index.ts
new file mode 100644
index 0000000..a2807bb
--- /dev/null
+++ b/src/api/qrLogin/index.ts
@@ -0,0 +1,197 @@
+import request from '@/utils/request';
+import type { ApiResult } from '@/api';
+import type {
+ QrLoginGenerateResponse,
+ QrLoginStatusResponse,
+ QrLoginConfirmRequest,
+ ScanResultParsed,
+ ScanResultType
+} from './model';
+
+/**
+ * 生成扫码登录token
+ */
+export async function generateQrLoginToken() {
+ const res = await request.post>(
+ '/api/qr-login/generate'
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 检查扫码登录状态
+ */
+export async function checkQrLoginStatus(token: string) {
+ const res = await request.get>(
+ `/api/qr-login/status/${token}`
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 确认扫码登录
+ */
+export async function confirmQrLogin(data: QrLoginConfirmRequest) {
+ const res = await request.post>(
+ '/api/qr-login/confirm',
+ data
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 微信小程序扫码登录确认
+ */
+export async function wechatMiniProgramConfirm(data: QrLoginConfirmRequest) {
+ const res = await request.post>(
+ '/api/qr-login/wechat-confirm',
+ data
+ );
+ if (res.code === 0 && res.data) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 扫码操作(更新状态为已扫码)
+ */
+export async function scanQrCode(token: string) {
+ const res = await request.post>(
+ `/api/qr-login/scan/${token}`
+ );
+ if (res.code === 0) {
+ return res.data;
+ }
+ return Promise.reject(new Error(res.message));
+}
+
+/**
+ * 判断字符串是否为有效的JSON
+ */
+export function isValidJSON(str: string): boolean {
+ try {
+ JSON.parse(str);
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+/**
+ * 解析扫码结果,识别二维码类型
+ */
+export function parseScanResult(scanResult: string): ScanResultParsed {
+ const rawContent = scanResult.trim();
+
+ try {
+ // 1. 尝试解析JSON格式(礼品卡核销)
+ if (isValidJSON(rawContent)) {
+ const json = JSON.parse(rawContent);
+ if (json.businessType === 'gift') {
+ return {
+ type: 'gift-verification',
+ rawContent,
+ data: json,
+ requireAuth: true,
+ requireAdmin: true
+ };
+ }
+ }
+
+ // 2. 检查是否为二维码登录token格式
+ // 假设二维码登录的格式为: qr-login:token 或者纯token(32位以上字符串)
+ if (rawContent.startsWith('qr-login:')) {
+ const token = rawContent.replace('qr-login:', '');
+ return {
+ type: 'qr-login',
+ rawContent,
+ data: { token },
+ requireAuth: true
+ };
+ }
+
+ // 检查是否为纯token格式(32位以上的字母数字组合)
+ if (/^[a-zA-Z0-9-]{32,}$/.test(rawContent)) {
+ return {
+ type: 'qr-login',
+ rawContent,
+ data: { token: rawContent },
+ requireAuth: true
+ };
+ }
+
+ // 3. 检查是否为礼品卡兑换码(6位字母数字组合)
+ if (/^[A-Z0-9]{6}$/.test(rawContent)) {
+ return {
+ type: 'gift-redeem',
+ rawContent,
+ data: { code: rawContent },
+ requireAuth: false
+ };
+ }
+
+ // 4. 检查是否为车辆查询码
+ if (rawContent.startsWith('vehicle-') || rawContent.startsWith('car-')) {
+ return {
+ type: 'vehicle-query' as ScanResultType,
+ rawContent,
+ data: { vehicleId: rawContent },
+ requireAuth: false
+ };
+ }
+
+ // 5. 检查URL格式的二维码
+ if (rawContent.startsWith('http://') || rawContent.startsWith('https://')) {
+ const url = new URL(rawContent);
+
+ // 检查是否包含二维码登录相关参数
+ if (url.searchParams.has('qr-login-token') || url.pathname.includes('/qr-login/')) {
+ const token = url.searchParams.get('qr-login-token') || url.pathname.split('/').pop();
+ return {
+ type: 'qr-login',
+ rawContent,
+ data: { token },
+ requireAuth: true
+ };
+ }
+
+ // 检查是否为礼品卡相关URL
+ if (url.pathname.includes('/gift/') || url.searchParams.has('gift-code')) {
+ const code = url.searchParams.get('gift-code') || url.pathname.split('/').pop();
+ return {
+ type: 'gift-redeem',
+ rawContent,
+ data: { code },
+ requireAuth: false
+ };
+ }
+ }
+
+ // 6. 默认返回未知类型
+ return {
+ type: 'unknown',
+ rawContent,
+ data: { content: rawContent },
+ requireAuth: false
+ };
+
+ } catch (error) {
+ console.error('解析扫码结果失败:', error);
+ return {
+ type: 'unknown',
+ rawContent,
+ data: { content: rawContent, error: error.message },
+ requireAuth: false
+ };
+ }
+}
diff --git a/src/api/qrLogin/model/index.ts b/src/api/qrLogin/model/index.ts
new file mode 100644
index 0000000..c831119
--- /dev/null
+++ b/src/api/qrLogin/model/index.ts
@@ -0,0 +1,85 @@
+/**
+ * 二维码登录相关类型定义
+ */
+
+/**
+ * 二维码登录生成响应
+ */
+export interface QrLoginGenerateResponse {
+ /** 扫码登录token */
+ token: string;
+ /** 二维码内容 */
+ qrCode: string;
+ /** 过期时间(秒) */
+ expiresIn: number;
+}
+
+/**
+ * 二维码登录状态响应
+ */
+export interface QrLoginStatusResponse {
+ /** 登录状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期, cancelled-已取消 */
+ status: 'pending' | 'scanned' | 'confirmed' | 'expired' | 'cancelled';
+ /** 状态描述 */
+ message?: string;
+ /** 登录成功时返回的用户信息 */
+ user?: any;
+ /** 登录成功时返回的访问令牌 */
+ accessToken?: string;
+ /** 剩余过期时间(秒) */
+ remainingTime?: number;
+}
+
+/**
+ * 二维码登录确认请求
+ */
+export interface QrLoginConfirmRequest {
+ /** 扫码登录token */
+ token: string;
+ /** 用户ID */
+ userId?: number;
+ /** 登录平台: web-网页端, app-移动应用, miniprogram-微信小程序 */
+ platform?: string;
+ /** 微信小程序相关信息 */
+ wechatInfo?: WechatMiniProgramInfo;
+}
+
+/**
+ * 微信小程序信息
+ */
+export interface WechatMiniProgramInfo {
+ /** 微信openid */
+ openid?: string;
+ /** 微信unionid */
+ unionid?: string;
+ /** 微信昵称 */
+ nickname?: string;
+ /** 微信头像 */
+ avatar?: string;
+}
+
+/**
+ * 扫码结果类型
+ */
+export type ScanResultType =
+ | 'qr-login' // 二维码登录
+ | 'gift-verification' // 礼品卡核销
+ | 'gift-redeem' // 礼品卡兑换
+ | 'vehicle-query' // 车辆查询
+ | 'unknown'; // 未知类型
+
+/**
+ * 扫码结果解析
+ */
+export interface ScanResultParsed {
+ /** 扫码结果类型 */
+ type: ScanResultType;
+ /** 原始扫码内容 */
+ rawContent: string;
+ /** 解析后的数据 */
+ data: any;
+ /** 是否需要权限验证 */
+ requireAuth?: boolean;
+ /** 是否需要管理员权限 */
+ requireAdmin?: boolean;
+}
diff --git a/src/api/shop/shopDealerReferee/model/index.ts b/src/api/shop/shopDealerReferee/model/index.ts
index 61c1e49..ab12fa5 100644
--- a/src/api/shop/shopDealerReferee/model/index.ts
+++ b/src/api/shop/shopDealerReferee/model/index.ts
@@ -8,8 +8,18 @@ export interface ShopDealerReferee {
id?: number;
// 分销商用户ID
dealerId?: number;
+ // 分销商名称
+ dealerName?: string;
+ // 分销商手机号
+ dealerPhone?: string;
// 用户id(被推荐人)
userId?: number;
+ // 用户头像
+ avatar?: string;
+ // 用户昵称
+ nickname?: string;
+ // 用户手机号
+ phone?: string;
// 推荐关系层级(1,2,3)
level?: number;
// 商城ID
diff --git a/src/app.config.ts b/src/app.config.ts
index bd3fe80..39b34e4 100644
--- a/src/app.config.ts
+++ b/src/app.config.ts
@@ -47,6 +47,12 @@ export default defineAppConfig({
"theme/index"
]
},
+ {
+ "root": "pages/test",
+ "pages": [
+ "scan"
+ ]
+ },
{
"root": "dealer",
"pages": [
diff --git a/src/components/UniversalScanner.tsx b/src/components/UniversalScanner.tsx
new file mode 100644
index 0000000..3ae4b0d
--- /dev/null
+++ b/src/components/UniversalScanner.tsx
@@ -0,0 +1,303 @@
+import React from 'react';
+import Taro from '@tarojs/taro';
+import { parseScanResult, wechatMiniProgramConfirm, scanQrCode } from '@/api/qrLogin';
+import type { ScanResultParsed } from '@/api/qrLogin/model';
+import navTo from '@/utils/common';
+import { useUser } from '@/hooks/useUser';
+
+/**
+ * 统一扫码处理组件
+ */
+export interface UniversalScannerProps {
+ /** 扫码成功回调 */
+ onScanSuccess?: (result: ScanResultParsed) => void;
+ /** 扫码失败回调 */
+ onScanError?: (error: string) => void;
+ /** 是否显示处理结果提示 */
+ showToast?: boolean;
+}
+
+/**
+ * 统一扫码处理Hook
+ */
+export function useUniversalScanner(props: UniversalScannerProps = {}) {
+ const {
+ onScanSuccess,
+ onScanError,
+ showToast = true
+ } = props;
+
+ const { user, isLoggedIn, isAdmin, loginUser } = useUser();
+
+ /**
+ * 启动扫码
+ */
+ const startScan = () => {
+ Taro.scanCode({
+ onlyFromCamera: true,
+ scanType: ['qrCode', 'barCode'],
+ success: (res) => {
+ handleScanResult(res.result);
+ },
+ fail: (err) => {
+ console.error('扫码失败:', err);
+ const errorMsg = '扫码失败,请重试';
+ if (showToast) {
+ Taro.showToast({
+ title: errorMsg,
+ icon: 'error'
+ });
+ }
+ onScanError?.(errorMsg);
+ }
+ });
+ };
+
+ /**
+ * 处理扫码结果
+ */
+ const handleScanResult = async (scanResult: string) => {
+ try {
+ console.log('扫码结果:', scanResult);
+
+ // 解析扫码结果
+ const parsed = parseScanResult(scanResult);
+ console.log('解析结果:', parsed);
+
+ // 权限检查
+ if (parsed.requireAuth && !isLoggedIn) {
+ if (showToast) {
+ Taro.showToast({
+ title: '请先登录',
+ icon: 'error'
+ });
+ }
+ onScanError?.('请先登录');
+ return;
+ }
+
+ if (parsed.requireAdmin && !isAdmin()) {
+ if (showToast) {
+ Taro.showToast({
+ title: '仅管理员可使用此功能',
+ icon: 'error'
+ });
+ }
+ onScanError?.('权限不足');
+ return;
+ }
+
+ // 根据类型处理
+ await handleByType(parsed);
+
+ // 回调
+ onScanSuccess?.(parsed);
+
+ } catch (error) {
+ console.error('处理扫码结果失败:', error);
+ const errorMsg = error.message || '处理扫码结果失败';
+ if (showToast) {
+ Taro.showToast({
+ title: errorMsg,
+ icon: 'error'
+ });
+ }
+ onScanError?.(errorMsg);
+ }
+ };
+
+ /**
+ * 根据类型处理扫码结果
+ */
+ const handleByType = async (parsed: ScanResultParsed) => {
+ switch (parsed.type) {
+ case 'qr-login':
+ await handleQrLogin(parsed);
+ break;
+
+ case 'gift-verification':
+ handleGiftVerification(parsed);
+ break;
+
+ case 'gift-redeem':
+ handleGiftRedeem(parsed);
+ break;
+
+ case 'vehicle-query':
+ handleVehicleQuery(parsed);
+ break;
+
+ case 'unknown':
+ handleUnknownType(parsed);
+ break;
+
+ default:
+ throw new Error(`未支持的扫码类型: ${parsed.type}`);
+ }
+ };
+
+ /**
+ * 处理二维码登录
+ */
+ const handleQrLogin = async (parsed: ScanResultParsed) => {
+ const { token } = parsed.data;
+
+ try {
+ if (showToast) {
+ Taro.showLoading({ title: '正在处理登录...' });
+ }
+
+ // 1. 先调用扫码接口,更新状态为已扫码
+ await scanQrCode(token);
+
+ // 2. 确认登录
+ const confirmData = {
+ token,
+ userId: user?.userId,
+ platform: 'miniprogram',
+ wechatInfo: {
+ openid: user?.openid,
+ unionid: user?.unionid,
+ nickname: user?.nickname || user?.realName,
+ avatar: user?.avatar
+ }
+ };
+
+ const result = await wechatMiniProgramConfirm(confirmData);
+
+ if (result.status === 'confirmed') {
+ if (showToast) {
+ Taro.hideLoading();
+ Taro.showToast({
+ title: '后台管理登录确认成功',
+ icon: 'success',
+ duration: 2000
+ });
+ }
+
+ // 显示成功提示弹窗
+ Taro.showModal({
+ title: '登录成功',
+ content: '您已成功确认后台管理系统登录,请在电脑端查看登录状态。',
+ showCancel: false,
+ confirmText: '知道了'
+ });
+
+ } else {
+ throw new Error(result.message || '登录确认失败');
+ }
+
+ } catch (error) {
+ if (showToast) {
+ Taro.hideLoading();
+ }
+
+ // 根据错误类型显示不同的提示
+ let errorMessage = '登录确认失败';
+ if (error.message?.includes('过期')) {
+ errorMessage = '二维码已过期,请重新生成';
+ } else if (error.message?.includes('无效')) {
+ errorMessage = '无效的登录二维码';
+ } else if (error.message) {
+ errorMessage = error.message;
+ }
+
+ if (showToast) {
+ Taro.showToast({
+ title: errorMessage,
+ icon: 'error',
+ duration: 3000
+ });
+ }
+
+ throw new Error(errorMessage);
+ }
+ };
+
+ /**
+ * 处理礼品卡核销
+ */
+ const handleGiftVerification = (parsed: ScanResultParsed) => {
+ // 跳转到核销页面,并传递扫码数据
+ const encryptedData = encodeURIComponent(JSON.stringify(parsed.data));
+ navTo(`/user/store/verification?scanData=${encryptedData}`, true);
+ };
+
+ /**
+ * 处理礼品卡兑换
+ */
+ const handleGiftRedeem = (parsed: ScanResultParsed) => {
+ const { code } = parsed.data;
+ navTo(`/user/gift/redeem?code=${encodeURIComponent(code)}`, true);
+ };
+
+ /**
+ * 处理车辆查询
+ */
+ const handleVehicleQuery = (parsed: ScanResultParsed) => {
+ const { vehicleId } = parsed.data;
+ navTo(`/hjm/query?id=${vehicleId}`, true);
+ };
+
+ /**
+ * 处理未知类型
+ */
+ const handleUnknownType = (parsed: ScanResultParsed) => {
+ // 显示选择弹窗,让用户选择如何处理
+ Taro.showActionSheet({
+ itemList: [
+ '复制内容',
+ '作为礼品卡兑换码',
+ '作为车辆查询码',
+ '取消'
+ ],
+ success: (res) => {
+ const { tapIndex } = res;
+ switch (tapIndex) {
+ case 0:
+ // 复制内容
+ Taro.setClipboardData({
+ data: parsed.rawContent,
+ success: () => {
+ if (showToast) {
+ Taro.showToast({
+ title: '已复制到剪贴板',
+ icon: 'success'
+ });
+ }
+ }
+ });
+ break;
+
+ case 1:
+ // 作为礼品卡兑换码
+ navTo(`/user/gift/redeem?code=${encodeURIComponent(parsed.rawContent)}`, true);
+ break;
+
+ case 2:
+ // 作为车辆查询码
+ navTo(`/hjm/query?id=${parsed.rawContent}`, true);
+ break;
+ }
+ }
+ });
+ };
+
+ return {
+ startScan,
+ handleScanResult
+ };
+}
+
+/**
+ * 统一扫码处理组件(如果需要作为组件使用)
+ */
+const UniversalScanner: React.FC = (props) => {
+ const { startScan } = useUniversalScanner(props);
+
+ // 这个组件主要提供Hook,不渲染UI
+ // 如果需要可以返回一个扫码按钮
+ return null;
+};
+
+export default UniversalScanner;
diff --git a/src/dealer/team/index.tsx b/src/dealer/team/index.tsx
index 68130d8..a62b934 100644
--- a/src/dealer/team/index.tsx
+++ b/src/dealer/team/index.tsx
@@ -1,7 +1,6 @@
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {Space,Empty, Avatar} from '@nutui/nutui-react-taro'
-import {User} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
@@ -13,11 +12,16 @@ import navTo from "@/utils/common";
interface TeamMemberWithStats extends ShopDealerReferee {
name?: string
avatar?: string
+ nickname?: string;
+ phone?: string;
orderCount?: number
commission?: string
status?: 'active' | 'inactive'
subMembers?: number
joinTime?: string
+ dealerAvatar?: string;
+ dealerName?: string;
+ dealerPhone?: string;
}
const DealerTeam: React.FC = () => {
@@ -40,8 +44,7 @@ const DealerTeam: React.FC = () => {
// 处理团队成员数据
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
...member,
- name: `用户${member.userId}`,
- avatar: '',
+ name: `${member.userId}`,
orderCount: 0,
commission: '0.00',
status: 'active' as const,
@@ -121,13 +124,12 @@ const DealerTeam: React.FC = () => {
}
className="mr-3"
/>
- {member.name}
+ {member.nickname}
{/*{getLevelIcon(Number(member.level))}*/}
{/**/}
diff --git a/src/pages/test/scan.tsx b/src/pages/test/scan.tsx
new file mode 100644
index 0000000..0b4e289
--- /dev/null
+++ b/src/pages/test/scan.tsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import { View, Text } from '@tarojs/components';
+import { Button } from '@nutui/nutui-react-taro';
+import { Scan } from '@nutui/icons-react-taro';
+import Taro from '@tarojs/taro';
+import { useUniversalScanner } from '@/components/UniversalScanner';
+import { useUser } from '@/hooks/useUser';
+
+const ScanTest: React.FC = () => {
+ const { user, isLoggedIn } = useUser();
+
+ const { startScan } = useUniversalScanner({
+ onScanSuccess: (result) => {
+ console.log('测试页面 - 扫码成功:', result);
+ Taro.showModal({
+ title: '扫码成功',
+ content: `类型: ${result.type}\n内容: ${result.rawContent}`,
+ showCancel: false
+ });
+ },
+ onScanError: (error) => {
+ console.error('测试页面 - 扫码失败:', error);
+ Taro.showModal({
+ title: '扫码失败',
+ content: error,
+ showCancel: false
+ });
+ }
+ });
+
+ const handleDirectScan = () => {
+ console.log('直接调用 Taro.scanCode');
+ Taro.scanCode({
+ success: (res) => {
+ console.log('直接扫码成功:', res.result);
+ Taro.showModal({
+ title: '直接扫码成功',
+ content: res.result,
+ showCancel: false
+ });
+ },
+ fail: (err) => {
+ console.error('直接扫码失败:', err);
+ Taro.showModal({
+ title: '直接扫码失败',
+ content: JSON.stringify(err),
+ showCancel: false
+ });
+ }
+ });
+ };
+
+ return (
+
+ 扫码功能测试
+
+
+ 登录状态: {isLoggedIn ? '已登录' : '未登录'}
+
+
+
+ 用户信息: {user ? JSON.stringify(user, null, 2) : '无'}
+
+
+
+ }
+ onClick={() => {
+ console.log('点击了统一扫码按钮');
+ startScan();
+ }}
+ >
+ 统一扫码测试
+
+
+ }
+ onClick={handleDirectScan}
+ >
+ 直接扫码测试
+
+
+
+
+
+ );
+};
+
+export default ScanTest;
diff --git a/src/pages/user/components/UserCard.tsx b/src/pages/user/components/UserCard.tsx
index 3f74407..4d55cdd 100644
--- a/src/pages/user/components/UserCard.tsx
+++ b/src/pages/user/components/UserCard.tsx
@@ -9,6 +9,7 @@ import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {useUser} from "@/hooks/useUser";
import {useUserData} from "@/hooks/useUserData";
+import {useUniversalScanner} from "@/components/UniversalScanner";
function UserCard() {
const {
@@ -20,7 +21,17 @@ function UserCard() {
getDisplayName,
getRoleName
} = useUser();
- const {data} = useUserData()
+ const {data} = useUserData();
+
+ // 统一扫码处理
+ const { startScan } = useUniversalScanner({
+ onScanSuccess: (result) => {
+ console.log('扫码成功:', result);
+ },
+ onScanError: (error) => {
+ console.error('扫码失败:', error);
+ }
+ });
useEffect(() => {
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
@@ -190,7 +201,48 @@ function UserCard() {
) : ''}
- {isAdmin() && navTo('/user/store/verification', true)}/>}
+ {isLoggedIn && (
+ {
+ console.log('扫码按钮被点击了');
+
+ // 检查 Taro.scanCode 是否存在
+ if (typeof Taro.scanCode === 'function') {
+ console.log('Taro.scanCode 函数存在');
+
+ // 直接测试 Taro.scanCode
+ Taro.scanCode({
+ success: (res) => {
+ console.log('直接扫码成功:', res.result);
+ Taro.showModal({
+ title: '扫码成功',
+ content: res.result,
+ showCancel: false
+ });
+ },
+ fail: (err) => {
+ console.error('直接扫码失败:', err);
+ Taro.showModal({
+ title: '扫码失败',
+ content: `错误信息: ${JSON.stringify(err)}`,
+ showCancel: false
+ });
+ }
+ });
+ } else {
+ console.error('Taro.scanCode 函数不存在');
+ Taro.showModal({
+ title: '错误',
+ content: 'Taro.scanCode 函数不存在,请检查 Taro 版本',
+ showCancel: false
+ });
+ }
+ }}
+ className="p-2 bg-blue-100 rounded cursor-pointer"
+ >
+
+
+ )}
navTo('/user/profile/profile', true)}>
{'个人资料'}
diff --git a/src/user/store/verification.tsx b/src/user/store/verification.tsx
index cb2cdb6..6ecc6a5 100644
--- a/src/user/store/verification.tsx
+++ b/src/user/store/verification.tsx
@@ -1,4 +1,4 @@
-import React, {useState} from 'react'
+import React, {useState, useEffect} from 'react'
import {View, Text, Image} from '@tarojs/components'
import {Button, Input} from '@nutui/nutui-react-taro'
import {Scan, Search} from '@nutui/icons-react-taro'
@@ -8,6 +8,7 @@ import {getShopGiftByCode, updateShopGift, decryptQrData} from "@/api/shop/shopG
import {useUser} from "@/hooks/useUser";
import type {ShopGift} from "@/api/shop/shopGift/model";
import {isValidJSON} from "@/utils/jsonUtils";
+import {useUniversalScanner} from "@/components/UniversalScanner";
const StoreVerification: React.FC = () => {
const {
@@ -18,8 +19,53 @@ const StoreVerification: React.FC = () => {
const [giftInfo, setGiftInfo] = useState(null)
const [loading, setLoading] = useState(false)
- // 扫码功能
+ // 统一扫码处理(仅用于管理员权限检查后的扫码)
+ const { startScan } = useUniversalScanner({
+ onScanSuccess: (result) => {
+ console.log('管理员扫码成功:', result);
+ },
+ onScanError: (error) => {
+ console.error('管理员扫码失败:', error);
+ }
+ });
+
+ // 页面加载时检查是否有传递的扫码数据
+ useEffect(() => {
+ const handlePageLoad = async () => {
+ try {
+ // 获取页面参数
+ const instance = Taro.getCurrentInstance();
+ const params = instance.router?.params;
+
+ if (params?.scanData) {
+ // 解析传递过来的扫码数据
+ const scanData = JSON.parse(decodeURIComponent(params.scanData));
+ console.log('接收到扫码数据:', scanData);
+
+ if (scanData.businessType === 'gift') {
+ setLoading(true);
+ await handleDecryptAndVerify(scanData.token, scanData.data);
+ }
+ }
+ } catch (error) {
+ console.error('处理页面参数失败:', error);
+ }
+ };
+
+ handlePageLoad();
+ }, []);
+
+ // 扫码功能(保留原有的直接扫码功能,用于管理员在此页面的直接操作)
const handleScan = () => {
+ // 检查管理员权限
+ if (!isAdmin()) {
+ Taro.showToast({
+ title: '仅管理员可使用核销功能',
+ icon: 'error'
+ });
+ return;
+ }
+
Taro.scanCode({
success: (res) => {
if (res.result) {