5 次代碼提交 1329883d91 ... c2cdabc6e9

作者 SHA1 備註 提交日期
  李思佳 c2cdabc6e9 Merge branch 'lisijia' into test_dev 3 周之前
  李思佳 0d96be306d feat(part-time-matchmaker): 移除可服务时间必填验证 3 周之前
  mazhenhang 0216bea611 Merge branch 'mzh' into test_dev 3 周之前
  wangwenju 751969ee94 Merge remote-tracking branch 'origin/test_dev' into test_dev 3 周之前
  wangwenju 54db759082 活动支付 3 周之前

+ 116 - 21
LiangZhiYUMao/pages/activities/detail.vue

@@ -85,6 +85,7 @@
 	export default {
 		data() {
 			return {
+				gatewayURL: 'http://localhost:8083',
 				activityId: null,
 				activity: {
 					name: '加载中...',
@@ -226,34 +227,128 @@
 				
 				uni.showModal({
 					title: '确认报名',
-					content: `确认报名参加"${this.activity.name}"吗?`,
-					success: async (res) => {
+					content: `确认报名参加"${this.activity.name}"吗?${this.activity.price > 0 ? `\n费用:¥${this.activity.price}` : ''}`,
+					success: (res) => {
 						if (res.confirm) {
-							try {
-								await api.activity.register(this.activityId, userId)
-								uni.showModal({
-									title: '报名成功',
-									content: '恭喜您报名成功!',
-									showCancel: false,
-									success: () => {
-										this.loadActivityDetail()
-									}
-								})
-							} catch (error) {
-								console.error('报名失败:', error)
-								// 后端会返回友好的错误信息(通过Result类的message字段)
-								const errorMsg = error.message || '报名失败,请稍后重试'
-								uni.showModal({
-									title: '提示',
-									content: errorMsg,
-									showCancel: false
-								})
+							// 如果活动价格大于0,需要进行支付
+							if (this.activity.price > 0) {
+								this.requestPay(userId, this.activity)
+							} else {
+								// 免费活动直接报名
+								this.freeRegister(userId)
 							}
 						}
 					}
 				})
 			},
 
+			// 免费活动报名
+			async freeRegister(userId) {
+				try {
+					await api.activity.register(this.activityId, userId)
+					uni.showModal({
+						title: '报名成功',
+						content: '恭喜您报名成功!',
+						showCancel: false,
+						success: () => {
+							this.loadActivityDetail()
+						}
+					})
+				} catch (error) {
+					console.error('报名失败:', error)
+					const errorMsg = error.message || '报名失败,请稍后重试'
+					uni.showModal({
+						title: '提示',
+						content: errorMsg,
+						showCancel: false
+					})
+				}
+			},
+
+			/**
+			 * 请求支付参数并调起微信支付
+			 */
+			requestPay(userId, activity) {
+				uni.showLoading({
+					title: '处理中...'
+				})
+				
+				uni.request({
+					url: this.gatewayURL + '/api/activity-order/create',
+					method: 'POST',
+					data: {
+						userId: userId,
+						activityId: this.activityId,
+						activityName: activity.name,
+						price: activity.price
+					},
+					header: {
+						'content-type': 'application/json',
+						'token': uni.getStorageSync('token') // 携带登录态
+					},
+					success: (res) => {
+						uni.hideLoading()
+						
+						if (res.data && res.data.code === 200) {
+							const payParams = res.data.ext || res.data.data // 后端返回的支付参数
+							this.callWxPay(payParams, activity)
+						} else {
+							uni.showToast({
+								title: res.data?.message || '获取支付参数失败',
+								icon: 'none'
+							})
+						}
+					},
+					fail: (err) => {
+						uni.hideLoading()
+						console.error('请求支付参数失败:', err)
+						uni.showToast({
+							title: '网络请求失败',
+							icon: 'none'
+						})
+					}
+				})
+			},
+
+			/**
+			 * 调起微信支付组件
+			 */
+			callWxPay(payParams, activity) {
+				uni.requestPayment({
+					provider: 'wxpay',
+					timeStamp: payParams.timeStamp,
+					nonceStr: payParams.nonceStr,
+					package: payParams.package,
+					signType: payParams.signType,
+					paySign: payParams.paySign,
+					success: (payResult) => {
+						if (payResult.errMsg === 'requestPayment:ok') {
+							// 支付成功后处理
+							uni.showModal({
+								title: '支付成功',
+								content: '恭喜您报名成功!',
+								showCancel: false,
+								success: () => {
+									this.loadActivityDetail()
+									// 可以跳转到我的活动列表或其他页面
+									// uni.navigateTo({ url: '/pages/mine/my-activities' })
+								}
+							})
+						}
+					},
+					fail: (err) => {
+						console.error('支付失败:', err)
+						if (err.errMsg !== 'requestPayment:fail cancel') {
+							uni.showModal({
+								title: '支付失败',
+								content: '请稍后重试',
+								showCancel: false
+							})
+						}
+					}
+				})
+			},
+
 			// 返回
 			goBack() {
 				uni.navigateBack()

+ 9 - 9
LiangZhiYUMao/pages/part-time-matchmaker/index.vue

@@ -102,7 +102,7 @@
 					<input class="form-input" v-model="formData.experience" placeholder="请输入您的婚姻介绍经验" />
 				</view>
 
-				<view class="form-group">
+				<!-- <view class="form-group">
 					<text class="form-label">可服务时间:</text>
 					<picker mode="multiSelector" :range="timeSlotData" :value="timeSlotIndex" @change="onTimeSlotChange">
 						<view class="form-input picker-input">
@@ -110,7 +110,7 @@
 							<text class="picker-arrow">></text>
 						</view>
 					</picker>
-				</view>
+				</view> -->
 
 				<view class="form-group">
 					<text class="form-label">个人简介:</text>
@@ -547,13 +547,13 @@
 					return
 				}
 				
-				if (!this.formData.startTime || !this.formData.endTime) {
-					uni.showToast({
-						title: '请选择可服务时间',
-						icon: 'none'
-					})
-					return
-				}
+				// if (!this.formData.startTime || !this.formData.endTime) {
+				// 	uni.showToast({
+				// 		title: '请选择可服务时间',
+				// 		icon: 'none'
+				// 	})
+				// 	return
+				// }
 				
 				if (!this.formData.introduction) {
 					uni.showToast({

+ 12 - 0
LiangZhiYUMao/utils/api.js

@@ -251,6 +251,18 @@ export default {
       url: '/activity/my',
       method: 'GET',
       data: params 
+    }),
+    
+    // 创建活动订单并获取支付参数
+    createOrder: (userId, activityId, activityName, price) => request({
+      url: '/activity-order/create',
+      method: 'POST',
+      data: {
+        userId,
+        activityId,
+        activityName,
+        price
+      }
     })
   },
 

+ 1 - 1
gateway/src/main/resources/application.yml

@@ -133,7 +133,7 @@ spring:
         - id: essential-route
           uri: http://localhost:1005
           predicates:
-            - Path=/api/user/**, /api/checkin/**, /api/vip/**
+            - Path=/api/user/**, /api/checkin/**, /api/vip/**, /api/activity-order/**
           filters:
             - StripPrefix=0
         

+ 40 - 0
service/Essential/src/main/java/com/zhentao/controller/ActivityOrderController.java

@@ -0,0 +1,40 @@
+package com.zhentao.controller;
+
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.zhentao.common.Result;
+import com.zhentao.dto.ActivityCreateOrderDto;
+import com.zhentao.service.ActivityOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 活动订单控制器
+ */
+@RestController
+@RequestMapping("/api/activity-order")
+public class ActivityOrderController {
+
+    @Autowired
+    private ActivityOrderService activityOrderService;
+
+    @PostMapping("/create")
+    public Result createOrder(@RequestBody ActivityCreateOrderDto dto) throws WxPayException {
+        return Result.success(activityOrderService.createOrderAndGetPayParams(dto.getUserId(), dto.getActivityId(), dto.getActivityName(), dto.getPrice()));
+    }
+
+    @PostMapping("/notify")
+    public String handlePayNotify(@RequestBody String notifyData) throws WxPayException {
+        return activityOrderService.handlePayNotify(notifyData);
+    }
+
+    @GetMapping("/status/{orderNo}")
+    public Map<String, Object> getOrderStatus(@PathVariable String orderNo) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("orderNo", orderNo);
+        result.put("status", activityOrderService.getOrderByOrderNo(orderNo).getStatus());
+        return result;
+    }
+}

+ 12 - 0
service/Essential/src/main/java/com/zhentao/dto/ActivityCreateOrderDto.java

@@ -0,0 +1,12 @@
+package com.zhentao.dto;
+
+
+import lombok.Data;
+
+@Data
+public class ActivityCreateOrderDto {
+    private Long userId;
+    private Integer activityId;
+    private String activityName;
+    private Double price;
+}

+ 45 - 0
service/Essential/src/main/java/com/zhentao/entity/ActivityOrder.java

@@ -0,0 +1,45 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 活动订单实体
+ */
+@Data
+@TableName("activity_order")
+public class ActivityOrder implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String orderNo; // 订单号
+
+    private Long userId; // 用户ID
+
+    private Integer activityId; // 活动ID
+
+    private String activityName; // 活动名称
+
+    private BigDecimal paymentAmount; // 支付金额
+
+    private String paymentMethod; // 支付方式
+
+    private Integer status; // 订单状态:0-待支付,1-已支付,2-已取消
+
+    private String transactionId; // 微信支付订单号
+
+    private LocalDateTime createTime; // 创建时间
+
+    private LocalDateTime paymentTime; // 支付时间
+
+    private LocalDateTime updateTime; // 更新时间
+}

+ 19 - 0
service/Essential/src/main/java/com/zhentao/mapper/ActivityOrderMapper.java

@@ -0,0 +1,19 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.ActivityOrder;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 活动订单Mapper
+ */
+@Mapper
+public interface ActivityOrderMapper extends BaseMapper<ActivityOrder> {
+
+    /**
+     * 根据订单号查询订单
+     * @param orderNo 订单号
+     * @return 活动订单
+     */
+    ActivityOrder selectByOrderNo(String orderNo);
+}

+ 39 - 0
service/Essential/src/main/java/com/zhentao/service/ActivityOrderService.java

@@ -0,0 +1,39 @@
+package com.zhentao.service;
+
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.zhentao.entity.ActivityOrder;
+
+import java.util.Map;
+
+/**
+ * 活动订单服务接口
+ */
+public interface ActivityOrderService {
+    
+    /**
+     * 创建活动订单并获取支付参数
+     * 
+     * @param userId 用户ID
+     * @param activityId 活动ID
+     * @param activityName 活动名称
+     * @param price 活动价格
+     * @return 微信支付参数
+     */
+    Map<String, Object> createOrderAndGetPayParams(Long userId, Integer activityId, String activityName, Double price) throws WxPayException;
+    
+    /**
+     * 处理微信支付回调
+     * 
+     * @param notifyData 回调数据
+     * @return 处理结果
+     */
+    String handlePayNotify(String notifyData) throws WxPayException;
+    
+    /**
+     * 根据订单号查询订单
+     * 
+     * @param orderNo 订单号
+     * @return 活动订单
+     */
+    ActivityOrder getOrderByOrderNo(String orderNo);
+}

+ 195 - 0
service/Essential/src/main/java/com/zhentao/service/impl/ActivityOrderServiceImpl.java

@@ -0,0 +1,195 @@
+package com.zhentao.service.impl;
+
+import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import com.github.binarywang.wxpay.util.SignUtils;
+import com.zhentao.entity.ActivityOrder;
+import com.zhentao.entity.ActivityRegistration;
+import com.zhentao.entity.Wx;
+import com.zhentao.pojo.Users;
+import com.zhentao.mapper.ActivityOrderMapper;
+import com.zhentao.mapper.ActivityRegistrationMapper;
+import com.zhentao.service.ActivityOrderService;
+import com.zhentao.service.UsersService;
+import com.zhentao.service.WxService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.RandomUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 活动订单服务实现
+ */
+@Slf4j
+@Service
+public class ActivityOrderServiceImpl implements ActivityOrderService {
+
+    @Autowired
+    private ActivityOrderMapper activityOrderMapper;
+
+    @Autowired
+    private ActivityRegistrationMapper activityRegistrationMapper;
+
+    @Autowired
+    private UsersService usersService;
+
+    @Autowired
+    private WxService wxService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> createOrderAndGetPayParams(Long userId, Integer activityId, String activityName, Double price) throws WxPayException {
+        // 1. 生成订单号
+        String orderNo = generateOrderNo();
+        LocalDateTime now = LocalDateTime.now();
+
+        // 2. 创建待支付订单
+        ActivityOrder order = new ActivityOrder();
+        order.setUserId(userId);
+        order.setActivityId(activityId);
+        order.setActivityName(activityName);
+        order.setOrderNo(orderNo);
+        order.setPaymentAmount(BigDecimal.valueOf(price));
+        order.setPaymentMethod("微信支付");
+        order.setStatus(0); // 待支付
+        order.setCreateTime(now);
+        activityOrderMapper.insert(order);
+
+        // 3. 构建微信支付请求
+        WxPayUnifiedOrderRequest payRequest = buildWxPayRequest(orderNo, activityName, price, userId);
+        WxPayUnifiedOrderResult wxPayResult = createWxPayOrder(payRequest);
+
+        // 4. 生成前端调起支付的参数
+        Map<String, Object> payParams = generatePayParams(wxPayResult, price);
+        payParams.put("orderNo", orderNo);
+        return payParams;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String handlePayNotify(String notifyData) throws WxPayException {
+        // 1. 解析回调数据并验证签名
+        WxPayConfig payConfig = getWxPayConfig();
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+
+        WxPayOrderNotifyResult notifyResult = wxPayService.parseOrderNotifyResult(notifyData);
+        String orderNo = notifyResult.getOutTradeNo();
+        String transactionId = notifyResult.getTransactionId();
+
+        // 2. 查询待支付订单
+        ActivityOrder order = activityOrderMapper.selectByOrderNo(orderNo);
+        if (order == null || order.getStatus() != 0) {
+            log.warn("活动订单不存在或已处理:{}", orderNo);
+            return WxPayNotifyResponse.fail("订单不存在或已处理");
+        }
+
+        // 3. 验证支付金额
+        int totalFee = order.getPaymentAmount().multiply(new BigDecimal("100")).intValue();
+        if (Integer.parseInt(String.valueOf(notifyResult.getTotalFee())) != totalFee) {
+            log.warn("活动订单金额不一致:{},实际支付:{}", orderNo, notifyResult.getTotalFee());
+            return WxPayNotifyResponse.fail("金额不一致");
+        }
+
+        // 4. 更新订单状态为已支付
+        order.setStatus(1);
+        order.setPaymentTime(LocalDateTime.now());
+        order.setTransactionId(transactionId);
+        order.setUpdateTime(LocalDateTime.now());
+        activityOrderMapper.updateById(order);
+
+        // 5. 创建活动报名记录
+        ActivityRegistration registration = new ActivityRegistration();
+        registration.setActivityId(order.getActivityId());
+        registration.setUserId(order.getUserId().intValue());
+        registration.setRegistrationTime(LocalDateTime.now());
+        registration.setStatus(1); // 已报名
+        registration.setCreatedTime(LocalDateTime.now());
+        registration.setUpdatedTime(LocalDateTime.now());
+        activityRegistrationMapper.insert(registration);
+
+        log.info("活动报名成功:用户{},活动{},订单{}", order.getUserId(), order.getActivityId(), orderNo);
+        return WxPayNotifyResponse.success("处理成功");
+    }
+
+    @Override
+    public ActivityOrder getOrderByOrderNo(String orderNo) {
+        return activityOrderMapper.selectByOrderNo(orderNo);
+    }
+
+    private String generateOrderNo() {
+        return "ACTIVITY" + System.currentTimeMillis() + RandomUtils.nextInt(1000, 9999);
+    }
+
+    private WxPayUnifiedOrderRequest buildWxPayRequest(String orderNo, String activityName, Double price, Long userId) {
+        Users user = usersService.getById(userId);
+        if (user == null || user.getWechatOpenid() == null) {
+            throw new RuntimeException("用户微信信息未绑定");
+        }
+
+        WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
+        request.setOutTradeNo(orderNo);
+        request.setBody("活动报名-" + activityName);
+        request.setTotalFee(BigDecimal.valueOf(price).multiply(new BigDecimal("100")).intValue());
+        request.setSpbillCreateIp("127.0.0.1");
+        request.setNotifyUrl("https://mini.workervip.com/api/activity-order/notify");
+        request.setTradeType("JSAPI");
+        request.setOpenid(user.getWechatOpenid());
+        return request;
+    }
+
+    private WxPayUnifiedOrderResult createWxPayOrder(WxPayUnifiedOrderRequest request) throws WxPayException {
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(getWxPayConfig());
+        return wxPayService.unifiedOrder(request);
+    }
+
+    private Map<String, Object> generatePayParams(WxPayUnifiedOrderResult wxPayResult, Double price) {
+        Wx wxConfig = wxService.list().get(0);
+        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
+        String nonceStr = RandomStringUtils.randomAlphanumeric(32);
+        String prepayId = wxPayResult.getPrepayId();
+
+        Map<String, String> signParams = new HashMap<>();
+        signParams.put("appId", wxConfig.getAppId());
+        signParams.put("timeStamp", timeStamp);
+        signParams.put("nonceStr", nonceStr);
+        signParams.put("package", "prepay_id=" + prepayId);
+        signParams.put("signType", "MD5");
+
+        String paySign = SignUtils.createSign(signParams, "7f633cbabd894b4d213bc6edffe3b119");
+
+        Map<String, Object> payParams = new HashMap<>();
+        payParams.put("appId", wxConfig.getAppId());
+        payParams.put("timeStamp", timeStamp);
+        payParams.put("nonceStr", nonceStr);
+        payParams.put("package", "prepay_id=" + prepayId);
+        payParams.put("signType", "MD5");
+        payParams.put("paySign", paySign);
+        payParams.put("totalFee", BigDecimal.valueOf(price).multiply(new BigDecimal("100")).intValue());
+        return payParams;
+    }
+
+    private WxPayConfig getWxPayConfig() {
+        Wx wxConfig = wxService.list().get(0);
+        WxPayConfig payConfig = new WxPayConfig();
+        payConfig.setAppId(wxConfig.getAppId());
+        payConfig.setMchId(wxConfig.getMchId());
+        payConfig.setMchKey("7f633cbabd894b4d213bc6edffe3b119");
+        payConfig.setNotifyUrl("https://mini.workervip.com/api/activity-order/notify");
+        return payConfig;
+    }
+}

+ 2 - 1
service/homePage/pom.xml

@@ -1,4 +1,4 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
@@ -95,5 +95,6 @@
             <artifactId>jackson-annotations</artifactId>
             <version>${jackson.version}</version>
         </dependency>
+
     </dependencies>
 </project>

+ 3 - 3
service/homePage/src/main/java/com/zhentao/controller/MatchmakerApplyController.java

@@ -83,9 +83,9 @@ public class MatchmakerApplyController {
                 return Result.error("婚姻介绍经验不能为空");
             }
             
-            if (matchmakerApply.getServerTime() == null || matchmakerApply.getServerTime().trim().isEmpty()) {
-                return Result.error("可服务时间不能为空");
-            }
+//            if (matchmakerApply.getServerTime() == null || matchmakerApply.getServerTime().trim().isEmpty()) {
+//                return Result.error("可服务时间不能为空");
+//            }
             
             if (matchmakerApply.getIntroduction() == null || matchmakerApply.getIntroduction().trim().isEmpty()) {
                 return Result.error("个人简介不能为空");