Web虚拟卡销售店铺实现方案
1. 项目概述1.1 项目背景
随着数字经济的发展,虚拟卡(如礼物卡、会员卡、游戏点卡等)的市场需求日益增长。本项目旨在构建一个完整的Web虚拟卡销售平台,包含前端销售体系、后端管理体系和移动端H5支付功能,采用Java作为后端技术栈,Vue.js作为前端框架,并集成微信支付功能。
1.2 体系架构
体系采用前后端分离架构:
[*]前端:Vue.js + Element UI (管理端) + Vant (移动端)
[*]后端:Spring Boot + Spring Security + MyBatis Plus
[*]数据库:MySQL
[*]缓存:Redis
[*]支付:微信支付H5 API
2. 技术选型与环境搭建
2.1 后端技术栈
// pom.xml 主要依赖
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version>
</dependency>
<!-- 其他 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2 前端技术栈
# 管理端前端
vue create admin-frontend
cd admin-frontend
vue add element-ui
npm install axios vue-router vuex --save
# 用户端前端
vue create user-frontend
cd user-frontend
npm install vant axios vue-router vuex --save
2.3 开发环境配置
[*]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,
: Tab,
: Tabs,
: List,
: 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,
: Card,
: Tag,
: Cell,
: CellGroup,
: Radio,
: RadioGroup,
: Stepper,
: 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,
: CellGroup,
: Radio,
: RadioGroup,
: Button,
: Dialog,
: Loading,
: 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=""
:page-size="pagination.size"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
class="pagination"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="50%">
<el-form :model="dialogForm" :rules="rules" ref="dialogForm" label-width="100px">
<el-form-item label="产品名称" prop="name">
<el-input v-model="dialogForm.name" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="产品分类" prop="categoryId">
<el-select v-model="dialogForm.categoryId" placeholder="请选择分类">
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id"
/>
</el-select>
</el-form-item>
<el-form-item label="产品价格" prop="price">
<el-input-number v-model="dialogForm.price" :min="0" :precision="2" :step="0.1" />
</el-form-item>
<el-form-item label="原价" prop="originalPrice">
<el-input-number v-model="dialogForm.originalPrice" :min="0" :precision="2" :step="0.1" />
</el-form-item>
<el-form-item label="产品图片" prop="imageUrl">
<el-upload
class="avatar-uploader"
action="/api/upload"
:show-file-list="false"
:on-success="handleImageSuccess"
:before-upload="beforeImageUpload"
>
<img v-if="dialogForm.imageUrl" :src="dialogForm.imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item label="详情图片" prop="detailImages">
<el-upload
action="/api/upload"
list-type="picture-card"
:file-list="detailImageList"
:on-success="handleDetailImageSuccess"
:on-remove="handleDetailImageRemove"
:before-upload="beforeImageUpload"
multiple
>
<i class="el-icon-plus"></i>
</el-upload>
</el-form-item>
<el-form-item label="产品描述" prop="description">
<el-input
type="textarea"
:rows="4"
v-model="dialogForm.description"
placeholder="请输入产品描述"
/>
</el-form-item>
<el-form-item label="排序权重" prop="sortOrder">
<el-input-number v-model="dialogForm.sortOrder" :min="0" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="dialogForm.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { getCardProducts, addCardProduct, updateCardProduct, deleteCardProduct, updateCardProductStatus } from '@/api/card';
import { getCardCategories } from '@/api/category';
export default {
data() {
return {
searchForm: {
name: '',
categoryId: null,
status: null
},
tableData: [],
selectedItems: [],
categories: [],
loading: false,
pagination: {
current: 1,
size: 10,
total: 0
},
dialogVisible: false,
dialogTitle: '新增产品',
dialogForm: {
id: null,
name: '',
categoryId: null,
price: 0,
originalPrice: 0,
imageUrl: '',
detailImages: [],
description: '',
sortOrder: 0,
status: 1
},
detailImageList: [],
rules: {
name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
categoryId: [{ required: true, message: '请选择产品分类', trigger: 'change' }],
price: [{ required: true, message: '请输入产品价格', trigger: 'blur' }]
}
};
},
created() {
this.loadCategories();
this.loadTableData();
},
methods: {
async loadCategories() {
try {
const res = await getCardCategories();
this.categories = res.data;
} catch (error) {
console.error('加载分类失败', error);
}
},
async loadTableData() {
this.loading = true;
try {
const params = {
...this.searchForm,
page: this.pagination.current,
pageSize: this.pagination.size
};
const res = await getCardProducts(params);
this.tableData = res.data.list;
this.pagination.total = res.data.total;
} catch (error) {
console.error('加载产品列表失败', error);
} finally {
this.loading = false;
}
},
getCategoryName(categoryId) {
const category = this.categories.find(item => item.id === categoryId);
return category ? category.name : '--';
},
handleSearch() {
this.pagination.current = 1;
this.loadTableData();
},
resetSearch() {
this.searchForm = {
name: '',
categoryId: null,
status: null
};
this.pagination.current = 1;
this.loadTableData();
},
handleSelectionChange(val) {
this.selectedItems = val;
},
handleSizeChange(val) {
this.pagination.size = val;
this.loadTableData();
},
handleCurrentChange(val) {
this.pagination.current = val;
this.loadTableData();
},
handleAdd() {
this.dialogTitle = '新增产品';
this.dialogForm = {
id: null,
name: '',
categoryId: null,
price: 0,
originalPrice: 0,
imageUrl: '',
detailImages: [],
description: '',
sortOrder: 0,
status: 1
};
this.detailImageList = [];
this.dialogVisible = true;
},
handleEdit(row) {
this.dialogTitle = '编辑产品';
this.dialogForm = {
...row,
detailImages: row.detailImages ? JSON.parse(row.detailImages) : []
};
this.detailImageList = this.dialogForm.detailImages.map(url => ({
url,
name: url.substring(url.lastIndexOf('/') + 1)
}));
this.dialogVisible = true;
},
async handleStatusChange(row) {
try {
await updateCardProductStatus(row.id, row.status ? 0 : 1);
this.$message.success('状态更新成功');
this.loadTableData();
} catch (error) {
this.$message.error('状态更新失败');
console.error(error);
}
},
handleBatchDelete() {
this.$confirm('确定要删除选中的产品吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const ids = this.selectedItems.map(item => item.id);
await deleteCardProduct(ids);
this.$message.success('删除成功');
this.loadTableData();
} catch (error) {
this.$message.error('删除失败');
console.error(error);
}
}).catch(() => {});
},
handleImageSuccess(res, file) {
this.dialogForm.imageUrl = res.data.url;
},
handleDetailImageSuccess(res, file) {
this.dialogForm.detailImages.push(res.data.url);
},
handleDetailImageRemove(file, fileList) {
const url = file.url || file.response.data.url;
this.dialogForm.detailImages = this.dialogForm.detailImages.filter(item => item !== url);
},
beforeImageUpload(file) {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) {
this.$message.error('只能上传图片!');
}
if (!isLt2M) {
this.$message.error('图片大小不能超过2MB!');
}
return isImage && isLt2M;
},
submitForm() {
this.$refs.dialogForm.validate(async valid => {
if (!valid) {
return;
}
try {
const formData = {
...this.dialogForm,
detailImages: JSON.stringify(this.dialogForm.detailImages)
};
if (this.dialogForm.id) {
await updateCardProduct(formData);
this.$message.success('更新成功');
} else {
await addCardProduct(formData);
this.$message.success('添加成功');
}
this.dialogVisible = false;
this.loadTableData();
} catch (error) {
this.$message.error(error.message || '操作失败');
console.error(error);
}
});
}
}
};
</script>
<style scoped>
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
}
.operation-card {
margin-bottom: 20px;
}
.pagination {
margin-top: 20px;
text-align: right;
}
.avatar-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 150px;
height: 150px;
}
.avatar-uploader:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 150px;
height: 150px;
line-height: 150px;
text-align: center;
}
.avatar {
width: 150px;
height: 150px;
display: block;
}
</style>
6. 微信H5支付集成
6.1 微信支付配置
[*] 申请微信支付商户号
[*]登录微信支付商户平台(https://pay.weixin.qq.com)
[*]完成商户号申请和资质认证
[*] 配置支付域名
[*]在商户平台配置支付域名(需备案)
[*]配置授权目次和回调域名
[*] 获取API密钥和证书
[*]设置APIv2密钥(32位)
[*]申请API证书(用于V3接口)
[*] 配置应用信息
[*]在商户平台配置H5支付信息
[*]设置支付场景和域名
6.2 支付流程实现
[*]前端发起支付哀求
// src/utils/wechat.js
import axios from 'axios';
export function isWeixinBrowser() {
return /micromessenger/i.test(navigator.userAgent);
}
export async function wechatPay(paymentData) {
return new Promise((resolve, reject) => {
if (typeof WeixinJSBridge === 'undefined') {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
reject(new Error('请在微信中打开页面'));
} else {
onBridgeReady();
}
function onBridgeReady() {
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
{
appId: paymentData.appId,
timeStamp: paymentData.timeStamp,
nonceStr: paymentData.nonceStr,
package: paymentData.package,
signType: paymentData.signType,
paySign: paymentData.paySign
},
function(res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
resolve();
} else {
reject(new Error(res.err_msg || '支付失败'));
}
}
);
}
});
}
[*]后端处理支付回调
// PaymentController.java
@RestController
@RequestMapping("/api/payment")
public class PaymentController {
// ... 其他代码 ...
@PostMapping("/callback/wechat")
public String wechatPayCallback(HttpServletRequest request) {
try {
// 获取请求头信息
String signature = request.getHeader("Wechatpay-Signature");
String serial = request.getHeader("Wechatpay-Serial");
String nonce = request.getHeader("Wechatpay-Nonce");
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 获取请求体
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
// 验证回调
if (!weChatPayUtil.verifyNotify(null, signature, serial, nonce, timestamp, body)) {
log.error("微信支付回调验证失败");
return "FAIL";
}
// 解析回调内容
JSONObject json = JSON.parseObject(body);
JSONObject resource = json.getJSONObject("resource");
String orderNo = resource.getString("out_trade_no");
String transactionId = resource.getString("transaction_id");
BigDecimal amount = resource.getJSONObject("amount")
.getBigDecimal("total")
.divide(new BigDecimal(100));
Date paymentTime = new Date(resource.getLong("success_time") * 1000);
// 处理支付成功逻辑
orderService.payOrderSuccess(orderNo, transactionId, amount, paymentTime);
log.info("微信支付回调处理成功, orderNo: {}", orderNo);
return "SUCCESS";
} catch (Exception e) {
log.error("微信支付回调处理失败", e);
return "FAIL";
}
}
}
其他略…
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]