Web虚拟卡销售店铺实现方案

1. 项目概述

1.1 项目背景

随着数字经济的发展,虚拟卡(如礼品卡、会员卡、游戏点卡等)的市场需求日益增长。本项目旨在构建一个完整的Web虚拟卡销售平台,包含前端销售系统、后端管理系统和移动端H5支付功能,采用Java作为后端技术栈,Vue.js作为前端框架,并集成微信支付功能。

1.2 系统架构

系统采用前后端分离架构:

  • 前端:Vue.js + Element UI (管理端) + Vant (移动端)
  • 后端:Spring Boot + Spring Security + MyBatis Plus
  • 数据库:MySQL
  • 缓存:Redis
  • 支付:微信支付H5 API

2. 技术选型与环境搭建

2.1 后端技术栈

// pom.xml 主要依赖
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- 数据库相关 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.1</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.8</version>
    </dependency>
    
    <!-- 工具类 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>31.0.1-jre</version>
    </dependency>
    
    <!-- 微信支付SDK -->
    <dependency>
        <groupId>com.github.wechatpay-apiv3</groupId>
        <artifactId>wechatpay-apache-httpclient</artifactId>
        <version>0.4.7</version>
    </dependency>
    
    <!-- 其他 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2.2 前端技术栈

# 管理端前端
vue create admin-frontend
cd admin-frontend
vue add element-ui
npm install axios vue-router vuex --save

# 用户端前端
vue create user-frontend
cd user-frontend
npm install vant axios vue-router vuex --save

2.3 开发环境配置

  1. JDK 1.8+
  2. Maven 3.6+
  3. Node.js 14+
  4. MySQL 5.7+
  5. Redis 5.0+
  6. IDE推荐:IntelliJ IDEA + VS Code

3. 数据库设计

3.1 数据库ER图

主要实体:

  • 用户(User)
  • 虚拟卡产品(CardProduct)
  • 卡密库存(CardSecret)
  • 订单(Order)
  • 支付记录(Payment)
  • 管理员(Admin)

3.2 数据表设计

-- 用户表
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-正常',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`),
  KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 虚拟卡产品表
CREATE TABLE `card_product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL COMMENT '产品名称',
  `category_id` bigint(20) NOT NULL COMMENT '分类ID',
  `description` text COMMENT '产品描述',
  `price` decimal(10,2) NOT NULL COMMENT '售价',
  `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价',
  `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
  `image_url` varchar(255) DEFAULT NULL COMMENT '图片URL',
  `detail_images` text COMMENT '详情图片,JSON数组',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-下架,1-上架',
  `sort_order` int(11) DEFAULT '0' COMMENT '排序权重',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_category` (`category_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='虚拟卡产品表';

-- 卡密库存表
CREATE TABLE `card_secret` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(20) NOT NULL COMMENT '产品ID',
  `card_no` varchar(100) NOT NULL COMMENT '卡号',
  `card_password` varchar(100) NOT NULL COMMENT '卡密',
  `status` tinyint(1) DEFAULT '0' COMMENT '状态:0-未售出,1-已售出,2-已锁定',
  `order_id` bigint(20) DEFAULT NULL COMMENT '订单ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_card_no` (`card_no`),
  KEY `idx_product_id` (`product_id`),
  KEY `idx_status` (`status`),
  KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡密库存表';

-- 订单表
CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(50) NOT NULL COMMENT '订单编号',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
  `payment_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
  `payment_type` tinyint(1) DEFAULT NULL COMMENT '支付方式:1-微信,2-支付宝',
  `status` tinyint(1) DEFAULT '0' COMMENT '订单状态:0-待支付,1-已支付,2-已发货,3-已完成,4-已取消',
  `payment_time` datetime DEFAULT NULL COMMENT '支付时间',
  `complete_time` datetime DEFAULT NULL COMMENT '完成时间',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_status` (`status`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- 订单明细表
CREATE TABLE `order_item` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_id` bigint(20) NOT NULL COMMENT '订单ID',
  `order_no` varchar(50) NOT NULL COMMENT '订单编号',
  `product_id` bigint(20) NOT NULL COMMENT '产品ID',
  `product_name` varchar(100) NOT NULL COMMENT '产品名称',
  `product_image` varchar(255) DEFAULT NULL COMMENT '产品图片',
  `quantity` int(11) NOT NULL COMMENT '购买数量',
  `price` decimal(10,2) NOT NULL COMMENT '单价',
  `total_price` decimal(10,2) NOT NULL COMMENT '总价',
  `card_secret_id` bigint(20) DEFAULT NULL COMMENT '卡密ID',
  `card_no` varchar(100) DEFAULT NULL COMMENT '卡号',
  `card_password` varchar(100) DEFAULT NULL COMMENT '卡密',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_order_id` (`order_id`),
  KEY `idx_order_no` (`order_no`),
  KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';

-- 支付记录表
CREATE TABLE `payment` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_id` bigint(20) NOT NULL COMMENT '订单ID',
  `order_no` varchar(50) NOT NULL COMMENT '订单编号',
  `payment_no` varchar(50) NOT NULL COMMENT '支付流水号',
  `payment_type` tinyint(1) NOT NULL COMMENT '支付方式:1-微信,2-支付宝',
  `payment_amount` decimal(10,2) NOT NULL COMMENT '支付金额',
  `payment_status` tinyint(1) DEFAULT '0' COMMENT '支付状态:0-未支付,1-支付成功,2-支付失败',
  `payment_time` datetime DEFAULT NULL COMMENT '支付时间',
  `callback_time` datetime DEFAULT NULL COMMENT '回调时间',
  `callback_content` text COMMENT '回调内容',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_payment_no` (`payment_no`),
  KEY `idx_order_id` (`order_id`),
  KEY `idx_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';

-- 管理员表
CREATE TABLE `admin` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码',
  `nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
  `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-正常',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';

-- 系统日志表
CREATE TABLE `sys_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `username` varchar(50) DEFAULT NULL COMMENT '用户名',
  `operation` varchar(50) DEFAULT NULL COMMENT '用户操作',
  `method` varchar(200) DEFAULT NULL COMMENT '请求方法',
  `params` text COMMENT '请求参数',
  `time` bigint(20) DEFAULT NULL COMMENT '执行时长(毫秒)',
  `ip` varchar(64) DEFAULT NULL COMMENT 'IP地址',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表';

4. 后端实现

4.1 Spring Boot项目结构

src/main/java/com/virtualcard/
├── config/                # 配置类
│   ├── SecurityConfig.java
│   ├── SwaggerConfig.java
│   ├── RedisConfig.java
│   └── WebMvcConfig.java
├── constant/              # 常量类
│   ├── OrderStatus.java
│   ├── PaymentType.java
│   └── RedisKey.java
├── controller/            # 控制器
│   ├── api/               # 用户API接口
│   │   ├── AuthController.java
│   │   ├── CardController.java
│   │   ├── OrderController.java
│   │   └── PaymentController.java
│   └── admin/             # 管理端接口
│       ├── AdminAuthController.java
│       ├── AdminCardController.java
│       ├── AdminOrderController.java
│       └── AdminUserController.java
├── dao/                   # 数据访问层
│   ├── entity/            # 实体类
│   │   ├── User.java
│   │   ├── CardProduct.java
│   │   ├── CardSecret.java
│   │   ├── Order.java
│   │   └── Payment.java
│   └── mapper/            # MyBatis Mapper接口
│       ├── UserMapper.java
│       ├── CardProductMapper.java
│       ├── CardSecretMapper.java
│       ├── OrderMapper.java
│       └── PaymentMapper.java
├── dto/                   # 数据传输对象
│   ├── request/           # 请求DTO
│   │   ├── LoginReq.java
│   │   ├── OrderCreateReq.java
│   │   └── PaymentReq.java
│   └── response/          # 响应DTO
│       ├── ApiResponse.java
│       ├── CardProductRes.java
│       └── OrderRes.java
├── exception/             # 异常处理
│   ├── BusinessException.java
│   └── GlobalExceptionHandler.java
├── service/               # 服务层
│   ├── impl/              # 服务实现
│   │   ├── AuthServiceImpl.java
│   │   ├── CardServiceImpl.java
│   │   ├── OrderServiceImpl.java
│   │   └── PaymentServiceImpl.java
│   └── AuthService.java
│   ├── CardService.java
│   ├── OrderService.java
│   └── PaymentService.java
├── util/                  # 工具类
│   ├── JwtUtil.java
│   ├── RedisUtil.java
│   ├── SnowFlakeUtil.java
│   └── WeChatPayUtil.java
└── VirtualCardApplication.java  # 启动类

4.2 核心功能实现

4.2.1 用户认证与授权
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Autowired
    private JwtAccessDeniedHandler jwtAccessDeniedHandler;
    
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll()
            .antMatchers("/api/payment/callback/**").permitAll()
            .antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs").permitAll()
            .antMatchers("/api/**").authenticated()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .accessDeniedHandler(jwtAccessDeniedHandler)
            .authenticationEntryPoint(jwtAuthenticationEntryPoint);
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

// JwtUtil.java
@Component
public class JwtUtil {
    private static final String SECRET = "your_jwt_secret";
    private static final long EXPIRATION = 86400L; // 24小时
    
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }
    
    public String getUsernameFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
    
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }
    
    private boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
    
    private Date getExpirationDateFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody()
                .getExpiration();
    }
}
4.2.2 虚拟卡管理
// CardServiceImpl.java
@Service
public class CardServiceImpl implements CardService {
    
