Web虚拟卡销售店铺实现方案
文章目录
1. 项目概述
1.1 项目背景
随着数字经济的发展,虚拟卡(如礼品卡、会员卡、游戏点卡等)的市场需求日益增长。本项目旨在构建一个完整的Web虚拟卡销售平台,包含前端销售系统、后端管理系统和移动端H5支付功能,采用Java作为后端技术栈,Vue.js作为前端框架,并集成微信支付功能。
1.2 系统架构
系统采用前后端分离架构:
- 前端:Vue.js + Element UI (管理端) + Vant (移动端)
- 后端:Spring Boot + Spring Security + MyBatis Plus
- 数据库:MySQL
- 缓存:Redis
- 支付:微信支付H5 API
2. 技术选型与环境搭建
2.1 后端技术栈
// pom.xml 主要依赖<dependencies><!--SpringBootStarter--><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 npminstall axios vue-router vuex --save # 用户端前端 vue create user-frontend cd user-frontend npminstall 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 数据表设计
-- 用户表CREATETABLE`user`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`username`varchar(50)NOTNULLCOMMENT'用户名',`password`varchar(100)NOTNULLCOMMENT'密码',`email`varchar(100)DEFAULTNULLCOMMENT'邮箱',`phone`varchar(20)DEFAULTNULLCOMMENT'手机号',`avatar`varchar(255)DEFAULTNULLCOMMENT'头像',`status`tinyint(1)DEFAULT'1'COMMENT'状态:0-禁用,1-正常',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`idx_username`(`username`),KEY`idx_phone`(`phone`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户表';-- 虚拟卡产品表CREATETABLE`card_product`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`name`varchar(100)NOTNULLCOMMENT'产品名称',`category_id`bigint(20)NOTNULLCOMMENT'分类ID',`description`textCOMMENT'产品描述',`price`decimal(10,2)NOTNULLCOMMENT'售价',`original_price`decimal(10,2)DEFAULTNULLCOMMENT'原价',`stock`int(11)NOTNULLDEFAULT'0'COMMENT'库存',`image_url`varchar(255)DEFAULTNULLCOMMENT'图片URL',`detail_images`textCOMMENT'详情图片,JSON数组',`status`tinyint(1)DEFAULT'1'COMMENT'状态:0-下架,1-上架',`sort_order`int(11)DEFAULT'0'COMMENT'排序权重',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),KEY`idx_category`(`category_id`),KEY`idx_status`(`status`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='虚拟卡产品表';-- 卡密库存表CREATETABLE`card_secret`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`product_id`bigint(20)NOTNULLCOMMENT'产品ID',`card_no`varchar(100)NOTNULLCOMMENT'卡号',`card_password`varchar(100)NOTNULLCOMMENT'卡密',`status`tinyint(1)DEFAULT'0'COMMENT'状态:0-未售出,1-已售出,2-已锁定',`order_id`bigint(20)DEFAULTNULLCOMMENT'订单ID',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`idx_card_no`(`card_no`),KEY`idx_product_id`(`product_id`),KEY`idx_status`(`status`),KEY`idx_order_id`(`order_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='卡密库存表';-- 订单表CREATETABLE`order`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`order_no`varchar(50)NOTNULLCOMMENT'订单编号',`user_id`bigint(20)NOTNULLCOMMENT'用户ID',`total_amount`decimal(10,2)NOTNULLCOMMENT'订单总金额',`payment_amount`decimal(10,2)NOTNULLCOMMENT'实付金额',`payment_type`tinyint(1)DEFAULTNULLCOMMENT'支付方式:1-微信,2-支付宝',`status`tinyint(1)DEFAULT'0'COMMENT'订单状态:0-待支付,1-已支付,2-已发货,3-已完成,4-已取消',`payment_time`datetimeDEFAULTNULLCOMMENT'支付时间',`complete_time`datetimeDEFAULTNULLCOMMENT'完成时间',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`idx_order_no`(`order_no`),KEY`idx_user_id`(`user_id`),KEY`idx_status`(`status`),KEY`idx_create_time`(`create_time`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='订单表';-- 订单明细表CREATETABLE`order_item`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`order_id`bigint(20)NOTNULLCOMMENT'订单ID',`order_no`varchar(50)NOTNULLCOMMENT'订单编号',`product_id`bigint(20)NOTNULLCOMMENT'产品ID',`product_name`varchar(100)NOTNULLCOMMENT'产品名称',`product_image`varchar(255)DEFAULTNULLCOMMENT'产品图片',`quantity`int(11)NOTNULLCOMMENT'购买数量',`price`decimal(10,2)NOTNULLCOMMENT'单价',`total_price`decimal(10,2)NOTNULLCOMMENT'总价',`card_secret_id`bigint(20)DEFAULTNULLCOMMENT'卡密ID',`card_no`varchar(100)DEFAULTNULLCOMMENT'卡号',`card_password`varchar(100)DEFAULTNULLCOMMENT'卡密',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',PRIMARYKEY(`id`),KEY`idx_order_id`(`order_id`),KEY`idx_order_no`(`order_no`),KEY`idx_product_id`(`product_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='订单明细表';-- 支付记录表CREATETABLE`payment`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`order_id`bigint(20)NOTNULLCOMMENT'订单ID',`order_no`varchar(50)NOTNULLCOMMENT'订单编号',`payment_no`varchar(50)NOTNULLCOMMENT'支付流水号',`payment_type`tinyint(1)NOTNULLCOMMENT'支付方式:1-微信,2-支付宝',`payment_amount`decimal(10,2)NOTNULLCOMMENT'支付金额',`payment_status`tinyint(1)DEFAULT'0'COMMENT'支付状态:0-未支付,1-支付成功,2-支付失败',`payment_time`datetimeDEFAULTNULLCOMMENT'支付时间',`callback_time`datetimeDEFAULTNULLCOMMENT'回调时间',`callback_content`textCOMMENT'回调内容',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`idx_payment_no`(`payment_no`),KEY`idx_order_id`(`order_id`),KEY`idx_order_no`(`order_no`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='支付记录表';-- 管理员表CREATETABLE`admin`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`username`varchar(50)NOTNULLCOMMENT'用户名',`password`varchar(100)NOTNULLCOMMENT'密码',`nickname`varchar(50)DEFAULTNULLCOMMENT'昵称',`avatar`varchar(255)DEFAULTNULLCOMMENT'头像',`email`varchar(100)DEFAULTNULLCOMMENT'邮箱',`phone`varchar(20)DEFAULTNULLCOMMENT'手机号',`status`tinyint(1)DEFAULT'1'COMMENT'状态:0-禁用,1-正常',`last_login_time`datetimeDEFAULTNULLCOMMENT'最后登录时间',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`idx_username`(`username`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='管理员表';-- 系统日志表CREATETABLE`sys_log`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`user_id`bigint(20)DEFAULTNULLCOMMENT'用户ID',`username`varchar(50)DEFAULTNULLCOMMENT'用户名',`operation`varchar(50)DEFAULTNULLCOMMENT'用户操作',`method`varchar(200)DEFAULTNULLCOMMENT'请求方法',`params`textCOMMENT'请求参数',`time`bigint(20)DEFAULTNULLCOMMENT'执行时长(毫秒)',`ip`varchar(64)DEFAULTNULLCOMMENT'IP地址',`create_time`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=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)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateUserDetailsService userDetailsService;@AutowiredprivateJwtAuthenticationFilter jwtAuthenticationFilter;@AutowiredprivateJwtAccessDeniedHandler jwtAccessDeniedHandler;@AutowiredprivateJwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{ auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{ 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@OverridepublicAuthenticationManagerauthenticationManagerBean()throwsException{returnsuper.authenticationManagerBean();}}// JwtUtil.java@ComponentpublicclassJwtUtil{privatestaticfinalString SECRET ="your_jwt_secret";privatestaticfinallong EXPIRATION =86400L;// 24小时publicStringgenerateToken(UserDetails userDetails){Map<String,Object> claims =newHashMap<>(); claims.put("sub", userDetails.getUsername()); claims.put("created",newDate());returnJwts.builder().setClaims(claims).setExpiration(newDate(System.currentTimeMillis()+ EXPIRATION *1000)).signWith(SignatureAlgorithm.HS512, SECRET).compact();}publicStringgetUsernameFromToken(String token){returnJwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody().getSubject();}publicbooleanvalidateToken(String token,UserDetails userDetails){finalString username =getUsernameFromToken(token);return(username.equals(userDetails.getUsername())&&!isTokenExpired(token);}privatebooleanisTokenExpired(String token){finalDate expiration =getExpirationDateFromToken(token);return expiration.before(newDate());}privateDategetExpirationDateFromToken(String token){returnJwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody().getExpiration();}}4.2.2 虚拟卡管理
// CardServiceImpl.java@ServicepublicclassCardServiceImplimplementsCardService{@AutowiredprivateCardProductMapper cardProductMapper;@AutowiredprivateCardSecretMapper cardSecretMapper;@AutowiredprivateRedisUtil redisUtil;privatestaticfinalString CARD_PRODUCT_CACHE_KEY ="card:product:list";privatestaticfinallong CACHE_EXPIRE =3600;// 1小时@OverridepublicList<CardProductRes>listAllProducts(){// 先查缓存String cache = redisUtil.get(CARD_PRODUCT_CACHE_KEY);if(StringUtils.isNotBlank(cache)){return JSON.parseArray(cache,CardProductRes.class);}// 缓存没有则查数据库QueryWrapper<CardProduct> queryWrapper =newQueryWrapper<>(); 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@TransactionalpublicList<CardSecret>lockCardSecrets(Long productId,int quantity,Long orderId){// 查询可用的卡密List<CardSecret> availableSecrets = cardSecretMapper.selectAvailableSecrets(productId, quantity);if(availableSecrets.size()< quantity){thrownewBusinessException("库存不足");}// 锁定卡密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@TransactionalpublicvoidunlockCardSecrets(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);}privateCardProductResconvertToRes(CardProduct product){CardProductRes res =newCardProductRes();BeanUtils.copyProperties(product, res);return res;}}4.2.3 订单服务
// OrderServiceImpl.java@ServicepublicclassOrderServiceImplimplementsOrderService{@AutowiredprivateOrderMapper orderMapper;@AutowiredprivateOrderItemMapper orderItemMapper;@AutowiredprivateCardService cardService;@AutowiredprivatePaymentService paymentService;@AutowiredprivateSnowFlakeUtil snowFlakeUtil;@Override@TransactionalpublicOrderRescreateOrder(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(newBigDecimal(req.getQuantity()));// 创建订单Order order =newOrder(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setPaymentAmount(totalAmount); order.setStatus(OrderStatus.UNPAID.getCode()); orderMapper.insert(order);// 创建订单明细List<OrderItem> orderItems =newArrayList<>();for(CardSecret secret : cardSecrets){OrderItem item =newOrderItem(); 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);// 更新卡密的订单IDList<Long> cardSecretIds = cardSecrets.stream().map(CardSecret::getId).collect(Collectors.toList()); cardService.updateCardSecretsOrderId(cardSecretIds, order.getId());// 返回订单信息OrderRes res =newOrderRes();BeanUtils.copyProperties(order, res); res.setItems(orderItems.stream().map(this::convertToItemRes).collect(Collectors.toList()));return res;}@Override@TransactionalpublicvoidcancelOrder(Long orderId,Long userId){Order order = orderMapper.selectById(orderId);if(order ==null){thrownewBusinessException("订单不存在");}if(!order.getUserId().equals(userId)){thrownewBusinessException("无权操作此订单");}if(order.getStatus()!=OrderStatus.UNPAID.getCode()){thrownewBusinessException("订单状态不允许取消");}// 更新订单状态 order.setStatus(OrderStatus.CANCELLED.getCode()); orderMapper.updateById(order);// 查询订单明细获取卡密IDList<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@TransactionalpublicvoidpayOrderSuccess(String orderNo,String paymentNo,BigDecimal paymentAmount,Date paymentTime){Order order = orderMapper.selectByOrderNo(orderNo);if(order ==null){thrownewBusinessException("订单不存在");}if(order.getStatus()!=OrderStatus.UNPAID.getCode()){thrownewBusinessException("订单状态不正确");}// 更新订单状态 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 =newPayment(); 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(newDate()); paymentService.createPayment(payment);}privateStringgenerateOrderNo(){return"ORD"+ snowFlakeUtil.nextId();}privateOrderItemResconvertToItemRes(OrderItem item){OrderItemRes res =newOrderItemRes();BeanUtils.copyProperties(item, res);return res;}}4.2.4 微信支付集成
// WeChatPayUtil.java@ComponentpublicclassWeChatPayUtil{@Value("${wechat.pay.appid}")privateString appId;@Value("${wechat.pay.mchid}")privateString mchId;@Value("${wechat.pay.apikey}")privateString apiKey;@Value("${wechat.pay.serialNo}")privateString serialNo;@Value("${wechat.pay.privateKey}")privateString privateKey;@Value("${wechat.pay.notifyUrl}")privateString notifyUrl;privateCloseableHttpClient httpClient;@PostConstructpublicvoidinit(){// 加载商户私钥PrivateKey merchantPrivateKey =PemUtil.loadPrivateKey(newByteArrayInputStream(privateKey.getBytes()));// 构造HttpClient httpClient =WechatPayHttpClientBuilder.create().withMerchant(mchId, serialNo, merchantPrivateKey).withValidator(newWechatPay2Validator(apiKey.getBytes())).build();}publicMap<String,String>createH5Payment(String orderNo,BigDecimal amount,String description,String clientIp)throwsException{// 构造请求参数Map<String,Object> params =newHashMap<>(); 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",newHashMap<String,Object>(){{put("total", amount.multiply(newBigDecimal(100)).intValue());put("currency","CNY");}}); params.put("scene_info",newHashMap<String,Object>(){{put("payer_client_ip", clientIp);put("h5_info",newHashMap<String,Object>(){{put("type","Wap");}});}});// 发送请求HttpPost httpPost =newHttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/h5"); httpPost.addHeader("Accept","application/json"); httpPost.addHeader("Content-type","application/json"); httpPost.setEntity(newStringEntity(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 =newHashMap<>();JSONObject json = JSON.parseObject(responseBody); result.put("h5_url", json.getString("h5_url")); result.put("prepay_id", json.getString("prepay_id"));return result;}else{thrownewBusinessException("微信支付创建失败: "+ responseBody);}}finally{ response.close();}}publicbooleanverifyNotify(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){returnfalse;}// 验证订单状态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){returnfalse;}}privatebooleanverifySignature(byte[] message,String serial,byte[] signature){try{// 根据证书序列号查询证书String cert =getWechatPayCert(serial);if(cert ==null){returnfalse;}// 加载证书X509EncodedKeySpec publicKeySpec =newX509EncodedKeySpec(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){returnfalse;}}privateStringgetWechatPayCert(String serial){// 这里应该实现从微信支付平台获取证书的逻辑// 实际项目中应该缓存证书,避免频繁请求// 简化实现,返回配置的证书return"your_wechat_pay_cert_content";}}// PaymentController.java@RestController@RequestMapping("/api/payment")publicclassPaymentController{@AutowiredprivateOrderService orderService;@AutowiredprivatePaymentService paymentService;@AutowiredprivateWeChatPayUtil weChatPayUtil;@PostMapping("/create")publicApiResponse<Map<String,String>>createPayment(@RequestBodyPaymentReq req,HttpServletRequest request){// 查询订单Order order = orderService.getOrderByNo(req.getOrderNo());if(order ==null){returnApiResponse.fail("订单不存在");}if(order.getStatus()!=OrderStatus.UNPAID.getCode()){returnApiResponse.fail("订单状态不正确");}// 创建微信支付try{Map<String,String> result = weChatPayUtil.createH5Payment( order.getOrderNo(), order.getPaymentAmount(),"虚拟卡购买-"+ order.getOrderNo(),getClientIp(request));// 保存支付记录Payment payment =newPayment(); 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);returnApiResponse.success(result);}catch(Exception e){returnApiResponse.fail("支付创建失败: "+ e.getMessage());}}@PostMapping("/callback/wechat")publicStringwechatPayCallback(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(newBigDecimal(100));Date paymentTime =newDate(resource.getLong("success_time")*1000);// 处理支付成功逻辑 orderService.payOrderSuccess(orderNo, transactionId, amount, paymentTime);return"SUCCESS";}catch(Exception e){return"FAIL";}}privateStringgetClientIp(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> <header-component title="虚拟卡商城" :show-back="false" /> <div> <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> <header-component title="确认订单" :show-back="true" /> <div v-if="!isVirtual"> <van-contact-card type="edit" :name="address.name" :tel="address.phone" @click="editAddress" /> </div> <div> <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> <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)}`" /> </van-cell-group> </div> <div> <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> <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> <header-component title="支付订单" :show-back="true" /> <div> <van-cell-group> <van-cell title="订单编号" :value="orderNo" /> <van-cell title="支付金额"> <span>¥{{ amount.toFixed(2) }}</span> </van-cell> </van-cell-group> </div> <div> <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" /> </template> </van-cell> </van-cell-group> </van-radio-group> </div> <div> <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> <div v-if="paymentStatus === 'pending'"> <van-loading size="24px">正在调起支付...</van-loading> </div> <div v-else-if="paymentStatus === 'success'"> <van-icon name="checked" color="#07c160" size="50px" /> <p>支付成功</p> </div> <div v-else> <van-icon name="close" color="#ee0a24" size="50px" /> <p>支付失败</p> <p>{{ 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> <el-card> <el-form :inline="true" :model="searchForm"> <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> <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 @selection-change="handleSelectionChange" v-loading="loading" > <el-table-column type="selection" /> <el-table-column prop="id" label="ID" /> <el-table-column prop="name" label="产品名称" min-width="150" /> <el-table-column label="分类"> <template slot-scope="scope"> {{ getCategoryName(scope.row.categoryId) }} </template> </el-table-column> <el-table-column prop="price" label="价格"> <template slot-scope="scope"> ¥{{ scope.row.price.toFixed(2) }} </template> </el-table-column> <el-table-column prop="stock" label="库存" /> <el-table-column label="状态"> <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="创建时间" /> <el-table-column label="操作" 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" /> </el-card> <!-- 新增/编辑对话框 --> <el-dialog :title="dialogTitle" :visible.sync="dialogVisible"> <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 action="/api/upload" :show-file-list="false" :on-success="handleImageSuccess" :before-upload="beforeImageUpload" > <img v-if="dialogForm.imageUrl" :src="dialogForm.imageUrl" /> <i v-else></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></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"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="submitForm">确 定</el-button> </span> </el-dialog> </div> </template> <script> import { getCardProducts, addCardProduct, updateCardProduct, deleteCardProduct, updateCardProductStatus } from '@/api/card'; import { getCardCategories } from '@/api/category'; export default { data() { return { searchForm: { name: '', categoryId: null, status: null }, tableData: [], selectedItems: [], categories: [], loading: false, pagination: { current: 1, size: 10, total: 0 }, dialogVisible: false, dialogTitle: '新增产品', dialogForm: { id: null, name: '', categoryId: null, price: 0, originalPrice: 0, imageUrl: '', detailImages: [], description: '', sortOrder: 0, status: 1 }, detailImageList: [], rules: { name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }], categoryId: [{ required: true, message: '请选择产品分类', trigger: 'change' }], price: [{ required: true, message: '请输入产品价格', trigger: 'blur' }] } }; }, created() { this.loadCategories(); this.loadTableData(); }, methods: { async loadCategories() { try { const res = await getCardCategories(); this.categories = res.data; } catch (error) { console.error('加载分类失败', error); } }, async loadTableData() { this.loading = true; try { const params = { ...this.searchForm, page: this.pagination.current, pageSize: this.pagination.size }; const res = await getCardProducts(params); this.tableData = res.data.list; this.pagination.total = res.data.total; } catch (error) { console.error('加载产品列表失败', error); } finally { this.loading = false; } }, getCategoryName(categoryId) { const category = this.categories.find(item => item.id === categoryId); return category ? category.name : '--'; }, handleSearch() { this.pagination.current = 1; this.loadTableData(); }, resetSearch() { this.searchForm = { name: '', categoryId: null, status: null }; this.pagination.current = 1; this.loadTableData(); }, handleSelectionChange(val) { this.selectedItems = val; }, handleSizeChange(val) { this.pagination.size = val; this.loadTableData(); }, handleCurrentChange(val) { this.pagination.current = val; this.loadTableData(); }, handleAdd() { this.dialogTitle = '新增产品'; this.dialogForm = { id: null, name: '', categoryId: null, price: 0, originalPrice: 0, imageUrl: '', detailImages: [], description: '', sortOrder: 0, status: 1 }; this.detailImageList = []; this.dialogVisible = true; }, handleEdit(row) { this.dialogTitle = '编辑产品'; this.dialogForm = { ...row, detailImages: row.detailImages ? JSON.parse(row.detailImages) : [] }; this.detailImageList = this.dialogForm.detailImages.map(url => ({ url, name: url.substring(url.lastIndexOf('/') + 1) })); this.dialogVisible = true; }, async handleStatusChange(row) { try { await updateCardProductStatus(row.id, row.status ? 0 : 1); this.$message.success('状态更新成功'); this.loadTableData(); } catch (error) { this.$message.error('状态更新失败'); console.error(error); } }, handleBatchDelete() { this.$confirm('确定要删除选中的产品吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(async () => { try { const ids = this.selectedItems.map(item => item.id); await deleteCardProduct(ids); this.$message.success('删除成功'); this.loadTableData(); } catch (error) { this.$message.error('删除失败'); console.error(error); } }).catch(() => {}); }, handleImageSuccess(res, file) { this.dialogForm.imageUrl = res.data.url; }, handleDetailImageSuccess(res, file) { this.dialogForm.detailImages.push(res.data.url); }, handleDetailImageRemove(file, fileList) { const url = file.url || file.response.data.url; this.dialogForm.detailImages = this.dialogForm.detailImages.filter(item => item !== url); }, beforeImageUpload(file) { const isImage = file.type.startsWith('image/'); const isLt2M = file.size / 1024 / 1024 < 2; if (!isImage) { this.$message.error('只能上传图片!'); } if (!isLt2M) { this.$message.error('图片大小不能超过2MB!'); } return isImage && isLt2M; }, submitForm() { this.$refs.dialogForm.validate(async valid => { if (!valid) { return; } try { const formData = { ...this.dialogForm, detailImages: JSON.stringify(this.dialogForm.detailImages) }; if (this.dialogForm.id) { await updateCardProduct(formData); this.$message.success('更新成功'); } else { await addCardProduct(formData); this.$message.success('添加成功'); } this.dialogVisible = false; this.loadTableData(); } catch (error) { this.$message.error(error.message || '操作失败'); console.error(error); } }); } } }; </script> <style scoped> .search-card { margin-bottom: 20px; } .search-form { display: flex; flex-wrap: wrap; } .operation-card { margin-bottom: 20px; } .pagination { margin-top: 20px; text-align: right; } .avatar-uploader { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; width: 150px; height: 150px; } .avatar-uploader:hover { border-color: #409EFF; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 150px; height: 150px; line-height: 150px; text-align: center; } .avatar { width: 150px; height: 150px; display: block; } </style> 6. 微信H5支付集成
6.1 微信支付配置
- 申请微信支付商户号
- 登录微信支付商户平台(https://pay.weixin.qq.com)
- 完成商户号申请和资质认证
- 配置支付域名
- 在商户平台配置支付域名(需备案)
- 配置授权目录和回调域名
- 获取API密钥和证书
- 设置APIv2密钥(32位)
- 申请API证书(用于V3接口)
- 配置应用信息
- 在商户平台配置H5支付信息
- 设置支付场景和域名
6.2 支付流程实现
- 前端发起支付请求
// src/utils/wechat.jsimport axios from'axios';exportfunctionisWeixinBrowser(){return/micromessenger/i.test(navigator.userAgent);}exportasyncfunctionwechatPay(paymentData){returnnewPromise((resolve, reject)=>{if(typeof WeixinJSBridge ==='undefined'){if(document.addEventListener){ document.addEventListener('WeixinJSBridgeReady', onBridgeReady,false);}elseif(document.attachEvent){ document.attachEvent('WeixinJSBridgeReady', onBridgeReady); document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);}reject(newError('请在微信中打开页面'));}else{onBridgeReady();}functiononBridgeReady(){ 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(newError(res.err_msg ||'支付失败'));}});}});}- 后端处理支付回调
// PaymentController.java@RestController@RequestMapping("/api/payment")publicclassPaymentController{// ... 其他代码 ...@PostMapping("/callback/wechat")publicStringwechatPayCallback(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(newBigDecimal(100));Date paymentTime =newDate(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";}}}其他略…