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

打印 上一主题 下一主题

主题 1503|帖子 1503|积分 4509

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
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 后端技术栈

  1. // pom.xml 主要依赖
  2. <dependencies>
  3.     <!-- Spring Boot Starter -->
  4.     <dependency>
  5.         <groupId>org.springframework.boot</groupId>
  6.         <artifactId>spring-boot-starter-web</artifactId>
  7.     </dependency>
  8.     <dependency>
  9.         <groupId>org.springframework.boot</groupId>
  10.         <artifactId>spring-boot-starter-security</artifactId>
  11.     </dependency>
  12.     <dependency>
  13.         <groupId>org.springframework.boot</groupId>
  14.         <artifactId>spring-boot-starter-data-redis</artifactId>
  15.     </dependency>
  16.    
  17.     <!-- 数据库相关 -->
  18.     <dependency>
  19.         <groupId>mysql</groupId>
  20.         <artifactId>mysql-connector-java</artifactId>
  21.         <scope>runtime</scope>
  22.     </dependency>
  23.     <dependency>
  24.         <groupId>com.baomidou</groupId>
  25.         <artifactId>mybatis-plus-boot-starter</artifactId>
  26.         <version>3.5.1</version>
  27.     </dependency>
  28.     <dependency>
  29.         <groupId>com.alibaba</groupId>
  30.         <artifactId>druid-spring-boot-starter</artifactId>
  31.         <version>1.2.8</version>
  32.     </dependency>
  33.    
  34.     <!-- 工具类 -->
  35.     <dependency>
  36.         <groupId>org.apache.commons</groupId>
  37.         <artifactId>commons-lang3</artifactId>
  38.     </dependency>
  39.     <dependency>
  40.         <groupId>com.google.guava</groupId>
  41.         <artifactId>guava</artifactId>
  42.         <version>31.0.1-jre</version>
  43.     </dependency>
  44.    
  45.     <!-- 微信支付SDK -->
  46.     <dependency>
  47.         <groupId>com.github.wechatpay-apiv3</groupId>
  48.         <artifactId>wechatpay-apache-httpclient</artifactId>
  49.         <version>0.4.7</version>
  50.     </dependency>
  51.    
  52.     <!-- 其他 -->
  53.     <dependency>
  54.         <groupId>org.projectlombok</groupId>
  55.         <artifactId>lombok</artifactId>
  56.         <optional>true</optional>
  57.     </dependency>
  58. </dependencies>