    @Autowired
    private CardProductMapper cardProductMapper;
    
    @Autowired
    private CardSecretMapper cardSecretMapper;
    
    @Autowired
    private RedisUtil redisUtil;
    
    private static final String CARD_PRODUCT_CACHE_KEY = "card:product:list";
    private static final long CACHE_EXPIRE = 3600; // 1小时
    
    @Override
    public List<CardProductRes> listAllProducts() {
        // 先查缓存
        String cache = redisUtil.get(CARD_PRODUCT_CACHE_KEY);
        if (StringUtils.isNotBlank(cache)) {
            return JSON.parseArray(cache, CardProductRes.class);
        }
        
        // 缓存没有则查数据库
        QueryWrapper<CardProduct> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("status", 1).orderByAsc("sort_order");
        List<CardProduct> products = cardProductMapper.selectList(queryWrapper);
        
        List<CardProductRes> result = products.stream()
                .map(this::convertToRes)
                .collect(Collectors.toList());
        
        // 存入缓存
        redisUtil.set(CARD_PRODUCT_CACHE_KEY, JSON.toJSONString(result), CACHE_EXPIRE);
        return result;
    }
    
    @Override
    @Transactional
    public List<CardSecret> lockCardSecrets(Long productId, int quantity, Long orderId) {
        // 查询可用的卡密
        List<CardSecret> availableSecrets = cardSecretMapper.selectAvailableSecrets(productId, quantity);
        if (availableSecrets.size() < quantity) {
            throw new BusinessException("库存不足");
        }
        
        // 锁定卡密
        List<Long> ids = availableSecrets.stream().map(CardSecret::getId).collect(Collectors.toList());
        cardSecretMapper.lockSecrets(ids, orderId);
        
        // 更新产品库存
        cardProductMapper.decreaseStock(productId, quantity);
        
        // 清除缓存
        redisUtil.del(CARD_PRODUCT_CACHE_KEY);
        
        return availableSecrets;
    }
    
    @Override
    @Transactional
    public void unlockCardSecrets(List<Long> cardSecretIds) {
        if (CollectionUtils.isEmpty(cardSecretIds)) {
            return;
        }
        
        // 查询卡密对应的产品ID和数量
        List<CardSecret> secrets = cardSecretMapper.selectBatchIds(cardSecretIds);
        if (CollectionUtils.isEmpty(secrets)) {
            return;
        }
        
        Map<Long, Long> productCountMap = secrets.stream()
                .collect(Collectors.groupingBy(CardSecret::getProductId, Collectors.counting()));
        
        // 解锁卡密
        cardSecretMapper.unlockSecrets(cardSecretIds);
        
        // 恢复产品库存
        for (Map.Entry<Long, Long> entry : productCountMap.entrySet()) {
            cardProductMapper.increaseStock(entry.getKey(), entry.getValue().intValue());
        }
        
        // 清除缓存
        redisUtil.del(CARD_PRODUCT_CACHE_KEY);
    }
    
