欢迎光临
我们一直在努力

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

文章目录

    • 1. 项目概述
      • 1.1 项目背景
      • 1.2 系统架构
    • 2. 技术选型与环境搭建
      • 2.1 后端技术栈
      • 2.2 前端技术栈
      • 2.3 开发环境配置
    • 3. 数据库设计
      • 3.1 数据库ER图
      • 3.2 数据表设计
    • 4. 后端实现
      • 4.1 Spring Boot项目结构
      • 4.2 核心功能实现
        • 4.2.1 用户认证与授权
        • 4.2.2 虚拟卡管理
        • 4.2.3 订单服务
        • 4.2.4 微信支付集成
    • 5. 前端实现
      • 5.1 用户端前端实现
        • 5.1.1 项目结构
        • 5.1.2 核心页面实现
      • 5.2 管理端前端实现
        • 5.2.1 项目结构
        • 5.2.2 核心页面实现
    • 6. 微信H5支付集成
      • 6.1 微信支付配置
      • 6.2 支付流程实现

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>springbootstarterweb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>springbootstartersecurity</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>springbootstarterdataredis</artifactId>
</dependency>

<! 数据库相关 >
<dependency>
<groupId>mysql</groupId>
<artifactId>mysqlconnectorjava</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatisplusbootstarter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druidspringbootstarter</artifactId>
<version>1.2.8</version>
</dependency>

<! 工具类 >
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commonslang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1jre</version>
</dependency>

<! 微信支付SDK >
<dependency>
<groupId>com.github.wechatpayapiv3</groupId>
<artifactId>wechatpayapachehttpclient</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 开发环境配置

  • JDK 1.8+
  • Maven 3.6+
  • Node.js 14+
  • MySQL 5.7+
  • Redis 5.0+
  • 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 微信支付配置

  • 申请微信支付商户号

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

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

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

    • 在商户平台配置H5支付信息
    • 设置支付场景和域名
  • 6.2 支付流程实现

  • 前端发起支付请求
  • // 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 || '支付失败'));
    }
    }
    );
    }
    });
    }

  • 后端处理支付回调
  • // 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";
    }
    }
    }

    其他略…

    赞(0)
    未经允许不得转载:171主机测评 » Web虚拟卡销售店铺实现方案
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址