马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
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 后端技术栈
- // pom.xml 主要依赖
- <dependencies>
- <!-- Spring Boot Starter -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
-
- <!-- 数据库相关 -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <version>3.5.1</version>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>1.2.8</version>
- </dependency>
-
- <!-- 工具类 -->
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
- </dependency>
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <version>31.0.1-jre</version>
- </dependency>
-
- <!-- 微信支付SDK -->
- <dependency>
- <groupId>com.github.wechatpay-apiv3</groupId>
- <artifactId>wechatpay-apache-httpclient</artifactId>
- <version>0.4.7</version>
- </dependency>
-
- <!-- 其他 -->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- </dependencies>
复制代码 2.2 前端技术栈
- # 管理端前端
- vue create admin-frontend
- cd admin-frontend
- vue add element-ui
- npm install axios vue-router vuex --save
- # 用户端前端
- vue create user-frontend
- cd user-frontend
- npm install vant axios vue-router vuex --save
复制代码 2.3 开发环境配置
- 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接口)
- 配置应用信息
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";
- }
- }
- }
复制代码 其他略…
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|