    private CardProductRes convertToRes(CardProduct product) {
        CardProductRes res = new CardProductRes();
        BeanUtils.copyProperties(product, res);
        return res;
    }
}
4.2.3 订单服务
// OrderServiceImpl.java
@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private OrderItemMapper orderItemMapper;
    
    @Autowired
    private CardService cardService;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private SnowFlakeUtil snowFlakeUtil;
    
    @Override
    @Transactional
    public OrderRes createOrder(OrderCreateReq req, Long userId) {
        // 生成订单号
        String orderNo = generateOrderNo();
        
        // 锁定卡密
        List<CardSecret> cardSecrets = cardService.lockCardSecrets(req.getProductId(), req.getQuantity(), null);
        
        // 计算总金额
        CardProduct product = cardService.getProductById(req.getProductId());
        BigDecimal totalAmount = product.getPrice().multiply(new BigDecimal(req.getQuantity()));
        
        // 创建订单
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setTotalAmount(totalAmount);
        order.setPaymentAmount(totalAmount);
        order.setStatus(OrderStatus.UNPAID.getCode());
        orderMapper.insert(order);
        
        // 创建订单明细
        List<OrderItem> orderItems = new ArrayList<>();
        for (CardSecret secret : cardSecrets) {
            OrderItem item = new OrderItem();
            item.setOrderId(order.getId());
            item.setOrderNo(orderNo);
            item.setProductId(req.getProductId());
            item.setProductName(product.getName());
            item.setProductImage(product.getImageUrl());
            item.setQuantity(1);
            item.setPrice(product.getPrice());
            item.setTotalPrice(product.getPrice());
            item.setCardSecretId(secret.getId());
            orderItems.add(item);
        }
        
        orderItemMapper.batchInsert(orderItems);
        
        // 更新卡密的订单ID
        List<Long> cardSecretIds = cardSecrets.stream().map(CardSecret::getId).collect(Collectors.toList());
        cardService.updateCardSecretsOrderId(cardSecretIds, order.getId());
        
        // 返回订单信息
        OrderRes res = new OrderRes();
        BeanUtils.copyProperties(order, res);
        res.setItems(orderItems.stream().map(this::convertToItemRes).collect(Collectors.toList()));
        return res;
    }
    
    @Override
    @Transactional
    public void cancelOrder(Long orderId, Long userId) {
        Order order = orderMapper.selectById(orderId);
        if (order == null) {
            throw new BusinessException("订单不存在");
        }
        
        if (!order.getUserId().equals(userId)) {
            throw new BusinessException("无权操作此订单");
        }
        
        if (order.getStatus() != OrderStatus.UNPAID.getCode()) {
            throw new BusinessException("订单状态不允许取消");
        }
        
        // 更新订单状态
        order.setStatus(OrderStatus.CANCELLED.getCode());
        orderMapper.updateById(order);
        
        // 查询订单明细获取卡密ID
        List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
        List<Long> cardSecretIds = items.stream()
                .map(OrderItem::getCardSecretId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        
        // 解锁卡密
        if (!cardSecretIds.isEmpty()) {
            cardService.unlockCardSecrets(cardSecretIds);
        }
    }
    
    @Override
    @Transactional
    public void payOrderSuccess(String orderNo, String paymentNo, BigDecimal paymentAmount, Date paymentTime) {
        Order order = orderMapper.selectByOrderNo(orderNo);
        if (order == null) {
            throw new BusinessException("订单不存在");
        }
        
        if (order.getStatus() != OrderStatus.UNPAID.getCode()) {
            throw new BusinessException("订单状态不正确");
        }
        
        // 更新订单状态
        order.setStatus(OrderStatus.PAID.getCode());
        order.setPaymentTime(paymentTime);
        orderMapper.updateById(order);
        
        // 更新卡密状态为已售出
        List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
        List<Long> cardSecretIds = items.stream()
                .map(OrderItem::getCardSecretId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        
        if (!cardSecretIds.isEmpty()) {
            cardService.sellCardSecrets(cardSecretIds);
        }
        
        // 创建支付记录
        Payment payment = new Payment();
        payment.setOrderId(order.getId());
        payment.setOrderNo(orderNo);
        payment.setPaymentNo(paymentNo);
        payment.setPaymentType(PaymentType.WECHAT.getCode());
        payment.setPaymentAmount(paymentAmount);
        payment.setPaymentStatus(1);
        payment.setPaymentTime(paymentTime);
        payment.setCallbackTime(new Date());
        paymentService.createPayment(payment);
    }
    
    private String generateOrderNo() {
        return "ORD" + snowFlakeUtil.nextId();
    }
    
    private OrderItemRes convertToItemRes(OrderItem item) {
        OrderItemRes res = new OrderItemRes();
        BeanUtils.copyProperties(item, res);
        return res;
    }
}
4.2.4 微信支付集成
// WeChatPayUtil.java
@Component
public class WeChatPayUtil {
    
    @Value("${wechat.pay.appid}")
    private String appId;
    
    @Value("${wechat.pay.mchid}")
    private String mchId;
    
    @Value("${wechat.pay.apikey}")
    private String apiKey;
    
    @Value("${wechat.pay.serialNo}")
    private String serialNo;
    
    @Value("${wechat.pay.privateKey}")
    private String privateKey;
    
    @Value("${wechat.pay.notifyUrl}")
    private String notifyUrl;
    
    private CloseableHttpClient httpClient;
    
    @PostConstruct
    public void init() {
        // 加载商户私钥
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes()));
        
        // 构造HttpClient
        httpClient = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, serialNo, merchantPrivateKey)
                .withValidator(new WechatPay2Validator(apiKey.getBytes()))
                .build();
    }
    
    public Map<String, String> createH5Payment(String orderNo, BigDecimal amount, String description, String clientIp) throws Exception {
        // 构造请求参数
        Map<String, Object> params = new HashMap<>();
        params.put("appid", appId);
        params.put("mchid", mchId);
        params.put("description", description);
        params.put("out_trade_no", orderNo);
        params.put("notify_url", notifyUrl);
        params.put("amount", new HashMap<String, Object>() {{
            put("total", amount.multiply(new BigDecimal(100)).intValue());
            put("currency", "CNY");
        }});
        params.put("scene_info", new HashMap<String, Object>() {{
            put("payer_client_ip", clientIp);
            put("h5_info", new HashMap<String, Object>() {{
                put("type", "Wap");
            }});
        }});
        
        // 发送请求
        HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/h5");
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type", "application/json");
        httpPost.setEntity(new StringEntity(JSON.toJSONString(params), "UTF-8"));
        
        CloseableHttpResponse response = httpClient.execute(httpPost);
        try {
            String responseBody = EntityUtils.toString(response.getEntity());
            if (response.getStatusLine().getStatusCode() == 200) {
                Map<String, String> result = new HashMap<>();
                JSONObject json = JSON.parseObject(responseBody);
                result.put("h5_url", json.getString("h5_url"));
                result.put("prepay_id", json.getString("prepay_id"));
                return result;
            } else {
                throw new BusinessException("微信支付创建失败: " + responseBody);
            }
        } finally {
            response.close();
        }
    }
    
    public boolean verifyNotify(Map<String, String> params, String signature, String serial, String nonce, String timestamp, String body) {
        try {
            // 验证签名
            String message = timestamp + "\n" + nonce + "\n" + body + "\n";
            boolean verifyResult = verifySignature(message.getBytes("utf-8"), serial, signature.getBytes("utf-8")));
            if (!verifyResult) {
                return false;
            }
            
            // 验证订单状态
            JSONObject json = JSON.parseObject(body);
            String orderNo = json.getJSONObject("resource").getString("out_trade_no");
            String tradeState = json.getJSONObject("resource").getString("trade_state");
            
            return "SUCCESS".equals(tradeState);
        } catch (Exception e) {
            return false;
        }
    }
    
    private boolean verifySignature(byte[] message, String serial, byte[] signature) {
        try {
            // 根据证书序列号查询证书
            String cert = getWechatPayCert(serial);
            if (cert == null) {
                return false;
            }
            
            // 加载证书
            X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(cert));
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
            
            // 验证签名
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initVerify(publicKey);
            sign.update(message);
            return sign.verify(signature);
        } catch (Exception e) {
            return false;
        }
    }
    
    private String getWechatPayCert(String serial) {
        // 这里应该实现从微信支付平台获取证书的逻辑
        // 实际项目中应该缓存证书,避免频繁请求
        // 简化实现,返回配置的证书
        return "your_wechat_pay_cert_content";
    }
}