复制代码
2.2 前端技术栈

  1. # 管理端前端
  2. vue create admin-frontend
  3. cd admin-frontend
  4. vue add element-ui
  5. npm install axios vue-router vuex --save
  6. # 用户端前端
  7. vue create user-frontend
  8. cd user-frontend
  9. 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 数据表设计

  1. -- 用户表
  2. CREATE TABLE `user` (
  3.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  4.   `username` varchar(50) NOT NULL COMMENT '用户名',
  5.   `password` varchar(100) NOT NULL COMMENT '密码',
  6.   `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  7.   `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  8.   `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
  9.   `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-正常',
  10.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  11.   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  12.   PRIMARY KEY (`id`),
  13.   UNIQUE KEY `idx_username` (`username`),
  14.   KEY `idx_phone` (`phone`)
  15. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
  16. -- 虚拟卡产品表
  17. CREATE TABLE `card_product` (
  18.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  19.   `name` varchar(100) NOT NULL COMMENT '产品名称',
  20.   `category_id` bigint(20) NOT NULL COMMENT '分类ID',
  21.   `description` text COMMENT '产品描述',
  22.   `price` decimal(10,2) NOT NULL COMMENT '售价',
  23.   `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价',
  24.   `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
  25.   `image_url` varchar(255) DEFAULT NULL COMMENT '图片URL',
  26.   `detail_images` text COMMENT '详情图片,JSON数组',
  27.   `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-下架,1-上架',
  28.   `sort_order` int(11) DEFAULT '0' COMMENT '排序权重',
  29.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  30.   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  31.   PRIMARY KEY (`id`),
  32.   KEY `idx_category` (`category_id`),
  33.   KEY `idx_status` (`status`)
  34. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='虚拟卡产品表';
  35. -- 卡密库存表
  36. CREATE TABLE `card_secret` (
  37.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  38.   `product_id` bigint(20) NOT NULL COMMENT '产品ID',
  39.   `card_no` varchar(100) NOT NULL COMMENT '卡号',
  40.   `card_password` varchar(100) NOT NULL COMMENT '卡密',
  41.   `status` tinyint(1) DEFAULT '0' COMMENT '状态:0-未售出,1-已售出,2-已锁定',
  42.   `order_id` bigint(20) DEFAULT NULL COMMENT '订单ID',
  43.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  44.   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  45.   PRIMARY KEY (`id`),
  46.   UNIQUE KEY `idx_card_no` (`card_no`),
  47.   KEY `idx_product_id` (`product_id`),
  48.   KEY `idx_status` (`status`),
  49.   KEY `idx_order_id` (`order_id`)
  50. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡密库存表';
  51. -- 订单表
  52. CREATE TABLE `order` (
  53.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  54.   `order_no` varchar(50) NOT NULL COMMENT '订单编号',
  55.   `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  56.   `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
  57.   `payment_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
  58.   `payment_type` tinyint(1) DEFAULT NULL COMMENT '支付方式:1-微信,2-支付宝',
  59.   `status` tinyint(1) DEFAULT '0' COMMENT '订单状态:0-待支付,1-已支付,2-已发货,3-已完成,4-已取消',
  60.   `payment_time` datetime DEFAULT NULL COMMENT '支付时间',
  61.   `complete_time` datetime DEFAULT NULL COMMENT '完成时间',
  62.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  63.   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  64.   PRIMARY KEY (`id`),
  65.   UNIQUE KEY `idx_order_no` (`order_no`),
  66.   KEY `idx_user_id` (`user_id`),
  67.   KEY `idx_status` (`status`),
  68.   KEY `idx_create_time` (`create_time`)
  69. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
  70. -- 订单明细表
  71. CREATE TABLE `order_item` (
  72.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  73.   `order_id` bigint(20) NOT NULL COMMENT '订单ID',
  74.   `order_no` varchar(50) NOT NULL COMMENT '订单编号',
  75.   `product_id` bigint(20) NOT NULL COMMENT '产品ID',
  76.   `product_name` varchar(100) NOT NULL COMMENT '产品名称',
  77.   `product_image` varchar(255) DEFAULT NULL COMMENT '产品图片',
  78.   `quantity` int(11) NOT NULL COMMENT '购买数量',
  79.   `price` decimal(10,2) NOT NULL COMMENT '单价',
  80.   `total_price` decimal(10,2) NOT NULL COMMENT '总价',
  81.   `card_secret_id` bigint(20) DEFAULT NULL COMMENT '卡密ID',
  82.   `card_no` varchar(100) DEFAULT NULL COMMENT '卡号',
  83.   `card_password` varchar(100) DEFAULT NULL COMMENT '卡密',
  84.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  85.   PRIMARY KEY (`id`),
  86.   KEY `idx_order_id` (`order_id`),
  87.   KEY `idx_order_no` (`order_no`),
  88.   KEY `idx_product_id` (`product_id`)
  89. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
  90. -- 支付记录表
  91. CREATE TABLE `payment` (
  92.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  93.   `order_id` bigint(20) NOT NULL COMMENT '订单ID',
  94.   `order_no` varchar(50) NOT NULL COMMENT '订单编号',
  95.   `payment_no` varchar(50) NOT NULL COMMENT '支付流水号',
  96.   `payment_type` tinyint(1) NOT NULL COMMENT '支付方式:1-微信,2-支付宝',
  97.   `payment_amount` decimal(10,2) NOT NULL COMMENT '支付金额',
  98.   `payment_status` tinyint(1) DEFAULT '0' COMMENT '支付状态:0-未支付,1-支付成功,2-支付失败',
  99.   `payment_time` datetime DEFAULT NULL COMMENT '支付时间',
  100.   `callback_time` datetime DEFAULT NULL COMMENT '回调时间',
  101.   `callback_content` text COMMENT '回调内容',
  102.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  103.   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  104.   PRIMARY KEY (`id`),
  105.   UNIQUE KEY `idx_payment_no` (`payment_no`),
  106.   KEY `idx_order_id` (`order_id`),
  107.   KEY `idx_order_no` (`order_no`)
  108. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';
  109. -- 管理员表
  110. CREATE TABLE `admin` (
  111.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  112.   `username` varchar(50) NOT NULL COMMENT '用户名',
  113.   `password` varchar(100) NOT NULL COMMENT '密码',
  114.   `nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
  115.   `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
  116.   `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  117.   `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  118.   `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-正常',
  119.   `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  120.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  121.   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  122.   PRIMARY KEY (`id`),
  123.   UNIQUE KEY `idx_username` (`username`)
  124. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';
  125. -- 系统日志表
  126. CREATE TABLE `sys_log` (
  127.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  128.   `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  129.   `username` varchar(50) DEFAULT NULL COMMENT '用户名',
  130.   `operation` varchar(50) DEFAULT NULL COMMENT '用户操作',
  131.   `method` varchar(200) DEFAULT NULL COMMENT '请求方法',
  132.   `params` text COMMENT '请求参数',
  133.   `time` bigint(20) DEFAULT NULL COMMENT '执行时长(毫秒)',
  134.   `ip` varchar(64) DEFAULT NULL COMMENT 'IP地址',
  135.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  136.   PRIMARY KEY (`id`)
  137. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表';
复制代码
4. 后端实现

4.1 Spring Boot项目布局

  1. src/main/java/com/virtualcard/
  2. ├── config/                # 配置类
  3. │   ├── SecurityConfig.java
  4. │   ├── SwaggerConfig.java
  5. │   ├── RedisConfig.java
  6. │   └── WebMvcConfig.java
  7. ├── constant/              # 常量类
  8. │   ├── OrderStatus.java
  9. │   ├── PaymentType.java
  10. │   └── RedisKey.java
  11. ├── controller/            # 控制器
  12. │   ├── api/               # 用户API接口
  13. │   │   ├── AuthController.java
  14. │   │   ├── CardController.java
  15. │   │   ├── OrderController.java
  16. │   │   └── PaymentController.java
  17. │   └── admin/             # 管理端接口
  18. │       ├── AdminAuthController.java
  19. │       ├── AdminCardController.java
  20. │       ├── AdminOrderController.java
  21. │       └── AdminUserController.java
  22. ├── dao/                   # 数据访问层
  23. │   ├── entity/            # 实体类
  24. │   │   ├── User.java
  25. │   │   ├── CardProduct.java
  26. │   │   ├── CardSecret.java
  27. │   │   ├── Order.java
  28. │   │   └── Payment.java
  29. │   └── mapper/            # MyBatis Mapper接口
  30. │       ├── UserMapper.java
  31. │       ├── CardProductMapper.java
  32. │       ├── CardSecretMapper.java
  33. │       ├── OrderMapper.java
  34. │       └── PaymentMapper.java
  35. ├── dto/                   # 数据传输对象
  36. │   ├── request/           # 请求DTO
  37. │   │   ├── LoginReq.java
  38. │   │   ├── OrderCreateReq.java
  39. │   │   └── PaymentReq.java
  40. │   └── response/          # 响应DTO
  41. │       ├── ApiResponse.java
  42. │       ├── CardProductRes.java
  43. │       └── OrderRes.java
  44. ├── exception/             # 异常处理
  45. │   ├── BusinessException.java
  46. │   └── GlobalExceptionHandler.java
  47. ├── service/               # 服务层
  48. │   ├── impl/              # 服务实现
  49. │   │   ├── AuthServiceImpl.java
  50. │   │   ├── CardServiceImpl.java
  51. │   │   ├── OrderServiceImpl.java
  52. │   │   └── PaymentServiceImpl.java
  53. │   └── AuthService.java
  54. │   ├── CardService.java
  55. │   ├── OrderService.java
  56. │   └── PaymentService.java
  57. ├── util/                  # 工具类
  58. │   ├── JwtUtil.java
  59. │   ├── RedisUtil.java
  60. │   ├── SnowFlakeUtil.java
  61. │   └── WeChatPayUtil.java
  62. └── VirtualCardApplication.java  # 启动类
复制代码
4.2 核心功能实现

4.2.1 用户认证与授权

  1. // SecurityConfig.java
  2. @Configuration
  3. @EnableWebSecurity
  4. @EnableGlobalMethodSecurity(prePostEnabled = true)
  5. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  6.    
  7.     @Autowired
  8.     private UserDetailsService userDetailsService;
  9.    
  10.     @Autowired
  11.     private JwtAuthenticationFilter jwtAuthenticationFilter;
  12.    
  13.     @Autowired
  14.     private JwtAccessDeniedHandler jwtAccessDeniedHandler;
  15.    
  16.     @Autowired
  17.     private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
  18.    
  19.     @Bean
  20.     public PasswordEncoder passwordEncoder() {
  21.         return new BCryptPasswordEncoder();
  22.     }
  23.    
  24.     @Override
  25.     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  26.         auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  27.     }
  28.    
  29.     @Override
  30.     protected void configure(HttpSecurity http) throws Exception {
  31.         http.csrf().disable()
  32.             .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  33.             .and()
  34.             .authorizeRequests()
  35.             .antMatchers("/api/auth/**").permitAll()
  36.             .antMatchers("/api/payment/callback/**").permitAll()
  37.             .antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs").permitAll()
  38.             .antMatchers("/api/**").authenticated()
  39.             .antMatchers("/admin/**").hasRole("ADMIN")
  40.             .anyRequest().authenticated()
  41.             .and()
  42.             .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
  43.             .exceptionHandling()
  44.             .accessDeniedHandler(jwtAccessDeniedHandler)
  45.             .authenticationEntryPoint(jwtAuthenticationEntryPoint);
  46.     }
  47.    
  48.     @Bean
  49.     @Override
  50.     public AuthenticationManager authenticationManagerBean() throws Exception {
  51.         return super.authenticationManagerBean();
  52.     }
  53. }
  54. // JwtUtil.java
  55. @Component
  56. public class JwtUtil {
  57.     private static final String SECRET = "your_jwt_secret";
  58.     private static final long EXPIRATION = 86400L; // 24小时
  59.    
  60.     public String generateToken(UserDetails userDetails) {
  61.         Map<String, Object> claims = new HashMap<>();
  62.         claims.put("sub", userDetails.getUsername());
  63.         claims.put("created", new Date());
  64.         return Jwts.builder()
  65.                 .setClaims(claims)
  66.                 .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
  67.                 .signWith(SignatureAlgorithm.HS512, SECRET)
  68.                 .compact();
  69.     }
  70.    
  71.     public String getUsernameFromToken(String token) {
  72.         return Jwts.parser()
  73.                 .setSigningKey(SECRET)
  74.                 .parseClaimsJws(token)
  75.                 .getBody()
  76.                 .getSubject();
  77.     }
  78.    
  79.     public boolean validateToken(String token, UserDetails userDetails) {
  80.         final String username = getUsernameFromToken(token);
  81.         return (username.equals(userDetails.getUsername()) && !isTokenExpired(token);
  82.     }
  83.    
  84.     private boolean isTokenExpired(String token) {
  85.         final Date expiration = getExpirationDateFromToken(token);
  86.         return expiration.before(new Date());
  87.     }
  88.    
  89.     private Date getExpirationDateFromToken(String token) {
  90.         return Jwts.parser()
  91.                 .setSigningKey(SECRET)
  92.                 .parseClaimsJws(token)
  93.                 .getBody()
  94.                 .getExpiration();
  95.     }
  96. }
复制代码
4.2.2 虚拟卡管理

  1. // CardServiceImpl.java
  2. @Service
  3. public class CardServiceImpl implements CardService {
  4.    
  5.     @Autowired
  6.     private CardProductMapper cardProductMapper;
  7.    
  8.     @Autowired
  9.     private CardSecretMapper cardSecretMapper;
  10.    
  11.     @Autowired
  12.     private RedisUtil redisUtil;
  13.    
  14.     private static final String CARD_PRODUCT_CACHE_KEY = "card:product:list";
  15.     private static final long CACHE_EXPIRE = 3600; // 1小时
  16.    
  17.     @Override
  18.     public List<CardProductRes> listAllProducts() {
  19.         // 先查缓存
  20.         String cache = redisUtil.get(CARD_PRODUCT_CACHE_KEY);
  21.         if (StringUtils.isNotBlank(cache)) {
  22.             return JSON.parseArray(cache, CardProductRes.class);
  23.         }
  24.         
  25.         // 缓存没有则查数据库
  26.         QueryWrapper<CardProduct> queryWrapper = new QueryWrapper<>();
  27.         queryWrapper.eq("status", 1).orderByAsc("sort_order");
  28.         List<CardProduct> products = cardProductMapper.selectList(queryWrapper);
  29.         
  30.         List<CardProductRes> result = products.stream()
  31.                 .map(this::convertToRes)
  32.                 .collect(Collectors.toList());
  33.         
  34.         // 存入缓存
  35.         redisUtil.set(CARD_PRODUCT_CACHE_KEY, JSON.toJSONString(result), CACHE_EXPIRE);
  36.         return result;
  37.     }
  38.    
  39.     @Override
  40.     @Transactional
  41.     public List<CardSecret> lockCardSecrets(Long productId, int quantity, Long orderId) {
  42.         // 查询可用的卡密
  43.         List<CardSecret> availableSecrets = cardSecretMapper.selectAvailableSecrets(productId, quantity);
  44.         if (availableSecrets.size() < quantity) {
  45.             throw new BusinessException("库存不足");
  46.         }
  47.         
  48.         // 锁定卡密
  49.         List<Long> ids = availableSecrets.stream().map(CardSecret::getId).collect(Collectors.toList());
  50.         cardSecretMapper.lockSecrets(ids, orderId);
  51.         
  52.         // 更新产品库存
  53.         cardProductMapper.decreaseStock(productId, quantity);
  54.         
  55.         // 清除缓存
  56.         redisUtil.del(CARD_PRODUCT_CACHE_KEY);
  57.         
  58.         return availableSecrets;
  59.     }
  60.    
  61.     @Override
  62.     @Transactional
  63.     public void unlockCardSecrets(List<Long> cardSecretIds) {
  64.         if (CollectionUtils.isEmpty(cardSecretIds)) {
  65.             return;
  66.         }
  67.         
  68.         // 查询卡密对应的产品ID和数量
  69.         List<CardSecret> secrets = cardSecretMapper.selectBatchIds(cardSecretIds);
  70.         if (CollectionUtils.isEmpty(secrets)) {
  71.             return;
  72.         }
  73.         
  74.         Map<Long, Long> productCountMap = secrets.stream()
  75.                 .collect(Collectors.groupingBy(CardSecret::getProductId, Collectors.counting()));
  76.         
  77.         // 解锁卡密
  78.         cardSecretMapper.unlockSecrets(cardSecretIds);
  79.         
  80.         // 恢复产品库存
  81.         for (Map.Entry<Long, Long> entry : productCountMap.entrySet()) {
  82.             cardProductMapper.increaseStock(entry.getKey(), entry.getValue().intValue());
  83.         }
  84.         
  85.         // 清除缓存
  86.         redisUtil.del(CARD_PRODUCT_CACHE_KEY);
  87.     }
  88.    
  89.     private CardProductRes convertToRes(CardProduct product) {
  90.         CardProductRes res = new CardProductRes();
  91.         BeanUtils.copyProperties(product, res);
  92.         return res;
  93.     }
  94. }
复制代码
4.2.3 订单服务

  1. // OrderServiceImpl.java
  2. @Service
  3. public class OrderServiceImpl implements OrderService {
  4.    
  5.     @Autowired
  6.     private OrderMapper orderMapper;
  7.    
  8.     @Autowired
  9.     private OrderItemMapper orderItemMapper;
  10.    
  11.     @Autowired
  12.     private CardService cardService;
  13.    
  14.     @Autowired
  15.     private PaymentService paymentService;
  16.    
  17.     @Autowired
  18.     private SnowFlakeUtil snowFlakeUtil;
  19.    
  20.     @Override
  21.     @Transactional
  22.     public OrderRes createOrder(OrderCreateReq req, Long userId) {
  23.         // 生成订单号
  24.         String orderNo = generateOrderNo();
  25.         
  26.         // 锁定卡密
  27.         List<CardSecret> cardSecrets = cardService.lockCardSecrets(req.getProductId(), req.getQuantity(), null);
  28.         
  29.         // 计算总金额
  30.         CardProduct product = cardService.getProductById(req.getProductId());
  31.         BigDecimal totalAmount = product.getPrice().multiply(new BigDecimal(req.getQuantity()));
  32.         
  33.         // 创建订单
  34.         Order order = new Order();
  35.         order.setOrderNo(orderNo);
  36.         order.setUserId(userId);
  37.         order.setTotalAmount(totalAmount);
  38.         order.setPaymentAmount(totalAmount);
  39.         order.setStatus(OrderStatus.UNPAID.getCode());
  40.         orderMapper.insert(order);
  41.         
  42.         // 创建订单明细
  43.         List<OrderItem> orderItems = new ArrayList<>();
  44.         for (CardSecret secret : cardSecrets) {
  45.             OrderItem item = new OrderItem();
  46.             item.setOrderId(order.getId());
  47.             item.setOrderNo(orderNo);
  48.             item.setProductId(req.getProductId());
  49.             item.setProductName(product.getName());
  50.             item.setProductImage(product.getImageUrl());
  51.             item.setQuantity(1);
  52.             item.setPrice(product.getPrice());
  53.             item.setTotalPrice(product.getPrice());
  54.             item.setCardSecretId(secret.getId());
  55.             orderItems.add(item);
  56.         }
  57.         
  58.         orderItemMapper.batchInsert(orderItems);
  59.         
  60.         // 更新卡密的订单ID
  61.         List<Long> cardSecretIds = cardSecrets.stream().map(CardSecret::getId).collect(Collectors.toList());
  62.         cardService.updateCardSecretsOrderId(cardSecretIds, order.getId());
  63.         
  64.         // 返回订单信息
  65.         OrderRes res = new OrderRes();
  66.         BeanUtils.copyProperties(order, res);
  67.         res.setItems(orderItems.stream().map(this::convertToItemRes).collect(Collectors.toList()));
  68.         return res;
  69.     }
  70.    
  71.     @Override
  72.     @Transactional
  73.     public void cancelOrder(Long orderId, Long userId) {
  74.         Order order = orderMapper.selectById(orderId);
  75.         if (order == null) {
  76.             throw new BusinessException("订单不存在");
  77.         }
  78.         
  79.         if (!order.getUserId().equals(userId)) {
  80.             throw new BusinessException("无权操作此订单");
  81.         }
  82.         
  83.         if (order.getStatus() != OrderStatus.UNPAID.getCode()) {
  84.             throw new BusinessException("订单状态不允许取消");
  85.         }
  86.         
  87.         // 更新订单状态
  88.         order.setStatus(OrderStatus.CANCELLED.getCode());
  89.         orderMapper.updateById(order);
  90.         
  91.         // 查询订单明细获取卡密ID
  92.         List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
  93.         List<Long> cardSecretIds = items.stream()
  94.                 .map(OrderItem::getCardSecretId)
  95.                 .filter(Objects::nonNull)
  96.                 .collect(Collectors.toList());
  97.         
  98.         // 解锁卡密
  99.         if (!cardSecretIds.isEmpty()) {
  100.             cardService.unlockCardSecrets(cardSecretIds);
  101.         }
  102.     }
  103.    
  104.     @Override
  105.     @Transactional
  106.     public void payOrderSuccess(String orderNo, String paymentNo, BigDecimal paymentAmount, Date paymentTime) {
  107.         Order order = orderMapper.selectByOrderNo(orderNo);
  108.         if (order == null) {
  109.             throw new BusinessException("订单不存在");
  110.         }
  111.         
  112.         if (order.getStatus() != OrderStatus.UNPAID.getCode()) {
  113.             throw new BusinessException("订单状态不正确");
  114.         }
  115.         
  116.         // 更新订单状态
  117.         order.setStatus(OrderStatus.PAID.getCode());
  118.         order.setPaymentTime(paymentTime);
  119.         orderMapper.updateById(order);
  120.         
  121.         // 更新卡密状态为已售出
  122.         List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
  123.         List<Long> cardSecretIds = items.stream()
  124.                 .map(OrderItem::getCardSecretId)
  125.                 .filter(Objects::nonNull)
  126.                 .collect(Collectors.toList());
  127.         
  128.         if (!cardSecretIds.isEmpty()) {
  129.             cardService.sellCardSecrets(cardSecretIds);
  130.         }
  131.         
  132.         // 创建支付记录
  133.         Payment payment = new Payment();
  134.         payment.setOrderId(order.getId());
  135.         payment.setOrderNo(orderNo);
  136.         payment.setPaymentNo(paymentNo);
  137.         payment.setPaymentType(PaymentType.WECHAT.getCode());
  138.         payment.setPaymentAmount(paymentAmount);
  139.         payment.setPaymentStatus(1);
  140.         payment.setPaymentTime(paymentTime);
  141.         payment.setCallbackTime(new Date());
  142.         paymentService.createPayment(payment);
  143.     }
  144.    
  145.     private String generateOrderNo() {
  146.         return "ORD" + snowFlakeUtil.nextId();
  147.     }
  148.    
  149.     private OrderItemRes convertToItemRes(OrderItem item) {
  150.         OrderItemRes res = new OrderItemRes();
  151.         BeanUtils.copyProperties(item, res);
  152.         return res;
  153.     }
  154. }
复制代码
4.2.4 微信支付集成

  1. // WeChatPayUtil.java
  2. @Component
  3. public class WeChatPayUtil {
  4.    
  5.     @Value("${wechat.pay.appid}")
  6.     private String appId;
  7.    
  8.     @Value("${wechat.pay.mchid}")
  9.     private String mchId;
  10.    
  11.     @Value("${wechat.pay.apikey}")
  12.     private String apiKey;
  13.    
  14.     @Value("${wechat.pay.serialNo}")
  15.     private String serialNo;
  16.    
  17.     @Value("${wechat.pay.privateKey}")
  18.     private String privateKey;
  19.    
  20.     @Value("${wechat.pay.notifyUrl}")
  21.     private String notifyUrl;
  22.    
  23.     private CloseableHttpClient httpClient;
  24.    
  25.     @PostConstruct
  26.     public void init() {
  27.         // 加载商户私钥
  28.         PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes()));
  29.         
  30.         // 构造HttpClient
  31.         httpClient = WechatPayHttpClientBuilder.create()
  32.                 .withMerchant(mchId, serialNo, merchantPrivateKey)
  33.                 .withValidator(new WechatPay2Validator(apiKey.getBytes()))
  34.                 .build();
  35.     }
  36.    
  37.     public Map<String, String> createH5Payment(String orderNo, BigDecimal amount, String description, String clientIp) throws Exception {
  38.         // 构造请求参数
  39.         Map<String, Object> params = new HashMap<>();
  40.         params.put("appid", appId);
  41.         params.put("mchid", mchId);
  42.         params.put("description", description);
  43.         params.put("out_trade_no", orderNo);
  44.         params.put("notify_url", notifyUrl);
  45.         params.put("amount", new HashMap<String, Object>() {{
  46.             put("total", amount.multiply(new BigDecimal(100)).intValue());
  47.             put("currency", "CNY");
  48.         }});
  49.         params.put("scene_info", new HashMap<String, Object>() {{
  50.             put("payer_client_ip", clientIp);
  51.             put("h5_info", new HashMap<String, Object>() {{
  52.                 put("type", "Wap");
  53.             }});
  54.         }});
  55.         
  56.         // 发送请求
  57.         HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/h5");
  58.         httpPost.addHeader("Accept", "application/json");
  59.         httpPost.addHeader("Content-type", "application/json");
  60.         httpPost.setEntity(new StringEntity(JSON.toJSONString(params), "UTF-8"));
  61.         
  62.         CloseableHttpResponse response = httpClient.execute(httpPost);
  63.         try {
  64.             String responseBody = EntityUtils.toString(response.getEntity());
  65.             if (response.getStatusLine().getStatusCode() == 200) {
  66.                 Map<String, String> result = new HashMap<>();
  67.                 JSONObject json = JSON.parseObject(responseBody);
  68.                 result.put("h5_url", json.getString("h5_url"));
  69.                 result.put("prepay_id", json.getString("prepay_id"));
  70.                 return result;
  71.             } else {
  72.                 throw new BusinessException("微信支付创建失败: " + responseBody);
  73.             }
  74.         } finally {
  75.             response.close();
  76.         }
  77.     }
  78.    
  79.     public boolean verifyNotify(Map<String, String> params, String signature, String serial, String nonce, String timestamp, String body) {
  80.         try {
  81.             // 验证签名
  82.             String message = timestamp + "\n" + nonce + "\n" + body + "\n";
  83.             boolean verifyResult = verifySignature(message.getBytes("utf-8"), serial, signature.getBytes("utf-8")));
  84.             if (!verifyResult) {
  85.                 return false;
  86.             }
  87.             
  88.             // 验证订单状态
  89.             JSONObject json = JSON.parseObject(body);
  90.             String orderNo = json.getJSONObject("resource").getString("out_trade_no");
  91.             String tradeState = json.getJSONObject("resource").getString("trade_state");
  92.             
  93.             return "SUCCESS".equals(tradeState);
  94.         } catch (Exception e) {
  95.             return false;
  96.         }
  97.     }
  98.    
  99.     private boolean verifySignature(byte[] message, String serial, byte[] signature) {
  100.         try {
  101.             // 根据证书序列号查询证书
  102.             String cert = getWechatPayCert(serial);
  103.             if (cert == null) {
  104.                 return false;
  105.             }
  106.             
  107.             // 加载证书
  108.             X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(cert));
  109.             KeyFactory keyFactory = KeyFactory.getInstance("RSA");
  110.             PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
  111.             
  112.             // 验证签名
  113.             Signature sign = Signature.getInstance("SHA256withRSA");
  114.             sign.initVerify(publicKey);
  115.             sign.update(message);
  116.             return sign.verify(signature);
  117.         } catch (Exception e) {
  118.             return false;
  119.         }
  120.     }
  121.    
  122.     private String getWechatPayCert(String serial) {
  123.         // 这里应该实现从微信支付平台获取证书的逻辑
  124.         // 实际项目中应该缓存证书,避免频繁请求
  125.         // 简化实现,返回配置的证书
  126.         return "your_wechat_pay_cert_content";
  127.     }
  128. }
  129. // PaymentController.java
  130. @RestController
  131. @RequestMapping("/api/payment")
  132. public class PaymentController {
  133.    
  134.     @Autowired
  135.     private OrderService orderService;
  136.    
  137.     @Autowired
  138.     private PaymentService paymentService;
  139.    
  140.     @Autowired
  141.     private WeChatPayUtil weChatPayUtil;
  142.    
  143.     @PostMapping("/create")
  144.     public ApiResponse<Map<String, String>> createPayment(@RequestBody PaymentReq req,
  145.                                                         HttpServletRequest request) {
  146.         // 查询订单
  147.         Order order = orderService.getOrderByNo(req.getOrderNo());
  148.         if (order == null) {
  149.             return ApiResponse.fail("订单不存在");
  150.         }
  151.         
  152.         if (order.getStatus() != OrderStatus.UNPAID.getCode()) {
  153.             return ApiResponse.fail("订单状态不正确");
  154.         }
  155.         
  156.         // 创建微信支付
  157.         try {
  158.             Map<String, String> result = weChatPayUtil.createH5Payment(
  159.                     order.getOrderNo(),
  160.                     order.getPaymentAmount(),
  161.                     "虚拟卡购买-" + order.getOrderNo(),
  162.                     getClientIp(request));
  163.             
  164.             // 保存支付记录
  165.             Payment payment = new Payment();
  166.             payment.setOrderId(order.getId());
  167.             payment.setOrderNo(order.getOrderNo());
  168.             payment.setPaymentNo(result.get("prepay_id"));
  169.             payment.setPaymentType(PaymentType.WECHAT.getCode());
  170.             payment.setPaymentAmount(order.getPaymentAmount());
  171.             payment.setPaymentStatus(0);
  172.             paymentService.createPayment(payment);
  173.             
  174.             return ApiResponse.success(result);
  175.         } catch (Exception e) {
  176.             return ApiResponse.fail("支付创建失败: " + e.getMessage());
  177.         }
  178.     }
  179.    
  180.     @PostMapping("/callback/wechat")
  181.     public String wechatPayCallback(HttpServletRequest request) {
  182.         try {
  183.             // 获取请求头信息
  184.             String signature = request.getHeader("Wechatpay-Signature");
  185.             String serial = request.getHeader("Wechatpay-Serial");
  186.             String nonce = request.getHeader("Wechatpay-Nonce");
  187.             String timestamp = request.getHeader("Wechatpay-Timestamp");
  188.             
  189.             // 获取请求体
  190.             String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
  191.             
  192.             // 验证回调
  193.             if (!weChatPayUtil.verifyNotify(null, signature, serial, nonce, timestamp, body)) {
  194.                 return "FAIL";
  195.             }
  196.             
  197.             // 解析回调内容
  198.             JSONObject json = JSON.parseObject(body);
  199.             JSONObject resource = json.getJSONObject("resource");
  200.             String orderNo = resource.getString("out_trade_no");
  201.             String transactionId = resource.getString("transaction_id");
  202.             BigDecimal amount = resource.getJSONObject("amount")
  203.                     .getBigDecimal("total")
  204.                     .divide(new BigDecimal(100));
  205.             Date paymentTime = new Date(resource.getLong("success_time") * 1000);
  206.             
  207.             // 处理支付成功逻辑
  208.             orderService.payOrderSuccess(orderNo, transactionId, amount, paymentTime);
  209.             
  210.             return "SUCCESS";
  211.         } catch (Exception e) {
  212.             return "FAIL";
  213.         }
  214.     }
  215.    
  216.     private String getClientIp(HttpServletRequest request) {
  217.         String ip = request.getHeader("X-Forwarded-For");
  218.         if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
  219.             int index = ip.indexOf(",");
  220.             if (index != -1) {
  221.                 return ip.substring(0, index);
  222.             } else {
  223.                 return ip;
  224.             }
  225.         }
  226.         ip = request.getHeader("X-Real-IP");
  227.         if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
  228.             return ip;
  229.         }
  230.         return request.getRemoteAddr();
  231.     }
  232. }
复制代码
5. 前端实现

5.1 用户端前端实现

5.1.1 项目布局

  1. src/
  2. ├── api/                  # API请求
  3. │   ├── auth.js           # 认证相关API
  4. │   ├── card.js           # 虚拟卡相关API
  5. │   ├── order.js          # 订单相关API
  6. │   └── payment.js        # 支付相关API
  7. ├── assets/               # 静态资源
  8. │   ├── css/              # 全局样式
  9. │   └── images/           # 图片资源
  10. ├── components/           # 公共组件
  11. │   ├── CardItem.vue      # 卡产品项组件
  12. │   ├── Header.vue        # 头部组件
  13. │   ├── Footer.vue        # 底部组件
  14. │   └── Loading.vue       # 加载组件
  15. ├── router/               # 路由配置
  16. │   └── index.js          # 路由定义
  17. ├── store/                # Vuex状态管理
  18. │   ├── modules/          # 模块化状态
  19. │   │   ├── auth.js       # 认证模块
  20. │   │   ├── card.js       # 虚拟卡模块
  21. │   │   └── order.js      # 订单模块
  22. │   └── index.js          # 主入口
  23. ├── utils/                # 工具函数
  24. │   ├── request.js        # axios封装
  25. │   ├── auth.js           # 认证工具
  26. │   └── wechat.js         # 微信相关工具
  27. ├── views/                # 页面组件
  28. │   ├── auth/             # 认证相关页面
  29. │   │   ├── Login.vue     # 登录页
  30. │   │   └── Register.vue  # 注册页
  31. │   ├── card/             # 虚拟卡相关页面
  32. │   │   ├── List.vue      # 卡列表页
  33. │   │   └── Detail.vue    # 卡详情页
  34. │   ├── order/            # 订单相关页面
  35. │   │   ├── Create.vue    # 订单创建页
  36. │   │   ├── Detail.vue    # 订单详情页
  37. │   │   └── List.vue      # 订单列表页
  38. │   ├── payment/          # 支付相关页面
  39. │   │   └── Pay.vue       # 支付页
  40. │   ├── Home.vue          # 首页
  41. │   └── User.vue          # 用户中心页
  42. ├── App.vue               # 根组件
  43. └── main.js               # 应用入口
复制代码
5.1.2 核心页面实现

虚拟卡列表页 (Card/List.vue)
  1. <template>
  2.   <div class="card-list">
  3.     <header-component title="虚拟卡商城" :show-back="false" />
  4.    
  5.     <div class="search-box">
  6.       <van-search
  7.         v-model="searchKeyword"
  8.         placeholder="搜索虚拟卡"
  9.         shape="round"
  10.         @search="onSearch"
  11.       />
  12.     </div>
  13.    
  14.     <van-tabs v-model="activeCategory" @click="onCategoryChange">
  15.       <van-tab v-for="category in categories" :key="category.id" :title="category.name" />
  16.     </van-tabs>
  17.    
  18.     <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
  19.       <van-list
  20.         v-model="loading"
  21.         :finished="finished"
  22.         finished-text="没有更多了"
  23.         @load="onLoad"
  24.       >
  25.         <card-item
  26.           v-for="card in cardList"
  27.           :key="card.id"
  28.           :card="card"
  29.           @click="goToDetail(card.id)"
  30.         />
  31.       </van-list>
  32.     </van-pull-refresh>
  33.   </div>
  34. </template>
  35. <script>
  36. import { Search, Tab, Tabs, List, PullRefresh } from 'vant';
  37. import HeaderComponent from '@/components/Header.vue';
  38. import CardItem from '@/components/CardItem.vue';
  39. import { getCardProducts, getCardCategories } from '@/api/card';
  40. export default {
  41.   components: {
  42.     [Search.name]: Search,
  43.     [Tab.name]: Tab,
  44.     [Tabs.name]: Tabs,
  45.     [List.name]: List,
  46.     [PullRefresh.name]: PullRefresh,
  47.     HeaderComponent,
  48.     CardItem
  49.   },
  50.   data() {
  51.     return {
  52.       searchKeyword: '',
  53.       activeCategory: 0,
  54.       categories: [],
  55.       cardList: [],
  56.       loading: false,
  57.       finished: false,
  58.       refreshing: false,
  59.       page: 1,
  60.       pageSize: 10
  61.     };
  62.   },
  63.   created() {
  64.     this.loadCategories();
  65.   },
  66.   methods: {
  67.     async loadCategories() {
  68.       try {
  69.         const res = await getCardCategories();
  70.         this.categories = [{ id: 0, name: '全部' }, ...res.data];
  71.       } catch (error) {
  72.         console.error('加载分类失败', error);
  73.       }
  74.     },
  75.     async onLoad() {
  76.       if (this.refreshing) {
  77.         this.cardList = [];
  78.         this.refreshing = false;
  79.       }
  80.       
  81.       try {
  82.         const params = {
  83.           page: this.page,
  84.           pageSize: this.pageSize,
  85.           categoryId: this.activeCategory === 0 ? null : this.activeCategory,
  86.           keyword: this.searchKeyword
  87.         };
  88.         
  89.         const res = await getCardProducts(params);
  90.         this.cardList = [...this.cardList, ...res.data.list];
  91.         this.loading = false;
  92.         
  93.         if (res.data.list.length < this.pageSize) {
  94.           this.finished = true;
  95.         } else {
  96.           this.page++;
  97.         }
  98.       } catch (error) {
  99.         this.loading = false;
  100.         this.finished = true;
  101.         console.error('加载卡片列表失败', error);
  102.       }
  103.     },
  104.     onRefresh() {
  105.       this.page = 1;
  106.       this.finished = false;
  107.       this.loading = true;
  108.       this.onLoad();
  109.     },
  110.     onSearch() {
  111.       this.page = 1;
  112.       this.cardList = [];
  113.       this.finished = false;
  114.       this.loading = true;
  115.       this.onLoad();
  116.     },
  117.     onCategoryChange() {
  118.       this.page = 1;
  119.       this.cardList = [];
  120.       this.finished = false;
  121.       this.loading = true;
  122.       this.onLoad();
  123.     },
  124.     goToDetail(id) {
  125.       this.$router.push(`/card/detail/${id}`);
  126.     }
  127.   }
  128. };
  129. </script>
  130. <style scoped>
  131. .card-list {
  132.   padding-bottom: 50px;
  133. }
  134. .search-box {
  135.   padding: 10px;
  136. }
  137. </style>
复制代码
订单创建页 (Order/Create.vue)
  1. <template>
  2.   <div class="order-create">
  3.     <header-component title="确认订单" :show-back="true" />
  4.    
  5.     <div class="address-section" v-if="!isVirtual">
  6.       <van-contact-card
  7.         type="edit"
  8.         :name="address.name"
  9.         :tel="address.phone"
  10.         @click="editAddress"
  11.       />
  12.     </div>
  13.    
  14.     <div class="card-info">
  15.       <van-card
  16.         :num="quantity"
  17.         :price="card.price"
  18.         :title="card.name"
  19.         :thumb="card.imageUrl"
  20.       >
  21.         <template #tags>
  22.           <van-tag plain type="danger">虚拟商品</van-tag>
  23.         </template>
  24.       </van-card>
  25.     </div>
  26.    
  27.     <div class="order-section">
  28.       <van-cell-group>
  29.         <van-cell title="购买数量">
  30.           <van-stepper v-model="quantity" integer min="1" :max="card.stock" />
  31.         </van-cell>
  32.         <van-cell title="商品金额" :value="`¥${(card.price * quantity).toFixed(2)}`" />
  33.         <van-cell title="优惠金额" value="¥0.00" />
  34.         <van-cell title="实付金额" :value="`¥${(card.price * quantity).toFixed(2)}`" class="total-price" />
  35.       </van-cell-group>
  36.     </div>
  37.    
  38.     <div class="payment-section">
  39.       <van-radio-group v-model="paymentType">
  40.         <van-cell-group title="支付方式">
  41.           <van-cell title="微信支付" clickable @click="paymentType = 1">
  42.             <template #right-icon>
  43.               <van-radio :name="1" />
  44.             </template>
  45.           </van-cell>
  46.         </van-cell-group>
  47.       </van-radio-group>
  48.     </div>
  49.    
  50.     <div class="submit-section">
  51.       <van-submit-bar
  52.         :price="totalPrice * 100"
  53.         button-text="提交订单"
  54.         @submit="createOrder"
  55.       />
  56.     </div>
  57.   </div>
  58. </template>
  59. <script>
  60. import { ContactCard, Card, Tag, Cell, CellGroup, Radio, RadioGroup, Stepper, SubmitBar } from 'vant';
  61. import HeaderComponent from '@/components/Header.vue';
  62. import { getCardDetail } from '@/api/card';
  63. import { createOrder } from '@/api/order';
  64. export default {
  65.   components: {
  66.     [ContactCard.name]: ContactCard,
  67.     [Card.name]: Card,
  68.     [Tag.name]: Tag,
  69.     [Cell.name]: Cell,
  70.     [CellGroup.name]: CellGroup,
  71.     [Radio.name]: Radio,
  72.     [RadioGroup.name]: RadioGroup,
  73.     [Stepper.name]: Stepper,
  74.     [SubmitBar.name]: SubmitBar,
  75.     HeaderComponent
  76.   },
  77.   data() {
  78.     return {
  79.       cardId: null,
  80.       card: {
  81.         id: null,
  82.         name: '',
  83.         price: 0,
  84.         stock: 0,
  85.         imageUrl: ''
  86.       },
  87.       quantity: 1,
  88.       paymentType: 1,
  89.       address: {
  90.         name: '张三',
  91.         phone: '13800138000',
  92.         address: '北京市朝阳区'
  93.       },
  94.       isVirtual: true
  95.     };
  96.   },
  97.   computed: {
  98.     totalPrice() {
  99.       return this.card.price * this.quantity;
  100.     }
  101.   },
  102.   created() {
  103.     this.cardId = this.$route.params.id;
  104.     this.loadCardDetail();
  105.   },
  106.   methods: {
  107.     async loadCardDetail() {
  108.       try {
  109.         const res = await getCardDetail(this.cardId);
  110.         this.card = res.data;
  111.       } catch (error) {
  112.         this.$toast.fail('加载卡片详情失败');
  113.         console.error(error);
  114.       }
  115.     },
  116.     editAddress() {
  117.       this.$router.push('/address/edit');
  118.     },
  119.     async createOrder() {
  120.       try {
  121.         this.$toast.loading({
  122.           message: '创建订单中...',
  123.           forbidClick: true
  124.         });
  125.         
  126.         const params = {
  127.           productId: this.cardId,
  128.           quantity: this.quantity
  129.         };
  130.         
  131.         const res = await createOrder(params);
  132.         this.$toast.clear();
  133.         
  134.         // 跳转到支付页面
  135.         this.$router.push({
  136.           path: '/payment/pay',
  137.           query: {
  138.             orderNo: res.data.orderNo,
  139.             amount: this.totalPrice
  140.           }
  141.         });
  142.       } catch (error) {
  143.         this.$toast.clear();
  144.         this.$toast.fail(error.message || '创建订单失败');
  145.         console.error(error);
  146.       }
  147.     }
  148.   }
  149. };
  150. </script>
  151. <style scoped>
  152. .order-create {
  153.   padding-bottom: 100px;
  154. }
  155. .address-section {
  156.   margin-bottom: 10px;
  157. }
  158. .card-info {
  159.   margin-bottom: 10px;
  160. }
  161. .total-price {
  162.   font-weight: bold;
  163.   color: #ee0a24;
  164. }
  165. </style>
复制代码
微信支付页 (Payment/Pay.vue)
  1. <template>
  2.   <div class="payment-page">
  3.     <header-component title="支付订单" :show-back="true" />
  4.    
  5.     <div class="payment-info">
  6.       <van-cell-group>
  7.         <van-cell title="订单编号" :value="orderNo" />
  8.         <van-cell title="支付金额">
  9.           <span class="price">¥{{ amount.toFixed(2) }}</span>
  10.         </van-cell>
  11.       </van-cell-group>
  12.     </div>
  13.    
  14.     <div class="payment-methods">
  15.       <van-radio-group v-model="paymentMethod">
  16.         <van-cell-group title="选择支付方式">
  17.           <van-cell title="微信支付" clickable @click="paymentMethod = 'wechat'">
  18.             <template #right-icon>
  19.               <van-radio name="wechat" />
  20.             </template>
  21.             <template #icon>
  22.               <img src="@/assets/images/wechat-pay.png" class="pay-icon" />
  23.             </template>
  24.           </van-cell>
  25.         </van-cell-group>
  26.       </van-radio-group>
  27.     </div>
  28.    
  29.     <div class="payment-btn">
  30.       <van-button
  31.         type="primary"
  32.         block
  33.         round
  34.         :loading="loading"
  35.         @click="handlePayment"
  36.       >
  37.         立即支付
  38.       </van-button>
  39.     </div>
  40.    
  41.     <van-dialog
  42.       v-model="showPaymentDialog"
  43.       title="微信支付"
  44.       show-cancel-button
  45.       :before-close="beforeClose"
  46.     >
  47.       <div class="payment-dialog">
  48.         <div v-if="paymentStatus === 'pending'" class="payment-pending">
  49.           <van-loading size="24px">正在调起支付...</van-loading>
  50.         </div>
  51.         <div v-else-if="paymentStatus === 'success'" class="payment-success">
  52.           <van-icon name="checked" color="#07c160" size="50px" />
  53.           <p>支付成功</p>
  54.         </div>
  55.         <div v-else class="payment-failed">
  56.           <van-icon name="close" color="#ee0a24" size="50px" />
  57.           <p>支付失败</p>
  58.           <p class="error-msg">{{ errorMsg }}</p>
  59.         </div>
  60.       </div>
  61.     </van-dialog>
  62.   </div>
  63. </template>
  64. <script>
  65. import { Cell, CellGroup, Radio, RadioGroup, Button, Dialog, Loading, Icon } from 'vant';
  66. import HeaderComponent from '@/components/Header.vue';
  67. import { createPayment } from '@/api/payment';
  68. import { getOrderDetail } from '@/api/order';
  69. import { isWeixinBrowser, wechatPay } from '@/utils/wechat';
  70. export default {
  71.   components: {
  72.     [Cell.name]: Cell,
  73.     [CellGroup.name]: CellGroup,
  74.     [Radio.name]: Radio,
  75.     [RadioGroup.name]: RadioGroup,
  76.     [Button.name]: Button,
  77.     [Dialog.name]: Dialog,
  78.     [Loading.name]: Loading,
  79.     [Icon.name]: Icon,
  80.     HeaderComponent
  81.   },
  82.   data() {
  83.     return {
  84.       orderNo: this.$route.query.orderNo,
  85.       amount: parseFloat(this.$route.query.amount),
  86.       paymentMethod: 'wechat',
  87.       loading: false,
  88.       showPaymentDialog: false,
  89.       paymentStatus: 'pending', // pending, success, failed
  90.       errorMsg: '',
  91.       timer: null,
  92.       isWeixin: isWeixinBrowser()
  93.     };
  94.   },
  95.   beforeDestroy() {
  96.     if (this.timer) {
  97.       clearInterval(this.timer);
  98.     }
  99.   },
  100.   methods: {
  101.     async handlePayment() {
  102.       if (this.paymentMethod !== 'wechat') {
  103.         this.$toast('请选择微信支付');
  104.         return;
  105.       }
  106.       
  107.       this.loading = true;
  108.       
  109.       try {
  110.         // 创建支付
  111.         const res = await createPayment({
  112.           orderNo: this.orderNo,
  113.           paymentType: 1 // 微信支付
  114.         });
  115.         
  116.         this.loading = false;
  117.         
  118.         if (this.isWeixin) {
  119.           // 微信浏览器内使用JSAPI支付
  120.           await this.wechatJsApiPay(res.data);
  121.         } else {
  122.           // 非微信浏览器使用H5支付
  123.           this.showPaymentDialog = true;
  124.           window.location.href = res.data.h5Url;
  125.          
  126.           // 启动轮询检查支付状态
  127.           this.startPaymentCheck();
  128.         }
  129.       } catch (error) {
  130.         this.loading = false;
  131.         this.$toast.fail(error.message || '支付创建失败');
  132.         console.error(error);
  133.       }
  134.     },
  135.     async wechatJsApiPay(paymentData) {
  136.       try {
  137.         await wechatPay(paymentData);
  138.         
  139.         // 支付成功,跳转到结果页
  140.         this.$router.push({
  141.           path: '/payment/result',
  142.           query: {
  143.             orderNo: this.orderNo,
  144.             status: 'success'
  145.           }
  146.         });
  147.       } catch (error) {
  148.         this.$toast.fail(error.message || '支付失败');
  149.         console.error(error);
  150.       }
  151.     },
  152.     startPaymentCheck() {
  153.       this.timer = setInterval(async () => {
  154.         try {
  155.           const res = await getOrderDetail(this.orderNo);
  156.          
  157.           if (res.data.status === 1) { // 已支付
  158.             this.paymentStatus = 'success';
  159.             clearInterval(this.timer);
  160.             
  161.             // 3秒后自动跳转
  162.             setTimeout(() => {
  163.               this.showPaymentDialog = false;
  164.               this.$router.push({
  165.                 path: '/payment/result',
  166.                 query: {
  167.                   orderNo: this.orderNo,
  168.                   status: 'success'
  169.                 }
  170.               });
  171.             }, 3000);
  172.           }
  173.         } catch (error) {
  174.           console.error('检查支付状态失败', error);
  175.         }
  176.       }, 3000);
  177.     },
  178.     beforeClose(action, done) {
  179.       if (action === 'confirm') {
  180.         if (this.paymentStatus === 'pending') {
  181.           this.$toast('支付处理中,请稍候');
  182.           done(false);
  183.         } else {
  184.           done();
  185.           this.$router.push({
  186.             path: '/payment/result',
  187.             query: {
  188.               orderNo: this.orderNo,
  189.               status: this.paymentStatus
  190.             }
  191.           });
  192.         }
  193.       } else {
  194.         done();
  195.       }
  196.     }
  197.   }
  198. };
  199. </script>
  200. <style scoped>
  201. .payment-page {
  202.   padding-bottom: 100px;
  203. }
  204. .payment-info {
  205.   margin-bottom: 10px;
  206. }
  207. .price {
  208.   color: #ee0a24;
  209.   font-weight: bold;
  210. }
  211. .pay-icon {
  212.   width: 24px;
  213.   height: 24px;
  214.   margin-right: 10px;
  215. }
  216. .payment-btn {
  217.   margin: 20px 15px;
  218. }
  219. .payment-dialog {
  220.   padding: 20px;
  221.   text-align: center;
  222. }
  223. .payment-success,
  224. .payment-failed {
  225.   padding: 20px 0;
  226. }
  227. .payment-success p,
  228. .payment-failed p {
  229.   margin-top: 10px;
  230.   font-size: 16px;
  231. }
  232. .error-msg {
  233.   color: #ee0a24;
  234.   font-size: 14px;
  235. }
  236. </style>
复制代码
5.2 管理端前端实现

5.2.1 项目布局

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


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

虚拟卡产品管理页 (Card/List.vue)
  1. <template>
  2.   <div class="card-management">
  3.     <el-card class="search-card">
  4.       <el-form :inline="true" :model="searchForm" class="search-form">
  5.         <el-form-item label="产品名称">
  6.           <el-input v-model="searchForm.name" placeholder="请输入产品名称" clearable />
  7.         </el-form-item>
  8.         <el-form-item label="产品分类">
  9.           <el-select v-model="searchForm.categoryId" placeholder="请选择分类" clearable>
  10.             <el-option
  11.               v-for="category in categories"
  12.               :key="category.id"
  13.               :label="category.name"
  14.               :value="category.id"
  15.             />
  16.           </el-select>
  17.         </el-form-item>
  18.         <el-form-item label="状态">
  19.           <el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
  20.             <el-option label="上架" :value="1" />
  21.             <el-option label="下架" :value="0" />
  22.           </el-select>
  23.         </el-form-item>
  24.         <el-form-item>
  25.           <el-button type="primary" @click="handleSearch">查询</el-button>
  26.           <el-button @click="resetSearch">重置</el-button>
  27.         </el-form-item>
  28.       </el-form>
  29.     </el-card>
  30.    
  31.     <el-card class="operation-card">
  32.       <el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增产品</el-button>
  33.       <el-button type="danger" icon="el-icon-delete" :disabled="!selectedItems.length" @click="handleBatchDelete">
  34.         批量删除
  35.       </el-button>
  36.     </el-card>
  37.    
  38.     <el-card>
  39.       <el-table
  40.         :data="tableData"
  41.         border
  42.         style="width: 100%"
  43.         @selection-change="handleSelectionChange"
  44.         v-loading="loading"
  45.       >
  46.         <el-table-column type="selection" width="55" />
  47.         <el-table-column prop="id" label="ID" width="80" />
  48.         <el-table-column prop="name" label="产品名称" min-width="150" />
  49.         <el-table-column label="分类" width="120">
  50.           <template slot-scope="scope">
  51.             {{ getCategoryName(scope.row.categoryId) }}
  52.           </template>
  53.         </el-table-column>
  54.         <el-table-column prop="price" label="价格" width="120">
  55.           <template slot-scope="scope">
  56.             ¥{{ scope.row.price.toFixed(2) }}
  57.           </template>
  58.         </el-table-column>
  59.         <el-table-column prop="stock" label="库存" width="100" />
  60.         <el-table-column label="状态" width="100">
  61.           <template slot-scope="scope">
  62.             <el-tag :type="scope.row.status ? 'success' : 'danger'">
  63.               {{ scope.row.status ? '上架' : '下架' }}
  64.             </el-tag>
  65.           </template>
  66.         </el-table-column>
  67.         <el-table-column prop="createTime" label="创建时间" width="180" />
  68.         <el-table-column label="操作" width="180" fixed="right">
  69.           <template slot-scope="scope">
  70.             <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
  71.             <el-button
  72.               size="mini"
  73.               :type="scope.row.status ? 'danger' : 'success'"
  74.               @click="handleStatusChange(scope.row)"
  75.             >
  76.               {{ scope.row.status ? '下架' : '上架' }}
  77.             </el-button>
  78.           </template>
  79.         </el-table-column>
  80.       </el-table>
  81.       
  82.       <el-pagination
  83.         @size-change="handleSizeChange"
  84.         @current-change="handleCurrentChange"
  85.         :current-page="pagination.current"
  86.         :page-sizes="[10, 20, 50, 100]"
  87.         :page-size="pagination.size"
  88.         layout="total, sizes, prev, pager, next, jumper"
  89.         :total="pagination.total"
  90.         class="pagination"
  91.       />
  92.     </el-card>
  93.    
  94.     <!-- 新增/编辑对话框 -->
  95.     <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="50%">
  96.       <el-form :model="dialogForm" :rules="rules" ref="dialogForm" label-width="100px">
  97.         <el-form-item label="产品名称" prop="name">
  98.           <el-input v-model="dialogForm.name" placeholder="请输入产品名称" />
  99.         </el-form-item>
  100.         <el-form-item label="产品分类" prop="categoryId">
  101.           <el-select v-model="dialogForm.categoryId" placeholder="请选择分类">
  102.             <el-option
  103.               v-for="category in categories"
  104.               :key="category.id"
  105.               :label="category.name"
  106.               :value="category.id"
  107.             />
  108.           </el-select>
  109.         </el-form-item>
  110.         <el-form-item label="产品价格" prop="price">
  111.           <el-input-number v-model="dialogForm.price" :min="0" :precision="2" :step="0.1" />
  112.         </el-form-item>
  113.         <el-form-item label="原价" prop="originalPrice">
  114.           <el-input-number v-model="dialogForm.originalPrice" :min="0" :precision="2" :step="0.1" />
  115.         </el-form-item>
  116.         <el-form-item label="产品图片" prop="imageUrl">
  117.           <el-upload
  118.             class="avatar-uploader"
  119.             action="/api/upload"
  120.             :show-file-list="false"
  121.             :on-success="handleImageSuccess"
  122.             :before-upload="beforeImageUpload"
  123.           >
  124.             <img v-if="dialogForm.imageUrl" :src="dialogForm.imageUrl" class="avatar" />
  125.             <i v-else class="el-icon-plus avatar-uploader-icon"></i>
  126.           </el-upload>
  127.         </el-form-item>
  128.         <el-form-item label="详情图片" prop="detailImages">
  129.           <el-upload
  130.             action="/api/upload"
  131.             list-type="picture-card"
  132.             :file-list="detailImageList"
  133.             :on-success="handleDetailImageSuccess"
  134.             :on-remove="handleDetailImageRemove"
  135.             :before-upload="beforeImageUpload"
  136.             multiple
  137.           >
  138.             <i class="el-icon-plus"></i>
  139.           </el-upload>
  140.         </el-form-item>
  141.         <el-form-item label="产品描述" prop="description">
  142.           <el-input
  143.             type="textarea"
  144.             :rows="4"
  145.             v-model="dialogForm.description"
  146.             placeholder="请输入产品描述"
  147.           />
  148.         </el-form-item>
  149.         <el-form-item label="排序权重" prop="sortOrder">
  150.           <el-input-number v-model="dialogForm.sortOrder" :min="0" />
  151.         </el-form-item>
  152.         <el-form-item label="状态" prop="status">
  153.           <el-switch v-model="dialogForm.status" :active-value="1" :inactive-value="0" />
  154.         </el-form-item>
  155.       </el-form>
  156.       <span slot="footer" class="dialog-footer">
  157.         <el-button @click="dialogVisible = false">取 消</el-button>
  158.         <el-button type="primary" @click="submitForm">确 定</el-button>
  159.       </span>
  160.     </el-dialog>
  161.   </div>
  162. </template>
  163. <script>
  164. import { getCardProducts, addCardProduct, updateCardProduct, deleteCardProduct, updateCardProductStatus } from '@/api/card';
  165. import { getCardCategories } from '@/api/category';
  166. export default {
  167.   data() {
  168.     return {
  169.       searchForm: {
  170.         name: '',
  171.         categoryId: null,
  172.         status: null
  173.       },
  174.       tableData: [],
  175.       selectedItems: [],
  176.       categories: [],
  177.       loading: false,
  178.       pagination: {
  179.         current: 1,
  180.         size: 10,
  181.         total: 0
  182.       },
  183.       dialogVisible: false,
  184.       dialogTitle: '新增产品',
  185.       dialogForm: {
  186.         id: null,
  187.         name: '',
  188.         categoryId: null,
  189.         price: 0,
  190.         originalPrice: 0,
  191.         imageUrl: '',
  192.         detailImages: [],
  193.         description: '',
  194.         sortOrder: 0,
  195.         status: 1
  196.       },
  197.       detailImageList: [],
  198.       rules: {
  199.         name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
  200.         categoryId: [{ required: true, message: '请选择产品分类', trigger: 'change' }],
  201.         price: [{ required: true, message: '请输入产品价格', trigger: 'blur' }]
  202.       }
  203.     };
  204.   },
  205.   created() {
  206.     this.loadCategories();
  207.     this.loadTableData();
  208.   },
  209.   methods: {
  210.     async loadCategories() {
  211.       try {
  212.         const res = await getCardCategories();
  213.         this.categories = res.data;
  214.       } catch (error) {
  215.         console.error('加载分类失败', error);
  216.       }
  217.     },
  218.     async loadTableData() {
  219.       this.loading = true;
  220.       
  221.       try {
  222.         const params = {
  223.           ...this.searchForm,
  224.           page: this.pagination.current,
  225.           pageSize: this.pagination.size
  226.         };
  227.         
  228.         const res = await getCardProducts(params);
  229.         this.tableData = res.data.list;
  230.         this.pagination.total = res.data.total;
  231.       } catch (error) {
  232.         console.error('加载产品列表失败', error);
  233.       } finally {
  234.         this.loading = false;
  235.       }
  236.     },
  237.     getCategoryName(categoryId) {
  238.       const category = this.categories.find(item => item.id === categoryId);
  239.       return category ? category.name : '--';
  240.     },
  241.     handleSearch() {
  242.       this.pagination.current = 1;
  243.       this.loadTableData();
  244.     },
  245.     resetSearch() {
  246.       this.searchForm = {
  247.         name: '',
  248.         categoryId: null,
  249.         status: null
  250.       };
  251.       this.pagination.current = 1;
  252.       this.loadTableData();
  253.     },
  254.     handleSelectionChange(val) {
  255.       this.selectedItems = val;
  256.     },
  257.     handleSizeChange(val) {
  258.       this.pagination.size = val;
  259.       this.loadTableData();
  260.     },
  261.     handleCurrentChange(val) {
  262.       this.pagination.current = val;
  263.       this.loadTableData();
  264.     },
  265.     handleAdd() {
  266.       this.dialogTitle = '新增产品';
  267.       this.dialogForm = {
  268.         id: null,
  269.         name: '',
  270.         categoryId: null,
  271.         price: 0,
  272.         originalPrice: 0,
  273.         imageUrl: '',
  274.         detailImages: [],
  275.         description: '',
  276.         sortOrder: 0,
  277.         status: 1
  278.       };
  279.       this.detailImageList = [];
  280.       this.dialogVisible = true;
  281.     },
  282.     handleEdit(row) {
  283.       this.dialogTitle = '编辑产品';
  284.       this.dialogForm = {
  285.         ...row,
  286.         detailImages: row.detailImages ? JSON.parse(row.detailImages) : []
  287.       };
  288.       this.detailImageList = this.dialogForm.detailImages.map(url => ({
  289.         url,
  290.         name: url.substring(url.lastIndexOf('/') + 1)
  291.       }));
  292.       this.dialogVisible = true;
  293.     },
  294.     async handleStatusChange(row) {
  295.       try {
  296.         await updateCardProductStatus(row.id, row.status ? 0 : 1);
  297.         this.$message.success('状态更新成功');
  298.         this.loadTableData();
  299.       } catch (error) {
  300.         this.$message.error('状态更新失败');
  301.         console.error(error);
  302.       }
  303.     },
  304.     handleBatchDelete() {
  305.       this.$confirm('确定要删除选中的产品吗?', '提示', {
  306.         confirmButtonText: '确定',
  307.         cancelButtonText: '取消',
  308.         type: 'warning'
  309.       }).then(async () => {
  310.         try {
  311.           const ids = this.selectedItems.map(item => item.id);
  312.           await deleteCardProduct(ids);
  313.           this.$message.success('删除成功');
  314.           this.loadTableData();
  315.         } catch (error) {
  316.           this.$message.error('删除失败');
  317.           console.error(error);
  318.         }
  319.       }).catch(() => {});
  320.     },
  321.     handleImageSuccess(res, file) {
  322.       this.dialogForm.imageUrl = res.data.url;
  323.     },
  324.     handleDetailImageSuccess(res, file) {
  325.       this.dialogForm.detailImages.push(res.data.url);
  326.     },
  327.     handleDetailImageRemove(file, fileList) {
  328.       const url = file.url || file.response.data.url;
  329.       this.dialogForm.detailImages = this.dialogForm.detailImages.filter(item => item !== url);
  330.     },
  331.     beforeImageUpload(file) {
  332.       const isImage = file.type.startsWith('image/');
  333.       const isLt2M = file.size / 1024 / 1024 < 2;
  334.       
  335.       if (!isImage) {
  336.         this.$message.error('只能上传图片!');
  337.       }
  338.       if (!isLt2M) {
  339.         this.$message.error('图片大小不能超过2MB!');
  340.       }
  341.       
  342.       return isImage && isLt2M;
  343.     },
  344.     submitForm() {
  345.       this.$refs.dialogForm.validate(async valid => {
  346.         if (!valid) {
  347.           return;
  348.         }
  349.         
  350.         try {
  351.           const formData = {
  352.             ...this.dialogForm,
  353.             detailImages: JSON.stringify(this.dialogForm.detailImages)
  354.           };
  355.          
  356.           if (this.dialogForm.id) {
  357.             await updateCardProduct(formData);
  358.             this.$message.success('更新成功');
  359.           } else {
  360.             await addCardProduct(formData);
  361.             this.$message.success('添加成功');
  362.           }
  363.          
  364.           this.dialogVisible = false;
  365.           this.loadTableData();
  366.         } catch (error) {
  367.           this.$message.error(error.message || '操作失败');
  368.           console.error(error);
  369.         }
  370.       });
  371.     }
  372.   }
  373. };
  374. </script>
  375. <style scoped>
  376. .search-card {
  377.   margin-bottom: 20px;
  378. }
  379. .search-form {
  380.   display: flex;
  381.   flex-wrap: wrap;
  382. }
  383. .operation-card {
  384.   margin-bottom: 20px;
  385. }
  386. .pagination {
  387.   margin-top: 20px;
  388.   text-align: right;
  389. }
  390. .avatar-uploader {
  391.   border: 1px dashed #d9d9d9;
  392.   border-radius: 6px;
  393.   cursor: pointer;
  394.   position: relative;
  395.   overflow: hidden;
  396.   width: 150px;
  397.   height: 150px;
  398. }
  399. .avatar-uploader:hover {
  400.   border-color: #409EFF;
  401. }
  402. .avatar-uploader-icon {
  403.   font-size: 28px;
  404.   color: #8c939d;
  405.   width: 150px;
  406.   height: 150px;
  407.   line-height: 150px;
  408.   text-align: center;
  409. }
  410. .avatar {
  411.   width: 150px;
  412.   height: 150px;
  413.   display: block;
  414. }
  415. </style>
复制代码
6. 微信H5支付集成

6.1 微信支付配置


  • 申请微信支付商户号

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

  • 配置支付域名

    • 在商户平台配置支付域名(需备案)
    • 配置授权目次和回调域名

  • 获取API密钥和证书

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

  • 配置应用信息

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

6.2 支付流程实现


  • 前端发起支付哀求
  1. // src/utils/wechat.js
  2. import axios from 'axios';
  3. export function isWeixinBrowser() {
  4.   return /micromessenger/i.test(navigator.userAgent);
  5. }
  6. export async function wechatPay(paymentData) {
  7.   return new Promise((resolve, reject) => {
  8.     if (typeof WeixinJSBridge === 'undefined') {
  9.       if (document.addEventListener) {
  10.         document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
  11.       } else if (document.attachEvent) {
  12.         document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
  13.         document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
  14.       }
  15.       reject(new Error('请在微信中打开页面'));
  16.     } else {
  17.       onBridgeReady();
  18.     }
  19.    
  20.     function onBridgeReady() {
  21.       WeixinJSBridge.invoke(
  22.         'getBrandWCPayRequest',
  23.         {
  24.           appId: paymentData.appId,
  25.           timeStamp: paymentData.timeStamp,
  26.           nonceStr: paymentData.nonceStr,
  27.           package: paymentData.package,
  28.           signType: paymentData.signType,
  29.           paySign: paymentData.paySign
  30.         },
  31.         function(res) {
  32.           if (res.err_msg === 'get_brand_wcpay_request:ok') {
  33.             resolve();
  34.           } else {
  35.             reject(new Error(res.err_msg || '支付失败'));
  36.           }
  37.         }
  38.       );
  39.     }
  40.   });
  41. }
复制代码

  • 后端处理支付回调
  1. // PaymentController.java
  2. @RestController
  3. @RequestMapping("/api/payment")
  4. public class PaymentController {
  5.    
  6.     // ... 其他代码 ...
  7.    
  8.     @PostMapping("/callback/wechat")
  9.     public String wechatPayCallback(HttpServletRequest request) {
  10.         try {
  11.             // 获取请求头信息
  12.             String signature = request.getHeader("Wechatpay-Signature");
  13.             String serial = request.getHeader("Wechatpay-Serial");
  14.             String nonce = request.getHeader("Wechatpay-Nonce");
  15.             String timestamp = request.getHeader("Wechatpay-Timestamp");
  16.             
  17.             // 获取请求体
  18.             String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
  19.             
  20.             // 验证回调
  21.             if (!weChatPayUtil.verifyNotify(null, signature, serial, nonce, timestamp, body)) {
  22.                 log.error("微信支付回调验证失败");
  23.                 return "FAIL";
  24.             }
  25.             
  26.             // 解析回调内容
  27.             JSONObject json = JSON.parseObject(body);
  28.             JSONObject resource = json.getJSONObject("resource");
  29.             String orderNo = resource.getString("out_trade_no");
  30.             String transactionId = resource.getString("transaction_id");
  31.             BigDecimal amount = resource.getJSONObject("amount")
  32.                     .getBigDecimal("total")
  33.                     .divide(new BigDecimal(100));
  34.             Date paymentTime = new Date(resource.getLong("success_time") * 1000);
  35.             
  36.             // 处理支付成功逻辑
  37.             orderService.payOrderSuccess(orderNo, transactionId, amount, paymentTime);
  38.             
  39.             log.info("微信支付回调处理成功, orderNo: {}", orderNo);
  40.             return "SUCCESS";
  41.         } catch (Exception e) {
  42.             log.error("微信支付回调处理失败", e);
  43.             return "FAIL";
  44.         }
  45.     }
  46. }
复制代码
其他略…

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张裕

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表