// PaymentController.java
@RestController
@RequestMapping("/api/payment")
public class PaymentController {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private PaymentService paymentService;
    
    @Autowired
    private WeChatPayUtil weChatPayUtil;
    
    @PostMapping("/create")
    public ApiResponse<Map<String, String>> createPayment(@RequestBody PaymentReq req, 
                                                        HttpServletRequest request) {
        // 查询订单
        Order order = orderService.getOrderByNo(req.getOrderNo());
        if (order == null) {
            return ApiResponse.fail("订单不存在");
        }
        
        if (order.getStatus() != OrderStatus.UNPAID.getCode()) {
            return ApiResponse.fail("订单状态不正确");
        }
        
        // 创建微信支付
        try {
            Map<String, String> result = weChatPayUtil.createH5Payment(
                    order.getOrderNo(),
                    order.getPaymentAmount(),
                    "虚拟卡购买-" + order.getOrderNo(),
                    getClientIp(request));
            
            // 保存支付记录
            Payment payment = new Payment();
            payment.setOrderId(order.getId());
            payment.setOrderNo(order.getOrderNo());
            payment.setPaymentNo(result.get("prepay_id"));
            payment.setPaymentType(PaymentType.WECHAT.getCode());
            payment.setPaymentAmount(order.getPaymentAmount());
            payment.setPaymentStatus(0);
            paymentService.createPayment(payment);
            
            return ApiResponse.success(result);
        } catch (Exception e) {
            return ApiResponse.fail("支付创建失败: " + e.getMessage());
        }
    }
    
    @PostMapping("/callback/wechat")
    public String wechatPayCallback(HttpServletRequest request) {
        try {
            // 获取请求头信息
            String signature = request.getHeader("Wechatpay-Signature");
            String serial = request.getHeader("Wechatpay-Serial");
            String nonce = request.getHeader("Wechatpay-Nonce");
            String timestamp = request.getHeader("Wechatpay-Timestamp");
            
            // 获取请求体
            String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
            
            // 验证回调
            if (!weChatPayUtil.verifyNotify(null, signature, serial, nonce, timestamp, body)) {
                return "FAIL";
            }
            
            // 解析回调内容
            JSONObject json = JSON.parseObject(body);
            JSONObject resource = json.getJSONObject("resource");
            String orderNo = resource.getString("out_trade_no");
            String transactionId = resource.getString("transaction_id");
            BigDecimal amount = resource.getJSONObject("amount")
                    .getBigDecimal("total")
                    .divide(new BigDecimal(100));
            Date paymentTime = new Date(resource.getLong("success_time") * 1000);
            
            // 处理支付成功逻辑
            orderService.payOrderSuccess(orderNo, transactionId, amount, paymentTime);
            
            return "SUCCESS";
        } catch (Exception e) {
            return "FAIL";
        }
    }
    
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            int index = ip.indexOf(",");
            if (index != -1) {
                return ip.substring(0, index);
            } else {
                return ip;
            }
        }
        ip = request.getHeader("X-Real-IP");
        if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            return ip;
        }
        return request.getRemoteAddr();
    }
}

5. 前端实现

5.1 用户端前端实现

5.1.1 项目结构
src/
├── api/                  # API请求
│   ├── auth.js           # 认证相关API
│   ├── card.js           # 虚拟卡相关API
│   ├── order.js          # 订单相关API
│   └── payment.js        # 支付相关API
├── assets/               # 静态资源
│   ├── css/              # 全局样式
│   └── images/           # 图片资源
├── components/           # 公共组件
│   ├── CardItem.vue      # 卡产品项组件
│   ├── Header.vue        # 头部组件
│   ├── Footer.vue        # 底部组件
│   └── Loading.vue       # 加载组件
├── router/               # 路由配置
│   └── index.js          # 路由定义
├── store/                # Vuex状态管理
│   ├── modules/          # 模块化状态
│   │   ├── auth.js       # 认证模块
│   │   ├── card.js       # 虚拟卡模块
│   │   └── order.js      # 订单模块
│   └── index.js          # 主入口
├── utils/                # 工具函数
│   ├── request.js        # axios封装
│   ├── auth.js           # 认证工具
│   └── wechat.js         # 微信相关工具
├── views/                # 页面组件
│   ├── auth/             # 认证相关页面
│   │   ├── Login.vue     # 登录页
│   │   └── Register.vue  # 注册页
│   ├── card/             # 虚拟卡相关页面
│   │   ├── List.vue      # 卡列表页
│   │   └── Detail.vue    # 卡详情页
│   ├── order/            # 订单相关页面
│   │   ├── Create.vue    # 订单创建页
│   │   ├── Detail.vue    # 订单详情页
│   │   └── List.vue      # 订单列表页
│   ├── payment/          # 支付相关页面
│   │   └── Pay.vue       # 支付页
│   ├── Home.vue          # 首页
│   └── User.vue          # 用户中心页
├── App.vue               # 根组件
└── main.js               # 应用入口
5.1.2 核心页面实现

虚拟卡列表页 (Card/List.vue)

<template>
  <div class="card-list">
    <header-component title="虚拟卡商城" :show-back="false" />
    
    <div class="search-box">
      <van-search
        v-model="searchKeyword"
        placeholder="搜索虚拟卡"
        shape="round"
        @search="onSearch"
      />
    </div>
    
    <van-tabs v-model="activeCategory" @click="onCategoryChange">
      <van-tab v-for="category in categories" :key="category.id" :title="category.name" />
    </van-tabs>
    
    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
      <van-list
        v-model="loading"
        :finished="finished"
        finished-text="没有更多了"
        @load="onLoad"
      >
        <card-item
          v-for="card in cardList"
          :key="card.id"
          :card="card"
          @click="goToDetail(card.id)"
        />
      </van-list>
    </van-pull-refresh>
  </div>
</template>

<script>
import { Search, Tab, Tabs, List, PullRefresh } from 'vant';
import HeaderComponent from '@/components/Header.vue';
import CardItem from '@/components/CardItem.vue';
import { getCardProducts, getCardCategories } from '@/api/card';

export default {
  components: {
    [Search.name]: Search,
    [Tab.name]: Tab,
    [Tabs.name]: Tabs,
    [List.name]: List,
    [PullRefresh.name]: PullRefresh,
    HeaderComponent,
    CardItem
  },
  data() {
    return {
      searchKeyword: '',
      activeCategory: 0,
      categories: [],
      cardList: [],
      loading: false,
      finished: false,
      refreshing: false,
      page: 1,
      pageSize: 10
    };
  },
  created() {
    this.loadCategories();
  },
  methods: {
    async loadCategories() {
      try {
        const res = await getCardCategories();
        this.categories = [{ id: 0, name: '全部' }, ...res.data];
      } catch (error) {
        console.error('加载分类失败', error);
      }
    },
    async onLoad() {
      if (this.refreshing) {
        this.cardList = [];
        this.refreshing = false;
      }
      
      try {
        const params = {
          page: this.page,
          pageSize: this.pageSize,
          categoryId: this.activeCategory === 0 ? null : this.activeCategory,
          keyword: this.searchKeyword
        };
        
        const res = await getCardProducts(params);
        this.cardList = [...this.cardList, ...res.data.list];
        this.loading = false;
        
        if (res.data.list.length < this.pageSize) {
          this.finished = true;
        } else {
          this.page++;
        }
      } catch (error) {
        this.loading = false;
        this.finished = true;
        console.error('加载卡片列表失败', error);
      }
    },
    onRefresh() {
      this.page = 1;
      this.finished = false;
      this.loading = true;
      this.onLoad();
    },
    onSearch() {
      this.page = 1;
      this.cardList = [];
      this.finished = false;
      this.loading = true;
      this.onLoad();
    },
    onCategoryChange() {
      this.page = 1;
      this.cardList = [];
      this.finished = false;
      this.loading = true;
      this.onLoad();
    },
    goToDetail(id) {
      this.$router.push(`/card/detail/${id}`);
    }
  }
};
</script>

<style scoped>
.card-list {
  padding-bottom: 50px;
}

.search-box {
  padding: 10px;
}
</style>

订单创建页 (Order/Create.vue)

<template>
  <div class="order-create">
    <header-component title="确认订单" :show-back="true" />
    
    <div class="address-section" v-if="!isVirtual">
      <van-contact-card
        type="edit"
        :name="address.name"
        :tel="address.phone"
        @click="editAddress"
      />
    </div>
    
    <div class="card-info">
      <van-card
        :num="quantity"
        :price="card.price"
        :title="card.name"
        :thumb="card.imageUrl"
      >
        <template #tags>
          <van-tag plain type="danger">虚拟商品</van-tag>
        </template>
      </van-card>
    </div>
    
    <div class="order-section">
      <van-cell-group>
        <van-cell title="购买数量">
          <van-stepper v-model="quantity" integer min="1" :max="card.stock" />
        </van-cell>
        <van-cell title="商品金额" :value="`¥${(card.price * quantity).toFixed(2)}`" />
        <van-cell title="优惠金额" value="¥0.00" />
        <van-cell title="实付金额" :value="`¥${(card.price * quantity).toFixed(2)}`" class="total-price" />
      </van-cell-group>
    </div>
    
    <div class="payment-section">
      <van-radio-group v-model="paymentType">
        <van-cell-group title="支付方式">
          <van-cell title="微信支付" clickable @click="paymentType = 1">
            <template #right-icon>
              <van-radio :name="1" />
            </template>
          </van-cell>
        </van-cell-group>
      </van-radio-group>
    </div>
    
    <div class="submit-section">
      <van-submit-bar
        :price="totalPrice * 100"
        button-text="提交订单"
        @submit="createOrder"
      />
    </div>
  </div>
</template>

<script>
import { ContactCard, Card, Tag, Cell, CellGroup, Radio, RadioGroup, Stepper, SubmitBar } from 'vant';
import HeaderComponent from '@/components/Header.vue';
import { getCardDetail } from '@/api/card';
import { createOrder } from '@/api/order';

export default {
  components: {
    [ContactCard.name]: ContactCard,
    [Card.name]: Card,
    [Tag.name]: Tag,
    [Cell.name]: Cell,
    [CellGroup.name]: CellGroup,
    [Radio.name]: Radio,
    [RadioGroup.name]: RadioGroup,
    [Stepper.name]: Stepper,
    [SubmitBar.name]: SubmitBar,
    HeaderComponent
  },
  data() {
    return {
      cardId: null,
      card: {
        id: null,
        name: '',
        price: 0,
        stock: 0,
        imageUrl: ''
      },
      quantity: 1,
      paymentType: 1,
      address: {
        name: '张三',
        phone: '13800138000',
        address: '北京市朝阳区'
      },
      isVirtual: true
    };
  },
  computed: {
    totalPrice() {
      return this.card.price * this.quantity;
    }
  },
  created() {
    this.cardId = this.$route.params.id;
    this.loadCardDetail();
  },
  methods: {
    async loadCardDetail() {
      try {
        const res = await getCardDetail(this.cardId);
        this.card = res.data;
      } catch (error) {
        this.$toast.fail('加载卡片详情失败');
        console.error(error);
      }
    },
    editAddress() {
      this.$router.push('/address/edit');
    },
    async createOrder() {
      try {
        this.$toast.loading({
          message: '创建订单中...',
          forbidClick: true
        });
        
        const params = {
          productId: this.cardId,
          quantity: this.quantity
        };
        
        const res = await createOrder(params);
        this.$toast.clear();
        
        // 跳转到支付页面
        this.$router.push({
          path: '/payment/pay',
          query: {
            orderNo: res.data.orderNo,
            amount: this.totalPrice
          }
        });
      } catch (error) {
        this.$toast.clear();
        this.$toast.fail(error.message || '创建订单失败');
        console.error(error);
      }
    }
  }
};
</script>

<style scoped>
.order-create {
  padding-bottom: 100px;
}

.address-section {
  margin-bottom: 10px;
}

.card-info {
  margin-bottom: 10px;
}

.total-price {
  font-weight: bold;
  color: #ee0a24;
}
</style>

微信支付页 (Payment/Pay.vue)

<template>
  <div class="payment-page">
    <header-component title="支付订单" :show-back="true" />
    
    <div class="payment-info">
      <van-cell-group>
        <van-cell title="订单编号" :value="orderNo" />
        <van-cell title="支付金额">
          <span class="price">¥{{ amount.toFixed(2) }}</span>
        </van-cell>
      </van-cell-group>
    </div>
    
    <div class="payment-methods">
      <van-radio-group v-model="paymentMethod">
        <van-cell-group title="选择支付方式">
          <van-cell title="微信支付" clickable @click="paymentMethod = 'wechat'">
            <template #right-icon>
              <van-radio name="wechat" />
            </template>
            <template #icon>
              <img src="@/assets/images/wechat-pay.png" class="pay-icon" />
            </template>
          </van-cell>
        </van-cell-group>
      </van-radio-group>
    </div>
    
    <div class="payment-btn">
      <van-button
        type="primary"
        block
        round
        :loading="loading"
        @click="handlePayment"
      >
        立即支付
      </van-button>
    </div>
    
    <van-dialog
      v-model="showPaymentDialog"
      title="微信支付"
      show-cancel-button
      :before-close="beforeClose"
    >
      <div class="payment-dialog">
        <div v-if="paymentStatus === 'pending'" class="payment-pending">
          <van-loading size="24px">正在调起支付...</van-loading>
        </div>
        <div v-else-if="paymentStatus === 'success'" class="payment-success">
          <van-icon name="checked" color="#07c160" size="50px" />
          <p>支付成功</p>
        </div>
        <div v-else class="payment-failed">
          <van-icon name="close" color="#ee0a24" size="50px" />
          <p>支付失败</p>
          <p class="error-msg">{{ errorMsg }}</p>
        </div>
      </div>
    </van-dialog>
  </div>
</template>

<script>
import { Cell, CellGroup, Radio, RadioGroup, Button, Dialog, Loading, Icon } from 'vant';
import HeaderComponent from '@/components/Header.vue';
import { createPayment } from '@/api/payment';
import { getOrderDetail } from '@/api/order';
import { isWeixinBrowser, wechatPay } from '@/utils/wechat';

export default {
  components: {
    [Cell.name]: Cell,
    [CellGroup.name]: CellGroup,
    [Radio.name]: Radio,
    [RadioGroup.name]: RadioGroup,
    [Button.name]: Button,
    [Dialog.name]: Dialog,
    [Loading.name]: Loading,
    [Icon.name]: Icon,
    HeaderComponent
  },
  data() {
    return {
      orderNo: this.$route.query.orderNo,
      amount: parseFloat(this.$route.query.amount),
      paymentMethod: 'wechat',
      loading: false,
      showPaymentDialog: false,
      paymentStatus: 'pending', // pending, success, failed
      errorMsg: '',
      timer: null,
      isWeixin: isWeixinBrowser()
    };
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  },
  methods: {
    async handlePayment() {
      if (this.paymentMethod !== 'wechat') {
        this.$toast('请选择微信支付');
        return;
      }
      
      this.loading = true;
      
      try {
        // 创建支付
        const res = await createPayment({
          orderNo: this.orderNo,
          paymentType: 1 // 微信支付
        });
        
        this.loading = false;
        
        if (this.isWeixin) {
          // 微信浏览器内使用JSAPI支付
          await this.wechatJsApiPay(res.data);
        } else {
          // 非微信浏览器使用H5支付
          this.showPaymentDialog = true;
          window.location.href = res.data.h5Url;
          
          // 启动轮询检查支付状态
          this.startPaymentCheck();
        }
      } catch (error) {
        this.loading = false;
        this.$toast.fail(error.message || '支付创建失败');
        console.error(error);
      }
    },
    async wechatJsApiPay(paymentData) {
      try {
        await wechatPay(paymentData);
        
        // 支付成功,跳转到结果页
        this.$router.push({
          path: '/payment/result',
          query: {
            orderNo: this.orderNo,
            status: 'success'
          }
        });
      } catch (error) {
        this.$toast.fail(error.message || '支付失败');
        console.error(error);
      }
    },
    startPaymentCheck() {
      this.timer = setInterval(async () => {
        try {
          const res = await getOrderDetail(this.orderNo);
          
          if (res.data.status === 1) { // 已支付
            this.paymentStatus = 'success';
            clearInterval(this.timer);
            
            // 3秒后自动跳转
            setTimeout(() => {
              this.showPaymentDialog = false;
              this.$router.push({
                path: '/payment/result',
                query: {
                  orderNo: this.orderNo,
                  status: 'success'
                }
              });
            }, 3000);
          }
        } catch (error) {
          console.error('检查支付状态失败', error);
        }
      }, 3000);
    },
    beforeClose(action, done) {
      if (action === 'confirm') {
        if (this.paymentStatus === 'pending') {
          this.$toast('支付处理中,请稍候');
          done(false);
        } else {
          done();
          this.$router.push({
            path: '/payment/result',
            query: {
              orderNo: this.orderNo,
              status: this.paymentStatus
            }
          });
        }
      } else {
        done();
      }
    }
  }
};
</script>

<style scoped>
.payment-page {
  padding-bottom: 100px;
}

.payment-info {
  margin-bottom: 10px;
}

.price {
  color: #ee0a24;
  font-weight: bold;
}

.pay-icon {
  width: 24px;
  height: 24px;
  margin-right: 10px;
}

.payment-btn {
  margin: 20px 15px;
}

.payment-dialog {
  padding: 20px;
  text-align: center;
}

.payment-success,
.payment-failed {
  padding: 20px 0;
}

.payment-success p,
.payment-failed p {
  margin-top: 10px;
  font-size: 16px;
}

.error-msg {
  color: #ee0a24;
  font-size: 14px;
}
</style>

5.2 管理端前端实现

5.2.1 项目结构

管理端前端结构与用户端类似,但使用Element UI作为UI框架,主要包含以下功能模块:

  • 管理员登录
  • 虚拟卡产品管理
  • 卡密库存管理
  • 订单管理
  • 用户管理
  • 数据统计
5.2.2 核心页面实现

虚拟卡产品管理页 (Card/List.vue)

<template>
  <div class="card-management">
    <el-card class="search-card">
      <el-form :inline="true" :model="searchForm" class="search-form">
        <el-form-item label="产品名称">
          <el-input v-model="searchForm.name" placeholder="请输入产品名称" clearable />
        </el-form-item>
        <el-form-item label="产品分类">
          <el-select v-model="searchForm.categoryId" placeholder="请选择分类" clearable>
            <el-option
              v-for="category in categories"
              :key="category.id"
              :label="category.name"
              :value="category.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
            <el-option label="上架" :value="1" />
            <el-option label="下架" :value="0" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">查询</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    
    <el-card class="operation-card">
      <el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增产品</el-button>
      <el-button type="danger" icon="el-icon-delete" :disabled="!selectedItems.length" @click="handleBatchDelete">
        批量删除
      </el-button>
    </el-card>
    
    <el-card>
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        @selection-change="handleSelectionChange"
        v-loading="loading"
      >
        <el-table-column type="selection" width="55" />
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="name" label="产品名称" min-width="150" />
        <el-table-column label="分类" width="120">
          <template slot-scope="scope">
            {{ getCategoryName(scope.row.categoryId) }}
          </template>
        </el-table-column>
        <el-table-column prop="price" label="价格" width="120">
          <template slot-scope="scope">
            ¥{{ scope.row.price.toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column prop="stock" label="库存" width="100" />
        <el-table-column label="状态" width="100">
          <template slot-scope="scope">
            <el-tag :type="scope.row.status ? 'success' : 'danger'">
              {{ scope.row.status ? '上架' : '下架' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" width="180" />
        <el-table-column label="操作" width="180" fixed="right">
          <template slot-scope="scope">
            <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
            <el-button
              size="mini"
              :type="scope.row.status ? 'danger' : 'success'"
              @click="handleStatusChange(scope.row)"
            >
              {{ scope.row.status ? '下架' : '上架' }}
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="pagination.current"
        :page-sizes="[10, 20, 50, 100]"
        :page-size="pagination.size"
        layout="total, sizes, prev, pager, next, jumper"
        :total="pagination.total"
        class="pagination"
      />
    </el-card>
    
    <!-- 新增/编辑对话框 -->
    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="50%">
      <el-form :model="dialogForm" :rules="rules" ref="dialogForm" label-width="100px">
        <el-form-item label="产品名称" prop="name">
          <el-input v-model="dialogForm.name" placeholder="请输入产品名称" />
        </el-form-item>
        <el-form-item label="产品分类" prop="categoryId">
          <el-select v-model="dialogForm.categoryId" placeholder="请选择分类">
            <el-option
              v-for="category in categories"
              :key="category.id"
              :label="category.name"
              :value="category.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="产品价格" prop="price">
          <el-input-number v-model="dialogForm.price" :min="0" :precision="2" :step="0.1" />
        </el-form-item>
        <el-form-item label="原价" prop="originalPrice">
          <el-input-number v-model="dialogForm.originalPrice" :min="0" :precision="2" :step="0.1" />
        </el-form-item>
        <el-form-item label="产品图片" prop="imageUrl">
          <el-upload
            class="avatar-uploader"
            action="/api/upload"
            :show-file-list="false"
            :on-success="handleImageSuccess"
            :before-upload="beforeImageUpload"
          >
            <img v-if="dialogForm.imageUrl" :src="dialogForm.imageUrl" class="avatar" />
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
          </el-upload>
        </el-form-item>
        <el-form-item label="详情图片" prop="detailImages">
          <el-upload
            action="/api/upload"
            list-type="picture-card"
            :file-list="detailImageList"
            :on-success="handleDetailImageSuccess"
            :on-remove="handleDetailImageRemove"
            :before-upload="beforeImageUpload"
            multiple
          >
            <i class="el-icon-plus"></i>
          </el-upload>
        </el-form-item>
        <el-form-item label="产品描述" prop="description">
          <el-input
            type="textarea"
            :rows="4"
            v-model="dialogForm.description"
            placeholder="请输入产品描述"
          />
        </el-form-item>
        <el-form-item label="排序权重" prop="sortOrder">
          <el-input-number v-model="dialogForm.sortOrder" :min="0" />
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-switch v-model="dialogForm.status" :active-value="1" :inactive-value="0" />
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitForm">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import { getCardProducts, addCardProduct, updateCardProduct, deleteCardProduct, updateCardProductStatus } from '@/api/card';
import { getCardCategories } from '@/api/category';

export default {
  data() {
    return {
      searchForm: {
        name: '',
        categoryId: null,
        status: null
      },
      tableData: [],
      selectedItems: [],
      categories: [],
      loading: false,
      pagination: {
        current: 1,
        size: 10,
        total: 0
      },
      dialogVisible: false,
      dialogTitle: '新增产品',
      dialogForm: {
        id: null,
        name: '',
        categoryId: null,
        price: 0,
        originalPrice: 0,
        imageUrl: '',
        detailImages: [],
        description: '',
        sortOrder: 0,
        status: 1
      },
      detailImageList: [],
      rules: {
        name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
        categoryId: [{ required: true, message: '请选择产品分类', trigger: 'change' }],
        price: [{ required: true, message: '请输入产品价格', trigger: 'blur' }]
      }
    };
  },
  created() {
    this.loadCategories();
    this.loadTableData();
  },
  methods: {
    async loadCategories() {
      try {
        const res = await getCardCategories();
        this.categories = res.data;
      } catch (error) {
        console.error('加载分类失败', error);
      }
    },
    async loadTableData() {
      this.loading = true;
      
      try {
        const params = {
          ...this.searchForm,
          page: this.pagination.current,
          pageSize: this.pagination.size
        };
        
        const res = await getCardProducts(params);
        this.tableData = res.data.list;
        this.pagination.total = res.data.total;
      } catch (error) {
        console.error('加载产品列表失败', error);
      } finally {
        this.loading = false;
      }
    },
    getCategoryName(categoryId) {
      const category = this.categories.find(item => item.id === categoryId);
      return category ? category.name : '--';
    },
    handleSearch() {
      this.pagination.current = 1;
      this.loadTableData();
    },
    resetSearch() {
      this.searchForm = {
        name: '',
        categoryId: null,
        status: null
      };
      this.pagination.current = 1;
      this.loadTableData();
    },
    handleSelectionChange(val) {
      this.selectedItems = val;
    },
    handleSizeChange(val) {
      this.pagination.size = val;
      this.loadTableData();
    },
    handleCurrentChange(val) {
      this.pagination.current = val;
      this.loadTableData();
    },
    handleAdd() {
      this.dialogTitle = '新增产品';
      this.dialogForm = {
        id: null,
        name: '',
        categoryId: null,
        price: 0,
        originalPrice: 0,
        imageUrl: '',
        detailImages: [],
        description: '',
        sortOrder: 0,
        status: 1
      };
      this.detailImageList = [];
      this.dialogVisible = true;
    },
    handleEdit(row) {
      this.dialogTitle = '编辑产品';
      this.dialogForm = {
        ...row,
        detailImages: row.detailImages ? JSON.parse(row.detailImages) : []
      };
      this.detailImageList = this.dialogForm.detailImages.map(url => ({
        url,
        name: url.substring(url.lastIndexOf('/') + 1)
      }));
      this.dialogVisible = true;
    },
    async handleStatusChange(row) {
      try {
        await updateCardProductStatus(row.id, row.status ? 0 : 1);
        this.$message.success('状态更新成功');
        this.loadTableData();
      } catch (error) {
        this.$message.error('状态更新失败');
        console.error(error);
      }
    },
    handleBatchDelete() {
      this.$confirm('确定要删除选中的产品吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(async () => {
        try {
          const ids = this.selectedItems.map(item => item.id);
          await deleteCardProduct(ids);
          this.$message.success('删除成功');
          this.loadTableData();
        } catch (error) {
          this.$message.error('删除失败');
          console.error(error);
        }
      }).catch(() => {});
    },
    handleImageSuccess(res, file) {
      this.dialogForm.imageUrl = res.data.url;
    },
    handleDetailImageSuccess(res, file) {
      this.dialogForm.detailImages.push(res.data.url);
    },
    handleDetailImageRemove(file, fileList) {
      const url = file.url || file.response.data.url;
      this.dialogForm.detailImages = this.dialogForm.detailImages.filter(item => item !== url);
    },
    beforeImageUpload(file) {
      const isImage = file.type.startsWith('image/');
      const isLt2M = file.size / 1024 / 1024 < 2;
      
      if (!isImage) {
        this.$message.error('只能上传图片!');
      }
      if (!isLt2M) {
        this.$message.error('图片大小不能超过2MB!');
      }
      
      return isImage && isLt2M;
    },
    submitForm() {
      this.$refs.dialogForm.validate(async valid => {
        if (!valid) {
          return;
        }
        
        try {
          const formData = {
            ...this.dialogForm,
            detailImages: JSON.stringify(this.dialogForm.detailImages)
          };
          
          if (this.dialogForm.id) {
            await updateCardProduct(formData);
            this.$message.success('更新成功');
          } else {
            await addCardProduct(formData);
            this.$message.success('添加成功');
          }
          
          this.dialogVisible = false;
          this.loadTableData();
        } catch (error) {
          this.$message.error(error.message || '操作失败');
          console.error(error);
        }
      });
    }
  }
};
</script>

<style scoped>
.search-card {
  margin-bottom: 20px;
}

.search-form {
  display: flex;
  flex-wrap: wrap;
}

.operation-card {
  margin-bottom: 20px;
}

.pagination {
  margin-top: 20px;
  text-align: right;
}

.avatar-uploader {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  width: 150px;
  height: 150px;
}

.avatar-uploader:hover {
  border-color: #409EFF;
}

.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 150px;
  height: 150px;
  line-height: 150px;
  text-align: center;
}

.avatar {
  width: 150px;
  height: 150px;
  display: block;
}
</style>

6. 微信H5支付集成

6.1 微信支付配置

  1. 申请微信支付商户号

    • 登录微信支付商户平台(https://pay.weixin.qq.com)
    • 完成商户号申请和资质认证
  2. 配置支付域名

    • 在商户平台配置支付域名(需备案)
    • 配置授权目录和回调域名
  3. 获取API密钥和证书

    • 设置APIv2密钥(32位)
    • 申请API证书(用于V3接口)
  4. 配置应用信息

    • 在商户平台配置H5支付信息
    • 设置支付场景和域名

6.2 支付流程实现

  1. 前端发起支付请求
// src/utils/wechat.js
import axios from 'axios';

export function isWeixinBrowser() {
  return /micromessenger/i.test(navigator.userAgent);
}

export async function wechatPay(paymentData) {
  return new Promise((resolve, reject) => {
    if (typeof WeixinJSBridge === 'undefined') {
      if (document.addEventListener) {
        document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
      } else if (document.attachEvent) {
        document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
        document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
      }
      reject(new Error('请在微信中打开页面'));
    } else {
      onBridgeReady();
    }
    
    function onBridgeReady() {
      WeixinJSBridge.invoke(
        'getBrandWCPayRequest',
        {
          appId: paymentData.appId,
          timeStamp: paymentData.timeStamp,
          nonceStr: paymentData.nonceStr,
          package: paymentData.package,
          signType: paymentData.signType,
          paySign: paymentData.paySign
        },
        function(res) {
          if (res.err_msg === 'get_brand_wcpay_request:ok') {
            resolve();
          } else {
            reject(new Error(res.err_msg || '支付失败'));
          }
        }
      );
    }
  });
}
  1. 后端处理支付回调
// PaymentController.java
@RestController
@RequestMapping("/api/payment")
public class PaymentController {
    
    // ... 其他代码 ...
    
    @PostMapping("/callback/wechat")
    public String wechatPayCallback(HttpServletRequest request) {
        try {
            // 获取请求头信息
            String signature = request.getHeader("Wechatpay-Signature");
            String serial = request.getHeader("Wechatpay-Serial");
            String nonce = request.getHeader("Wechatpay-Nonce");
            String timestamp = request.getHeader("Wechatpay-Timestamp");
            
            // 获取请求体
            String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
            
            // 验证回调
            if (!weChatPayUtil.verifyNotify(null, signature, serial, nonce, timestamp, body)) {
                log.error("微信支付回调验证失败");
                return "FAIL";
            }
            
            // 解析回调内容
            JSONObject json = JSON.parseObject(body);
            JSONObject resource = json.getJSONObject("resource");
            String orderNo = resource.getString("out_trade_no");
            String transactionId = resource.getString("transaction_id");
            BigDecimal amount = resource.getJSONObject("amount")
                    .getBigDecimal("total")
                    .divide(new BigDecimal(100));
            Date paymentTime = new Date(resource.getLong("success_time") * 1000);
            
            // 处理支付成功逻辑
            orderService.payOrderSuccess(orderNo, transactionId, amount, paymentTime);
            
            log.info("微信支付回调处理成功, orderNo: {}", orderNo);
            return "SUCCESS";
        } catch (Exception e) {
            log.error("微信支付回调处理失败", e);
            return "FAIL";
        }
    }
}

其他略…

评论 71
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百锦再@新空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值