wlzboy
2025-12-03 656d6f8029f8bf9b2daa9dcc89101a879a70b860
feat:优先添加执行人
1个文件已添加
11个文件已修改
1417 ■■■■■ 已修改文件
app/pagesTask/components/StaffSelector.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/create-emergency.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/edit-emergency.vue 392 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/application/service/PaymentNotifyService.java 274 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/application/service/PaymentService.java 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/AlipayConfig.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/ThirdPartyConfig.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/WechatPayConfig.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/interfaces/controller/PaymentController.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/interfaces/controller/PaymentNotifyController.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskPaymentServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/components/StaffSelector.vue
@@ -6,11 +6,6 @@
        <view class="staff-item" v-for="(staff, index) in selectedStaff" :key="staff.userId">
          <view class="staff-info">
            <text class="staff-name">{{ staff.nickName }}</text>
            <view class="staff-roles">
              <text class="staff-role" v-for="(roleType, idx) in staff.types" :key="idx">
                {{ getUserTypeName(roleType) }}
              </text>
            </view>
          </view>
          <uni-icons 
            v-if="canRemove(index)"
@@ -56,11 +51,6 @@
        <view class="staff-filter">
          <view 
            class="filter-item" 
            :class="{ active: staffFilterType === 'all' }"
            @click="filterStaff('all')"
          >全部</view>
          <view
            class="filter-item"
            :class="{ active: staffFilterType === 'driver' }"
            @click="filterStaff('driver')"
          >司机</view>
@@ -90,16 +80,6 @@
              </view>
              <view class="staff-detail-row">
                <text class="staff-dept">{{ staff.deptName }}</text>
                <view class="staff-types">
                  <text
                    class="type-tag"
                    :class="'type-' + type"
                    v-for="(type, idx) in staff.types"
                    :key="idx"
                  >
                    {{ getUserTypeName(type) }}
                  </text>
                </view>
              </view>
            </view>
            <uni-icons 
@@ -169,7 +149,7 @@
      allStaffList: [],
      filteredStaffList: [],
      staffSearchKeyword: '',
      staffFilterType: 'all'
      staffFilterType: 'driver' // 默认选中司机
    }
  },
  computed: {
@@ -304,7 +284,7 @@
    closeStaffSelector() {
      this.$refs.staffPopup.close()
      this.staffSearchKeyword = ''
      this.staffFilterType = 'all'
      this.staffFilterType = 'driver' // 重置为默认司机
    },
    
    // 人员搜索
@@ -323,11 +303,6 @@
    filterStaffList() {
      let list = [...this.allStaffList]
      
      // 按类型过滤(支持多类型)
      if (this.staffFilterType !== 'all') {
        list = list.filter(staff => staff.types.includes(this.staffFilterType))
      }
      // 按关键词搜索
      if (this.staffSearchKeyword && this.staffSearchKeyword.trim() !== '') {
        const keyword = this.staffSearchKeyword.trim().toLowerCase()
@@ -336,6 +311,16 @@
                 (staff.phonenumber && staff.phonenumber.includes(keyword))
        })
      }
      // 根据选中的类型,将有该身份的人排在前面
      list.sort((a, b) => {
        const aHasType = a.types.includes(this.staffFilterType)
        const bHasType = b.types.includes(this.staffFilterType)
        if (aHasType && !bHasType) return -1  // a有该身份,排前面
        if (!aHasType && bHasType) return 1   // b有该身份,排前面
        return 0  // 都有或都没有,保持原顺序
      })
      
      this.filteredStaffList = list
    },
@@ -354,7 +339,7 @@
        this.selectedStaff.splice(index, 1)
      } else {
        // 未选中,添加
        this.selectedStaff.push(staff)
        this.selectedStaff.push({ ...staff })
      }
    },
    
app/pagesTask/create-emergency.vue
@@ -1690,172 +1690,6 @@
  }
}
// 人员选择弹窗样式
.staff-selector-popup {
  background-color: white;
  border-radius: 20rpx 20rpx 0 0;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  .popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 30rpx;
    border-bottom: 1rpx solid #f0f0f0;
    flex-shrink: 0;
    .popup-title {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
    }
    .popup-close {
      padding: 10rpx;
    }
  }
  .search-box {
    display: flex;
    align-items: center;
    margin: 20rpx 30rpx;
    padding: 15rpx 20rpx;
    background-color: #f5f5f5;
    border-radius: 10rpx;
    flex-shrink: 0;
    .search-input {
      flex: 1;
      margin-left: 10rpx;
      font-size: 28rpx;
    }
  }
  .staff-filter {
    display: flex;
    padding: 0 30rpx 20rpx;
    gap: 20rpx;
    flex-shrink: 0;
    .filter-item {
      flex: 1;
      text-align: center;
      padding: 15rpx 0;
      background-color: #f5f5f5;
      border-radius: 10rpx;
      font-size: 28rpx;
      color: #666;
      &.active {
        background-color: #007AFF;
        color: white;
      }
    }
  }
  .staff-list-popup {
    flex: 1;
    overflow-y: auto;
    padding: 0 30rpx;
    .staff-item-popup {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 25rpx 20rpx;
      border-bottom: 1rpx solid #f0f0f0;
      &:active {
        background-color: #f5f5f5;
      }
      .staff-info {
        flex: 1;
        .staff-name-row {
          display: flex;
          align-items: center;
          margin-bottom: 10rpx;
          .staff-name {
            font-size: 30rpx;
            font-weight: bold;
            color: #333;
            margin-right: 20rpx;
          }
          .staff-phone {
            font-size: 24rpx;
            color: #999;
          }
        }
        .staff-detail-row {
          display: flex;
          align-items: center;
          .staff-dept {
            font-size: 24rpx;
            color: #666;
            margin-right: 20rpx;
          }
          .staff-post {
            font-size: 24rpx;
            color: #007AFF;
          }
        }
      }
      .checkbox-empty {
        width: 40rpx;
        height: 40rpx;
        border: 2rpx solid #ddd;
        border-radius: 50%;
      }
    }
    .no-data {
      text-align: center;
      padding: 100rpx 0;
      color: #999;
      text {
        display: block;
        margin-top: 20rpx;
        font-size: 28rpx;
      }
    }
  }
  .popup-footer {
    display: flex;
    padding: 20rpx 30rpx;
    border-top: 1rpx solid #f0f0f0;
    gap: 20rpx;
    flex-shrink: 0;
    button {
      flex: 1;
      height: 80rpx;
      border-radius: 10rpx;
      font-size: 30rpx;
    }
    .cancel-btn {
      background-color: #f5f5f5;
      color: #666;
    }
    .confirm-btn {
      background-color: #007AFF;
      color: white;
    }
  }
}
// 智能识别弹窗样式
.smart-parse-popup {
  background-color: white;
app/pagesTask/edit-emergency.vue
@@ -47,27 +47,14 @@
        />
      </view>
      
      <view class="form-item">
        <view class="form-label">执行任务人员</view>
        <view class="staff-list">
          <view class="staff-item" v-for="(staff, index) in selectedStaff" :key="staff.userId">
            <view class="staff-info">
              <text class="staff-name">{{ staff.nickName }}</text>
              <text class="staff-role">({{ getUserTypeName(staff.type) || '未知职位' }})</text>
            </view>
            <uni-icons
              type="closeempty"
              size="20"
              color="#ff4d4f"
              @click="removeStaff(index)"
            ></uni-icons>
          </view>
          <view class="add-staff" @click="showStaffSelector">
            <uni-icons type="plusempty" size="20" color="#007AFF"></uni-icons>
            <text>添加人员</text>
          </view>
        </view>
      </view>
      <StaffSelector
        v-model="selectedStaff"
        label="执行任务人员"
        :required="false"
        :auto-add-current-user="false"
        :current-user-removable="true"
        @change="onStaffChange"
      />
      
      <view class="form-section-title">患者信息</view>
      <view class="form-item">
@@ -198,80 +185,6 @@
        ></map-selector>
      </view>
    </uni-popup>
    <!-- 人员选择器弹窗 -->
    <uni-popup ref="staffPopup" type="bottom" :mask-click="false">
      <view class="staff-popup-container">
        <view class="popup-header">
          <view class="popup-title">选择执行人员</view>
          <view class="close-btn" @click="closeStaffSelector">
            <uni-icons type="closeempty" size="20" color="#999"></uni-icons>
          </view>
        </view>
        <view class="search-bar">
          <input
            class="search-input"
            placeholder="搜索姓名或电话"
            v-model="staffSearchKeyword"
            @input="onStaffSearch"
          />
        </view>
        <view class="filter-tabs">
          <view
            class="filter-tab"
            :class="{ active: staffFilterType === 'driver' }"
            @click="filterStaff('driver')"
          >
            司机
          </view>
          <view
            class="filter-tab"
            :class="{ active: staffFilterType === 'doctor' }"
            @click="filterStaff('doctor')"
          >
            医生
          </view>
          <view
            class="filter-tab"
            :class="{ active: staffFilterType === 'nurse' }"
            @click="filterStaff('nurse')"
          >
            护士
          </view>
        </view>
        <scroll-view class="staff-list-scroll" scroll-y="true">
          <view
            class="staff-list-item"
            v-for="staff in filteredStaffList"
            :key="staff.userId"
            @click="toggleStaffSelection(staff)"
          >
            <view class="staff-item-info">
              <text class="staff-item-name">{{ staff.nickName }}</text>
              <text class="staff-item-dept">{{ staff.deptName }}</text>
            </view>
            <view class="staff-item-check">
              <uni-icons
                v-if="isStaffSelected(staff.userId)"
                type="checkmarkempty"
                size="24"
                color="#007AFF"
              ></uni-icons>
            </view>
          </view>
          <view v-if="filteredStaffList.length === 0" class="empty-tip">
            暂无人员数据
          </view>
        </scroll-view>
        <view class="popup-footer">
          <button class="confirm-btn" @click="confirmStaffSelection">确定(已选{{ selectedStaff.length }})</button>
        </view>
      </view>
    </uni-popup>
  </scroll-view>
</template>
@@ -280,7 +193,6 @@
import uniDatetimePicker from '@/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue'
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
import { getTask, updateTask } from "@/api/task"
import { listBranchUsers } from "@/api/system/user"
import { baiduDistanceByAddress } from "@/api/map"
import { calculateTransferPrice } from "@/api/price"
import MapSelector from './components/map-selector.vue'
@@ -289,6 +201,7 @@
import HospitalSelector from './components/HospitalSelector.vue'
import DiseaseSelector from './components/DiseaseSelector.vue'
import DepartureSelector from './components/DepartureSelector.vue'
import StaffSelector from './components/StaffSelector.vue'
import distanceCalculator from '@/mixins/distanceCalculator.js'
export default {
@@ -300,7 +213,8 @@
    OrganizationSelector,
    HospitalSelector,
    DiseaseSelector,
    DepartureSelector
    DepartureSelector,
    StaffSelector
  },
  mixins: [distanceCalculator],
  data() {
@@ -325,10 +239,6 @@
      departureLatitude: null,
      selectedDiseases: [], // 已选择的病情列表(确保初始化为空数组)
      selectedStaff: [], // 已选择的人员列表(确保初始化为空数组)
      allStaffList: [], // 所有人员列表
      filteredStaffList: [], // 过滤后的人员列表
      staffSearchKeyword: '', // 人员搜索关键词
      staffFilterType: 'driver', // 人员筛选类型
      taskForm: {
        transferTime: '',
        patient: {
@@ -375,7 +285,6 @@
    if (options.id) {
      this.taskId = options.id
      this.loadTaskDetail()
      this.loadDeptStaff() // 加载人员列表
    } else {
      this.$modal.showToast('任务ID不能为空')
      setTimeout(() => {
@@ -627,154 +536,10 @@
      // 组件已经通过 v-model 更新了 selectedDiseases
    },
    
    // 加载当前用户所在分公司的所有人员
    loadDeptStaff() {
      console.log('开始加载人员列表')
      listBranchUsers().then(response => {
        console.log('人员列表API响应:', response)
        const userList = response.data || []
        console.log('解析出的用户列表:', userList, '数量:', userList.length)
        this.allStaffList = userList.map(user => ({
          userId: user.userId,
          nickName: user.nickName,
          phonenumber: user.phonenumber,
          deptName: user.dept?.deptName || '',
          postName: user.posts && user.posts.length > 0 ? user.posts[0].postName : '',
          roleName: user.roles && user.roles.length > 0 ? user.roles[0].roleName : '',
          type: this.getUserType(user)
        }))
        console.log('处理后的人员列表:', this.allStaffList, '数量:', this.allStaffList.length)
        this.filterStaffList()
      }).catch(error => {
        console.error('加载人员列表失败:', error)
        this.$modal.showToast('加载人员列表失败')
      })
    },
    // 根据用户的岗位或角色判断类型
    getUserType(user) {
      const postName = user.posts && user.posts.length > 0 ? user.posts[0].postName : ''
      const roleName = user.roles && user.roles.length > 0 ? user.roles[0].roleName : ''
      const deptName = user.dept?.deptName || ''
      if (postName.includes('司机') || roleName.includes('司机') || deptName.includes('车队') || deptName.includes('司机')) {
        return 'driver'
      }
      if (postName.includes('护士') || roleName.includes('护士') || deptName.includes('护士')) {
        return 'nurse'
      }
      if (postName.includes('医生') || roleName.includes('医生') || deptName.includes('医生')) {
        return 'doctor'
      }
      if (deptName.includes('医护')) {
        return 'doctor'
      }
      return 'driver'
    },
    getUserTypeName(staffType) {
      switch(staffType) {
        case 'nurse':
          return '护士'
        case 'doctor':
          return '医生'
        case 'driver':
          return '司机'
        default:
          return '司机'
      }
    },
    // 显示人员选择弹窗
    showStaffSelector() {
      this.$refs.staffPopup.open()
      this.filterStaffList()
    },
    // 关闭人员选择弹窗
    closeStaffSelector() {
      this.$refs.staffPopup.close()
      this.staffSearchKeyword = ''
      this.staffFilterType = 'driver'
    },
    // 人员搜索
    onStaffSearch(e) {
      this.staffSearchKeyword = e.detail.value
      this.filterStaffList()
    },
    // 筛选人员类型
    filterStaff(type) {
      this.staffFilterType = type
      this.filterStaffList()
    },
    // 过滤人员列表
    filterStaffList() {
      console.log('开始过滤人员列表,原始数量:', this.allStaffList.length)
      let list = [...this.allStaffList]
      // 按类型过滤
      if (this.staffFilterType === 'driver') {
        list = list.filter(staff => staff.type === 'driver')
      } else if (this.staffFilterType === 'doctor') {
        list = list.filter(staff => staff.type === 'doctor')
      } else if (this.staffFilterType === 'nurse') {
        list = list.filter(staff => staff.type === 'nurse')
      }
      console.log('按类型过滤后:', this.staffFilterType, '数量:', list.length)
      // 按关键词搜索
      if (this.staffSearchKeyword && this.staffSearchKeyword.trim() !== '') {
        const keyword = this.staffSearchKeyword.trim().toLowerCase()
        list = list.filter(staff => {
          return staff.nickName.toLowerCase().includes(keyword) ||
                 (staff.phonenumber && staff.phonenumber.includes(keyword))
        })
      }
      console.log('按关键词过滤后,数量:', list.length)
      this.filteredStaffList = list
    },
    // 切换人员选中状态
    toggleStaffSelection(staff) {
      const index = this.selectedStaff.findIndex(s => s.userId === staff.userId)
      if (index > -1) {
        // 已选中,移除
        this.selectedStaff.splice(index, 1)
      } else {
        // 未选中,添加
        this.selectedStaff.push(staff)
      }
    },
    // 判断人员是否已选中
    isStaffSelected(userId) {
      return this.selectedStaff.some(staff => staff.userId === userId)
    },
    // 确认人员选择
    confirmStaffSelection() {
      if (this.selectedStaff.length === 0) {
        this.$modal.showToast('请至少选择一名人员')
        return
      }
      this.closeStaffSelector()
    },
    // 移除人员
    removeStaff(index) {
      this.selectedStaff.splice(index, 1)
    // 人员变化
    onStaffChange(staff) {
      console.log('选中人员变化:', staff)
      // 组件已经通过 v-model 更新了 selectedStaff
    },
    
    // 解析病情信息(从字符串解析出ICD-10疾病列表)- 修复:与创建界面保持一致
@@ -1451,133 +1216,6 @@
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
  }
  .staff-popup-container {
    height: 80vh;
    background-color: white;
    border-top-left-radius: 20rpx;
    border-top-right-radius: 20rpx;
    display: flex;
    flex-direction: column;
    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20rpx 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .popup-title {
        font-size: 32rpx;
        font-weight: bold;
        color: #333;
      }
      .close-btn {
        width: 50rpx;
        height: 50rpx;
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
    .search-bar {
      padding: 20rpx 30rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .search-input {
        height: 70rpx;
        padding: 0 20rpx;
        background-color: #f5f5f5;
        border-radius: 10rpx;
        font-size: 28rpx;
      }
    }
    .filter-tabs {
      display: flex;
      padding: 20rpx 30rpx;
      gap: 20rpx;
      border-bottom: 1rpx solid #f0f0f0;
      .filter-tab {
        flex: 1;
        height: 60rpx;
        line-height: 60rpx;
        text-align: center;
        background-color: #f5f5f5;
        border-radius: 10rpx;
        font-size: 28rpx;
        color: #666;
        &.active {
          background-color: #007AFF;
          color: white;
        }
      }
    }
    .staff-list-scroll {
      flex: 1;
      padding: 20rpx 30rpx;
      .staff-list-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20rpx;
        margin-bottom: 15rpx;
        background-color: #f8f8f8;
        border-radius: 10rpx;
        .staff-item-info {
          flex: 1;
          .staff-item-name {
            display: block;
            font-size: 30rpx;
            color: #333;
            margin-bottom: 8rpx;
          }
          .staff-item-dept {
            display: block;
            font-size: 24rpx;
            color: #999;
          }
        }
        .staff-item-check {
          width: 50rpx;
          display: flex;
          align-items: center;
          justify-content: center;
        }
      }
      .empty-tip {
        text-align: center;
        padding: 100rpx 0;
        color: #999;
        font-size: 28rpx;
      }
    }
    .popup-footer {
      padding: 20rpx 30rpx;
      border-top: 1rpx solid #f0f0f0;
      .confirm-btn {
        width: 100%;
        height: 80rpx;
        background-color: #007AFF;
        color: white;
        border-radius: 10rpx;
        font-size: 32rpx;
      }
    }
  }
dryad-payment/src/main/java/com/ruoyi/payment/application/service/PaymentNotifyService.java
@@ -62,7 +62,7 @@
            // 通信成功且业务成功
            if ("SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode)) {
                // 查询订单和交易(使用业务订单号查询)
                PaymentOrder order = paymentOrderMapper.selectByBizOrderIdAndChannel(outTradeNo, "WECHAT");
                PaymentOrder order = paymentOrderMapper.selectById(Long.parseLong(outTradeNo));
                if (order == null) {
                    log.error("订单不存在: {}", outTradeNo);
                    updateNotifyLog(notifyLog.getId(), false, "订单不存在");
@@ -134,7 +134,7 @@
            // 交易成功
            if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
                // 查询订单和交易(使用业务订单号查询)
                PaymentOrder order = paymentOrderMapper.selectByBizOrderIdAndChannel(outTradeNo, "ALIPAY");
                PaymentOrder order = paymentOrderMapper.selectById(Long.parseLong(outTradeNo));
                if (order == null) {
                    log.error("订单不存在: {}", outTradeNo);
                    updateNotifyLog(notifyLog.getId(), false, "订单不存在");
@@ -183,6 +183,276 @@
    }
    /**
     * 处理第三方支付宝回调
     * 第三方支付宝通过GET方式回调,参数通过URL传递
     *
     * 参数格式:
     * method: 方法名
     * APPID: 应用ID
     * PaidMoneyType: 支付类型
     * PaidMoney: 支付金额(分)
     * PaidRemarks: 支付备注(包含transaction_id)
     * UnixTime: 时间戳
     * Sign: 签名
     */
    @Transactional(rollbackFor = Exception.class)
    public void processAlipayThirdPartyNotify(Map<String, String> params) {
        String method = params.get("method"); // 方法名
        String appId = params.get("APPID"); // 应用ID
        String paidMoneyType = params.get("PaidMoneyType"); // 支付类型
        String paidMoney = params.get("PaidMoney"); // 支付金额(分)
        String paidRemarks = params.get("PaidRemarks"); // 支付备注(可能包含订单号和交易号)
        String unixTime = params.get("UnixTime"); // 时间戳
        String sign = params.get("Sign"); // 签名
        log.info("处理第三方支付宝回调,method: {}, APPID: {}, PaidMoneyType: {}, PaidMoney: {}, PaidRemarks: {}, UnixTime: {}, Sign: {}",
                method, appId, paidMoneyType, paidMoney, paidRemarks, unixTime, sign);
        log.info("第三方支付宝回调完整参数: {}", params);
        // 从PaidRemarks中提取订单号(out_trade_no)
        // PaidRemarks格式可能是: "订单号+交易号" 或其他格式,需要解析
        String outTradeNo = extractOutTradeNo(paidRemarks);
        String transactionId = extractTransactionId(paidRemarks);
        if (outTradeNo == null || outTradeNo.isEmpty()) {
            log.error("无法从PaidRemarks中提取订单号: {}", paidRemarks);
            return;
        }
        log.info("解析出订单号: {}, 交易号: {}", outTradeNo, transactionId);
        // 检查幂等性(使用订单号或签名)
        String notifyId = transactionId != null ? transactionId : sign;
        if (isNotifyProcessed("ALIPAY_THIRD_PARTY", notifyId)) {
            log.info("第三方支付宝回调已处理过,标识: {}", notifyId);
            return;
        }
        // 记录回调日志
        NotifyLog notifyLog = saveNotifyLog("ALIPAY_THIRD_PARTY", notifyId, JSON.toJSONString(params), true);
        try {
            // 第三方支付宝成功的判断条件(根据实际情况调整)
            // 通常只要能收到回调就表示支付成功
            if (paidMoney != null && Integer.parseInt(paidMoney) > 0) {
                // 查询订单(使用商户订单号,这里的outTradeNo实际是我们的订单ID)
                PaymentOrder order = paymentOrderMapper.selectById(Long.valueOf(outTradeNo));
                if (order == null) {
                    log.error("订单不存在: {}", outTradeNo);
                    updateNotifyLog(notifyLog.getId(), false, "订单不存在");
                    return;
                }
                // 验证金额是否匹配
                if (!order.getAmount().equals(Integer.parseInt(paidMoney))) {
                    log.error("订单金额不匹配,订单金额: {}, 回调金额: {}", order.getAmount(), paidMoney);
                    updateNotifyLog(notifyLog.getId(), false, "金额不匹配");
                    return;
                }
                // 查询待支付交易
                PaymentTransaction transaction = paymentTransactionMapper.selectPendingByOrderId(order.getId());
                if (transaction == null) {
                    // 如果没有待支付交易,尝试查询最新交易
                    transaction = paymentTransactionMapper.selectLatestByOrderId(order.getId());
                    if (transaction == null || "SUCCEEDED".equals(transaction.getStatus())) {
                        log.warn("交易已完成或不存在,订单ID: {}", order.getId());
                        updateNotifyLog(notifyLog.getId(), true, "交易已完成");
                        return;
                    }
                }
                // 更新交易状态
                transaction.setStatus("SUCCEEDED");
                if (transactionId != null) {
                    transaction.setChannelTradeNo(transactionId);
                }
                transaction.setPaidAt(LocalDateTime.now());
                transaction.setResponseSnapshot(JSON.toJSONString(params));
                paymentTransactionMapper.update(transaction);
                // 更新订单状态
                order.setStatus("SUCCEEDED");
                if (transactionId != null) {
                    order.setChannelTradeNo(transactionId);
                }
                order.setPaidAt(LocalDateTime.now());
                order.setLatestTransactionId(transaction.getId());
                paymentOrderMapper.update(order);
                // 更新通知日志
                notifyLog.setOrderId(order.getId());
                notifyLog.setTransactionId(transaction.getId());
                updateNotifyLog(notifyLog.getId(), true, "处理成功");
                // 触发业务回调
                bizCallbackService.triggerCallback(order, transaction);
                log.info("第三方支付宝回调处理成功,订单ID: {}, 交易ID: {}", order.getId(), transaction.getId());
            } else {
                log.warn("第三方支付宝支付金额异常: {}", paidMoney);
                updateNotifyLog(notifyLog.getId(), true, "金额异常: " + paidMoney);
            }
        } catch (Exception e) {
            log.error("处理第三方支付宝回调异常", e);
            updateNotifyLog(notifyLog.getId(), false, "处理异常: " + e.getMessage());
            throw e;
        }
    }
    /**
     * 从PaidRemarks中提取订单号
     * PaidRemarks可能包含订单号和交易号,需要解析
     */
    private String extractOutTradeNo(String paidRemarks) {
        if (paidRemarks == null || paidRemarks.isEmpty()) {
            return null;
        }
        // 假设PaidRemarks格式: "订单号+交易号" 或 "订单号"
        // 根据实际格式调整解析逻辑
        // 这里假设订单号在最前面,可能用某个分隔符分隔
        // 尝试提取纯数字作为订单ID
        String[] parts = paidRemarks.split("[^0-9]+");
        for (String part : parts) {
            if (part.length() > 10) { // 订单ID通常是长整型,长度较长
                return part;
            }
        }
        // 如果没有找到,返回整个字符串的数字部分
        return paidRemarks.replaceAll("[^0-9]", "");
    }
    /**
     * 从PaidRemarks中提取交易号
     */
    private String extractTransactionId(String paidRemarks) {
        if (paidRemarks == null || paidRemarks.isEmpty()) {
            return null;
        }
        // 如果PaidRemarks包含支付宝交易号(通常以2开头的28位数字)
        // 这里简单返回null,可以根据实际格式调整
        return null;
    }
    /**
     * 处理第三方微信回调
     * 第三方微信通过GET方式回调,参数通过URL传递
     *
     * 参数格式:
     * method: 方法名
     * APPID: 应用ID
     * PaidMoneyType: 支付类型
     * PaidMoney: 支付金额(分)
     * PaidRemarks: 支付备注(包含transaction_id)
     * UnixTime: 时间戳
     */
    @Transactional(rollbackFor = Exception.class)
    public void processWechatThirdPartyNotify(Map<String, String> params) {
        String method = params.get("method"); // 方法名
        String appId = params.get("APPID"); // 应用ID
        String paidMoneyType = params.get("PaidMoneyType"); // 支付类型
        String paidMoney = params.get("PaidMoney"); // 支付金额(分)
        String paidRemarks = params.get("PaidRemarks"); // 支付备注(可能包含订单号和交易号)
        String unixTime = params.get("UnixTime"); // 时间戳
        log.info("处理第三方微信回调,method: {}, APPID: {}, PaidMoneyType: {}, PaidMoney: {}, PaidRemarks: {}, UnixTime: {}",
                method, appId, paidMoneyType, paidMoney, paidRemarks, unixTime);
        log.info("第三方微信回调完整参数: {}", params);
        // 从PaidRemarks中提取订单号(out_trade_no)
        String outTradeNo = extractOutTradeNo(paidRemarks);
        String transactionId = extractTransactionId(paidRemarks);
        if (outTradeNo == null || outTradeNo.isEmpty()) {
            log.error("无法从PaidRemarks中提取订单号: {}", paidRemarks);
            return;
        }
        log.info("解析出订单号: {}, 交易号: {}", outTradeNo, transactionId);
        // 检查幂等性(使用订单号或时间戳)
        String notifyId = transactionId != null ? transactionId : (outTradeNo + "_" + unixTime);
        if (isNotifyProcessed("WECHAT_THIRD_PARTY", notifyId)) {
            log.info("第三方微信回调已处理过,标识: {}", notifyId);
            return;
        }
        // 记录回调日志
        NotifyLog notifyLog = saveNotifyLog("WECHAT_THIRD_PARTY", notifyId, JSON.toJSONString(params), true);
        try {
            // 第三方微信成功的判断条件(通常只要能收到回调就表示支付成功)
            if (paidMoney != null && Integer.parseInt(paidMoney) > 0) {
                // 查询订单(使用商户订单号,这里的outTradeNo实际是我们的订单ID)
                PaymentOrder order = paymentOrderMapper.selectById(Long.valueOf(outTradeNo));
                if (order == null) {
                    log.error("订单不存在: {}", outTradeNo);
                    updateNotifyLog(notifyLog.getId(), false, "订单不存在");
                    return;
                }
                // 验证金额是否匹配
                if (!order.getAmount().equals(Integer.parseInt(paidMoney))) {
                    log.error("订单金额不匹配,订单金额: {}, 回调金额: {}", order.getAmount(), paidMoney);
                    updateNotifyLog(notifyLog.getId(), false, "金额不匹配");
                    return;
                }
                // 查询待支付交易
                PaymentTransaction transaction = paymentTransactionMapper.selectPendingByOrderId(order.getId());
                if (transaction == null) {
                    // 如果没有待支付交易,尝试查询最新交易
                    transaction = paymentTransactionMapper.selectLatestByOrderId(order.getId());
                    if (transaction == null || "SUCCEEDED".equals(transaction.getStatus())) {
                        log.warn("交易已完成或不存在,订单ID: {}", order.getId());
                        updateNotifyLog(notifyLog.getId(), true, "交易已完成");
                        return;
                    }
                }
                // 更新交易状态
                transaction.setStatus("SUCCEEDED");
                if (transactionId != null) {
                    transaction.setChannelTradeNo(transactionId);
                }
                transaction.setPaidAt(LocalDateTime.now());
                transaction.setResponseSnapshot(JSON.toJSONString(params));
                paymentTransactionMapper.update(transaction);
                // 更新订单状态
                order.setStatus("SUCCEEDED");
                if (transactionId != null) {
                    order.setChannelTradeNo(transactionId);
                }
                order.setPaidAt(LocalDateTime.now());
                order.setLatestTransactionId(transaction.getId());
                paymentOrderMapper.update(order);
                // 更新通知日志
                notifyLog.setOrderId(order.getId());
                notifyLog.setTransactionId(transaction.getId());
                updateNotifyLog(notifyLog.getId(), true, "处理成功");
                // 触发业务回调
                bizCallbackService.triggerCallback(order, transaction);
                log.info("第三方微信回调处理成功,订单ID: {}, 交易ID: {}", order.getId(), transaction.getId());
            } else {
                log.warn("第三方微信支付金额异常: {}", paidMoney);
                updateNotifyLog(notifyLog.getId(), true, "金额异常: " + paidMoney);
            }
        } catch (Exception e) {
            log.error("处理第三方微信回调异常", e);
            updateNotifyLog(notifyLog.getId(), false, "处理异常: " + e.getMessage());
            throw e;
        }
    }
    /**
     * 检查通知是否已处理(幂等性)
     */
    private boolean isNotifyProcessed(String channel, String notifyId) {
dryad-payment/src/main/java/com/ruoyi/payment/application/service/PaymentService.java
@@ -12,6 +12,7 @@
import com.ruoyi.payment.infrastructure.channel.wechat.WxPayV2Client;
import com.ruoyi.payment.infrastructure.config.AlipayConfig;
import com.ruoyi.payment.infrastructure.config.QrCodeConfig;
import com.ruoyi.payment.infrastructure.config.WechatPayConfig;
import com.ruoyi.payment.infrastructure.persistence.mapper.PaymentOrderMapper;
import com.ruoyi.payment.infrastructure.persistence.mapper.PaymentTransactionMapper;
import com.ruoyi.payment.infrastructure.util.QrCodeUtil;
@@ -60,6 +61,9 @@
    @Autowired
    private AlipayConfig alipayConfig;
    @Autowired
    private WechatPayConfig wechatPayConfig;
    /**
     * 发起微信Native支付
     */
@@ -99,6 +103,7 @@
        requestParams.put("bizOrderId", request.getBizOrderId());
        requestParams.put("amount", request.getAmount());
        requestParams.put("subject", request.getSubject());
        requestParams.put("notifyUrl",wechatPayConfig.getNotifyUrl());
        transaction.setRequestParams(JSON.toJSONString(requestParams));
        paymentTransactionMapper.insert(transaction);
@@ -150,6 +155,7 @@
        requestParams.put("bizOrderId", request.getBizOrderId());
        requestParams.put("amount", request.getAmount());
        requestParams.put("subject", request.getSubject());
        requestParams.put("notifyUrl",alipayConfig.getNotifyUrl());
        transaction.setRequestParams(JSON.toJSONString(requestParams));
        paymentTransactionMapper.insert(transaction);
@@ -202,6 +208,7 @@
        requestParams.put("amount", request.getAmount());
        requestParams.put("subject", request.getSubject());
        requestParams.put("thirdParty", true);
        requestParams.put("notifyUrl",alipayConfig.getThirdParty().getDefaultNotifyUrl());
        transaction.setRequestParams(JSON.toJSONString(requestParams));
        paymentTransactionMapper.insert(transaction);
@@ -283,11 +290,11 @@
    private String callAlipayThirdPartyPrecreate(PaymentOrder order, PaymentRequest request) {
        try {
            // 使用AlipayConfig中配置的回调地址
            String notifyUrl = alipayConfig.getNotifyUrl();
            String notifyUrl = alipayConfig.getThirdParty().getDefaultNotifyUrl();
            String outTradeNo = String.valueOf(order.getId());
            Integer totalFee = order.getAmount(); // 单位:分
            String serviceOrdId = request.getBizOrderId(); // 业务订单ID
            return alipayThirdPartyClient.createQrCodeUrl(notifyUrl, outTradeNo, totalFee, serviceOrdId);
        } catch (Exception e) {
            log.error("调用第三方支付宝接口失败", e);
@@ -335,4 +342,298 @@
            throw new RuntimeException("查询交易状态失败: " + e.getMessage(), e);
        }
    }
}
    /**
     * 轮询查询支付状态(用于回调失败的补偿机制)
     * 支持微信和支付宝
     *
     * @param orderId 订单ID
     * @return 轮询结果(包含订单状态和交易状态)
     */
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> pollPaymentStatus(Long orderId) {
        log.info("开始轮询查询支付状态,订单ID: {}", orderId);
        Map<String, Object> result = new HashMap<>();
        // 1. 查询订单信息
        PaymentOrder order = paymentOrderMapper.selectById(orderId);
        if (order == null) {
            log.error("订单不存在,订单ID: {}", orderId);
            throw new RuntimeException("订单不存在");
        }
        // 如果订单已经是成功状态,直接返回
        if (OrderStatus.SUCCEEDED.getCode().equals(order.getStatus())) {
            log.info("订单已成功,无需轮询,订单ID: {}", orderId);
            result.put("orderStatus", order.getStatus());
            result.put("needPoll", false);
            result.put("message", "订单已成功");
            return result;
        }
        // 2. 查询最新交易记录
        PaymentTransaction transaction = paymentTransactionMapper.selectLatestByOrderId(orderId);
        if (transaction == null) {
            log.error("未找到交易记录,订单ID: {}", orderId);
            throw new RuntimeException("未找到交易记录");
        }
        // 如果交易已成功,但订单状态未更新,更新订单状态
        if (TransactionStatus.SUCCEEDED.getCode().equals(transaction.getStatus())) {
            log.info("交易已成功,同步订单状态,订单ID: {}", orderId);
            order.setStatus(OrderStatus.SUCCEEDED.getCode());
            order.setUpdatedAt(LocalDateTime.now());
            paymentOrderMapper.update(order);
            result.put("orderStatus", OrderStatus.SUCCEEDED.getCode());
            result.put("transactionStatus", transaction.getStatus());
            result.put("needPoll", false);
            result.put("message", "支付成功");
            return result;
        }
        // 3. 根据支付渠道调用相应的查询接口
        String channel = order.getChannel();
        boolean paymentSuccess = false;
        String tradeStatus = null;
        try {
            if (PayChannel.WECHAT.getCode().equals(channel)) {
                // 查询微信支付状态
                paymentSuccess = queryWechatPaymentStatus(order, transaction);
                tradeStatus = paymentSuccess ? "SUCCESS" : "NOTPAY";
                result.put("paymethod","WECHAT");
            } else if (PayChannel.ALIPAY.getCode().equals(channel)) {
                // 判断是否为第三方支付宝
                boolean isThirdParty = isThirdPartyAlipay(transaction);
                result.put("paymethod","ALIPAY");
                if (isThirdParty) {
                    // 查询第三方支付宝状态
                    tradeStatus = queryAlipayThirdPartyTradeStatus(orderId);
                    paymentSuccess = "SUCCESS".equals(tradeStatus);
                } else {
                    // 查询官方支付宝状态
                    paymentSuccess = queryAlipayPaymentStatus(order, transaction);
                    tradeStatus = paymentSuccess ? "TRADE_SUCCESS" : "WAIT_BUYER_PAY";
                }
            } else {
                log.error("不支持的支付渠道: {}", channel);
                throw new RuntimeException("不支持的支付渠道");
            }
        } catch (Exception e) {
            log.error("查询支付状态异常,订单ID: {}", orderId, e);
            result.put("orderStatus", order.getStatus());
            result.put("transactionStatus", transaction.getStatus());
            result.put("needPoll", true);
            result.put("error", e.getMessage());
            result.put("message", "查询异常,请稍后重试");
            return result;
        }
        // 4. 如果支付成功,更新订单和交易状态
        if (paymentSuccess) {
            log.info("轮询查询发现支付成功,订单ID: {}, 交易状态: {}", orderId, tradeStatus);
            // 更新交易状态
            transaction.setStatus(TransactionStatus.SUCCEEDED.getCode());
            transaction.setPaidAt(LocalDateTime.now());
            paymentTransactionMapper.update(transaction);
            // 更新订单状态
            order.setStatus(OrderStatus.SUCCEEDED.getCode());
            order.setUpdatedAt(LocalDateTime.now());
            paymentOrderMapper.update(order);
            result.put("orderStatus", OrderStatus.SUCCEEDED.getCode());
            result.put("transactionStatus", TransactionStatus.SUCCEEDED.getCode());
            result.put("tradeStatus", tradeStatus);
            result.put("needPoll", false);
            result.put("message", "支付成功");
            log.info("轮询查询处理完成,订单和交易状态已更新,订单ID: {}", orderId);
        } else {
            log.info("轮询查询,支付尚未完成,订单ID: {}, 交易状态: {}", orderId, tradeStatus);
            result.put("orderStatus", order.getStatus());
            result.put("transactionStatus", transaction.getStatus());
            result.put("tradeStatus", tradeStatus);
            result.put("needPoll", true);
            result.put("message", "支付尚未完成,请继续轮询");
        }
        return result;
    }
    /**
     * 查询微信支付状态
     */
    private boolean queryWechatPaymentStatus(PaymentOrder order, PaymentTransaction transaction) throws Exception {
        log.info("查询微信支付状态,订单ID: {}", order.getId());
        String outTradeNo = String.valueOf(order.getId());
        Map<String, String> queryResult = wxPayV2Client.queryOrder(outTradeNo);
        // 检查业务结果
        if (!"SUCCESS".equals(queryResult.get("result_code"))) {
            log.warn("微信订单查询业务失败: {}", queryResult.get("err_code_des"));
            return false;
        }
        // 检查交易状态
        String tradeState = queryResult.get("trade_state");
        log.info("微信订单状态: {}", tradeState);
        return "SUCCESS".equals(tradeState);
    }
    /**
     * 查询支付宝支付状态(官方接口)
     */
    private boolean queryAlipayPaymentStatus(PaymentOrder order, PaymentTransaction transaction) throws Exception {
        log.info("查询支付宝支付状态,订单ID: {}", order.getId());
        String outTradeNo = String.valueOf(order.getId());
        com.alipay.api.response.AlipayTradeQueryResponse queryResult = alipayF2FClient.queryOrder(outTradeNo);
        // 检查交易状态
        String tradeStatus = queryResult.getTradeStatus();
        log.info("支付宝订单状态: {}", tradeStatus);
        log.info("支付宝订单查询结果: {}", queryResult.getBody());
        // TRADE_SUCCESS 或 TRADE_FINISHED 表示支付成功
        return "TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus);
    }
    /**
     * 判断是否为第三方支付宝
     */
    private boolean isThirdPartyAlipay(PaymentTransaction transaction) {
        if (transaction.getRequestParams() == null) {
            return false;
        }
        try {
            Map<String, Object> params = JSON.parseObject(transaction.getRequestParams(), Map.class);
            return params.containsKey("thirdParty") && Boolean.TRUE.equals(params.get("thirdParty"));
        } catch (Exception e) {
            log.warn("解析交易请求参数失败", e);
            return false;
        }
    }
    /**
     * 根据交易单号查询微信支付状态
     *
     * @param outTradeNo 商户订单号(交易单号)
     * @return 交易状态信息
     */
    public Map<String, Object> queryWechatTradeByOutTradeNo(String outTradeNo) {
        log.info("根据交易单号查询微信支付状态,商户订单号: {}", outTradeNo);
        Map<String, Object> result = new HashMap<>();
        try {
            Map<String, String> queryResult = wxPayV2Client.queryOrder(outTradeNo);
            // 检查调用是否成功
            if (!"SUCCESS".equals(queryResult.get("return_code"))) {
                result.put("success", false);
                result.put("message", "微信查询接口调用失败: " + queryResult.get("return_msg"));
                return result;
            }
            // 检查业务结果
            if (!"SUCCESS".equals(queryResult.get("result_code"))) {
                result.put("success", false);
                result.put("message", "微信订单查询业务失败: " + queryResult.get("err_code_des"));
                result.put("errorCode", queryResult.get("err_code"));
                return result;
            }
            // 返回交易信息
            result.put("success", true);
            result.put("tradeState", queryResult.get("trade_state")); // 交易状态
            result.put("tradeStateDesc", queryResult.get("trade_state_desc")); // 交易状态描述
            result.put("outTradeNo", queryResult.get("out_trade_no")); // 商户订单号
            result.put("transactionId", queryResult.get("transaction_id")); // 微信支付订单号
            result.put("totalFee", queryResult.get("total_fee")); // 订单金额(分)
            result.put("timeEnd", queryResult.get("time_end")); // 支付完成时间
            result.put("openid", queryResult.get("openid")); // 用户标识
            // 判断是否支付成功
            boolean isPaid = "SUCCESS".equals(queryResult.get("trade_state"));
            result.put("isPaid", isPaid);
            result.put("message", isPaid ? "支付成功" : "支付未完成");
            log.info("微信交易查询成功,交易状态: {}", queryResult.get("trade_state"));
        } catch (Exception e) {
            log.error("查询微信交易状态异常,商户订单号: {}", outTradeNo, e);
            result.put("success", false);
            result.put("message", "查询异常: " + e.getMessage());
        }
        return result;
    }
    /**
     * 根据交易单号查询支付宝支付状态
     *
     * @param outTradeNo 商户订单号(交易单号)
     * @return 交易状态信息
     */
    public Map<String, Object> queryAlipayTradeByOutTradeNo(String outTradeNo) {
        log.info("根据交易单号查询支付宝支付状态,商户订单号: {}", outTradeNo);
        Map<String, Object> result = new HashMap<>();
        try {
            com.alipay.api.response.AlipayTradeQueryResponse queryResult = alipayF2FClient.queryOrder(outTradeNo);
            // 检查调用是否成功
            if (!queryResult.isSuccess()) {
                result.put("success", false);
                result.put("message", "支付宝查询失败: " + queryResult.getSubMsg());
                result.put("errorCode", queryResult.getSubCode());
                return result;
            }
            // 返回交易信息
            result.put("success", true);
            result.put("tradeStatus", queryResult.getTradeStatus()); // 交易状态
            result.put("outTradeNo", queryResult.getOutTradeNo()); // 商户订单号
            result.put("tradeNo", queryResult.getTradeNo()); // 支付宝交易号
            result.put("totalAmount", queryResult.getTotalAmount()); // 订单金额(元)
            result.put("buyerPayAmount", queryResult.getBuyerPayAmount()); // 买家实际支付金额
            result.put("sendPayDate", queryResult.getSendPayDate()); // 交易支付时间
            result.put("buyerLogonId", queryResult.getBuyerLogonId()); // 买家支付宝账号
            // 判断是否支付成功
            String tradeStatus = queryResult.getTradeStatus();
            boolean isPaid = "TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus);
            result.put("isPaid", isPaid);
            // 状态说明
            String message;
            if ("TRADE_SUCCESS".equals(tradeStatus)) {
                message = "交易支付成功";
            } else if ("TRADE_FINISHED".equals(tradeStatus)) {
                message = "交易结束,不可退款";
            } else if ("WAIT_BUYER_PAY".equals(tradeStatus)) {
                message = "交易创建,等待买家付款";
            } else if ("TRADE_CLOSED".equals(tradeStatus)) {
                message = "未付款交易超时关闭,或支付完成后全额退款";
            } else {
                message = "未知状态: " + tradeStatus;
            }
            result.put("message", message);
            log.info("支付宝交易查询成功,交易状态: {}", tradeStatus);
        } catch (Exception e) {
            log.error("查询支付宝交易状态异常,商户订单号: {}", outTradeNo, e);
            result.put("success", false);
            result.put("message", "查询异常: " + e.getMessage());
        }
        return result;
    }
}
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/AlipayConfig.java
@@ -48,7 +48,7 @@
        /** 第三方接口URL */
        private String url = "https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php";
        /** 默认回调地址 */
        private String defaultNotifyUrl = "https://dsp.966120.com.cn/alipay/pay_notify";
        private String defaultNotifyUrl = "https://api.966120.com.cn/alipay/DspNotifyUrl.php";
        /** 超时时间(毫秒) */
        private int timeout = 30000;
    }
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/ThirdPartyConfig.java
New file
@@ -0,0 +1,17 @@
package com.ruoyi.payment.infrastructure.config;
import lombok.Data;
import java.io.Serializable;
@Data
public class ThirdPartyConfig implements Serializable {
    /** 是否启用 */
    private boolean enabled = false;
    /** 第三方接口URL */
    private String url = "https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php";
    /** 默认回调地址 */
    private String defaultNotifyUrl = "https://api.966120.com.cn/alipay/DspNotifyUrl.php";
    /** 超时时间(毫秒) */
    private int timeout = 30000;
}
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/config/WechatPayConfig.java
@@ -33,4 +33,10 @@
    /** 签名类型 */
    private String signType = "MD5";
    /**
     * 第三方支付配置
     */
    private ThirdPartyConfig thirdParty = new ThirdPartyConfig();
}
dryad-payment/src/main/java/com/ruoyi/payment/interfaces/controller/PaymentController.java
@@ -33,6 +33,7 @@
    /**
     * 发起微信Native支付
     */
    @Anonymous()
    @PostMapping("/wechat/native")
    public AjaxResult createWechatNativePayment(@Validated @RequestBody PaymentRequest request) {
        try {
@@ -47,6 +48,7 @@
    /**
     * 发起支付宝当面付
     */
    @Anonymous()
    @PostMapping("/alipay/precreate")
    public AjaxResult createAlipayPrecreate(@Validated @RequestBody PaymentRequest request) {
        try {
@@ -61,6 +63,7 @@
    /**
     * 发起支付宝当面付(第三方接口)
     */
    @Anonymous()
    @PostMapping("/alipay/thirdparty/precreate")
    public AjaxResult createAlipayThirdPartyPrecreate(@Validated @RequestBody PaymentRequest request) {
        try {
@@ -75,6 +78,7 @@
    /**
     * 查询订单
     */
    @Anonymous()
    @GetMapping("/orders/{orderId}")
    public AjaxResult getOrder(@PathVariable Long orderId) {
        try {
@@ -92,6 +96,7 @@
    /**
     * 查询最新交易
     */
    @Anonymous()
    @GetMapping("/orders/{orderId}/transactions/latest")
    public AjaxResult getLatestTransaction(@PathVariable Long orderId) {
        try {
@@ -109,6 +114,7 @@
    /**
     * 查询支付宝第三方交易状态
     */
    @Anonymous()
    @GetMapping("/alipay/thirdparty/query/{orderId}")
    public AjaxResult queryAlipayThirdPartyTradeStatus(@PathVariable Long orderId) {
        try {
@@ -119,4 +125,92 @@
            return AjaxResult.error("查询失败: " + e.getMessage());
        }
    }
}
    /**
     * 轮询查询支付状态(用于回调失败的补偿机制)
     * <p>
     * 这个接口用于在支付回调未成功时,主动轮询查询支付状态。
     * 支持微信和支付宝(包括第三方支付宝)。
     * <p>
     * 返回结果包含:
     * - orderStatus: 订单状态
     * - transactionStatus: 交易状态
     * - tradeStatus: 第三方交易状态
     * - needPoll: 是否需要继续轮询
     * - message: 提示信息
     * <p>
     * 建议轮询策略:
     * 1. 首次轮询:立即轮询
     * 2. 后续轮询:间隔3-5秒
     * 3. 最多轮询20次,约100秒后停止
     * 4. 如果 needPoll=false,表示支付已完成或确认失败,无需继续轮询
     */
    @Anonymous()
    @GetMapping("/poll/{orderId}")
    public AjaxResult pollPaymentStatus(@PathVariable Long orderId) {
        try {
            java.util.Map<String, Object> result = paymentService.pollPaymentStatus(orderId);
            return AjaxResult.success(result);
        } catch (Exception e) {
            log.error("轮询查询支付状态失败", e);
            return AjaxResult.error("查询失败: " + e.getMessage());
        }
    }
    /**
     * 根据交易单号查询微信支付状态
     * <p>
     * 该接口用于通过商户订单号(交易单号)直接查询微信支付状态。
     * <p>
     * 返回结果包含:
     * - success: 查询是否成功
     * - isPaid: 是否已支付
     * - tradeState: 交易状态(SUCCESS-支付成功,NOTPAY-未支付,CLOSED-已关闭等)
     * - transactionId: 微信支付订单号
     * - totalFee: 订单金额(分)
     * - timeEnd: 支付完成时间
     * - message: 提示信息
     *
     * @param outTradeNo 商户订单号(交易单号)
     */
    @Anonymous()
    @GetMapping("/wechat/query/{outTradeNo}")
    public AjaxResult queryWechatTrade(@PathVariable String outTradeNo) {
        try {
            java.util.Map<String, Object> result = paymentService.queryWechatTradeByOutTradeNo(outTradeNo);
            return AjaxResult.success(result);
        } catch (Exception e) {
            log.error("查询微信交易状态失败,交易单号: {}", outTradeNo, e);
            return AjaxResult.error("查询失败: " + e.getMessage());
        }
    }
    /**
     * 根据交易单号查询支付宝支付状态
     * <p>
     * 该接口用于通过商户订单号(交易单号)直接查询支付宝支付状态。
     * <p>
     * 返回结果包含:
     * - success: 查询是否成功
     * - isPaid: 是否已支付
     * - tradeStatus: 交易状态(TRADE_SUCCESS-支付成功,WAIT_BUYER_PAY-等待付款,TRADE_CLOSED-已关闭等)
     * - tradeNo: 支付宝交易号
     * - totalAmount: 订单金额(元)
     * - sendPayDate: 交易支付时间
     * - buyerLogonId: 买家支付宝账号
     * - message: 提示信息
     *
     * @param outTradeNo 商户订单号(交易单号)
     */
    @Anonymous()
    @GetMapping("/alipay/query/{outTradeNo}")
    public AjaxResult queryAlipayTrade(@PathVariable String outTradeNo) {
        try {
            java.util.Map<String, Object> result = paymentService.queryAlipayTradeByOutTradeNo(outTradeNo);
            return AjaxResult.success(result);
        } catch (Exception e) {
            log.error("查询支付宝交易状态失败,交易单号: {}", outTradeNo, e);
            return AjaxResult.error("查询失败: " + e.getMessage());
        }
    }
}
dryad-payment/src/main/java/com/ruoyi/payment/interfaces/controller/PaymentNotifyController.java
@@ -37,7 +37,7 @@
     * 微信支付回调
     */
    @Anonymous
    @PostMapping("/wechat")
    @PostMapping("/wechat/notify")
    public String wechatNotify(HttpServletRequest request) {
        try {
            log.info("接收到微信支付回调");
@@ -78,7 +78,7 @@
     * 支付宝回调
     */
    @Anonymous
    @PostMapping("/alipay")
    @PostMapping("/alipay/notify")
    public String alipayNotify(HttpServletRequest request) {
        try {
            log.info("接收到支付宝回调");
@@ -113,6 +113,105 @@
    }
    /**
     * 第三方支付宝回调(GET请求)
     * <p>
     * 第三方支付宝通过GET方式回调,参数通过URL传递
     * <p>
     * 参数格式:
     * method=xxx&APPID=xxx&PaidMoneyType=xxx&PaidMoney=xxx&PaidRemarks=xxx&UnixTime=xxx&Sign=xxx
     * <p>
     * 参数说明:
     * - method: 方法名
     * - APPID: 应用ID
     * - PaidMoneyType: 支付类型
     * - PaidMoney: 支付金额(单位:分)
     * - PaidRemarks: 支付备注(包含订单号和交易号信息)
     * - UnixTime: 时间戳
     * - Sign: 签名
     * <p>
     * 示例:
     * GET /api/pay/notify/alipay/thirdparty?method=alipay.pay&APPID=123456&PaidMoneyType=alipay&PaidMoney=100&PaidRemarks=1234567890&UnixTime=1638360000&Sign=abc123
     */
    @Anonymous
    @GetMapping("/alipay/thirdparty")
    public String alipayThirdPartyNotify(HttpServletRequest request) {
        try {
            log.info("接收到第三方支付宝回调(GET请求)");
            // 1. 获取所有参数
            Map<String, String> params = new HashMap<>();
            Enumeration<String> parameterNames = request.getParameterNames();
            while (parameterNames.hasMoreElements()) {
                String name = parameterNames.nextElement();
                params.put(name, request.getParameter(name));
            }
            log.info("第三方支付宝回调参数: {}", params);
            // 2. 第三方支付宝不需要验签,或者根据第三方平台的签名规则进行验证
            // 这里直接处理,如果需要验签可以在Service层添加
            // 3. 处理回调(更新订单状态、触发业务回调)
            paymentNotifyService.processAlipayThirdPartyNotify(params);
            // 4. 返回第三方支付宝要求的应答
            return "success";
        } catch (Exception e) {
            log.error("处理第三方支付宝回调失败", e);
            return "fail";
        }
    }
    /**
     * 第三方微信回调(GET请求)
     * <p>
     * 第三方微信通过GET方式回调,参数通过URL传递
     * <p>
     * 参数格式:
     * method=xxx&APPID=xxx&PaidMoneyType=xxx&PaidMoney=xxx&PaidRemarks=xxx&UnixTime=xxx
     * <p>
     * 参数说明:
     * - method: 方法名
     * - APPID: 应用ID
     * - PaidMoneyType: 支付类型
     * - PaidMoney: 支付金额(单位:分)
     * - PaidRemarks: 支付备注(包含订单号和交易号信息)
     * - UnixTime: 时间戳
     * <p>
     * 示例:
     * GET /api/pay/notify/wechat/thirdparty?method=wechat.pay&APPID=123456&PaidMoneyType=wechat&PaidMoney=100&PaidRemarks=1234567890&UnixTime=1638360000
     */
    @Anonymous
    @GetMapping("/wechat/thirdparty")
    public String wechatThirdPartyNotify(HttpServletRequest request) {
        try {
            log.info("接收到第三方微信回调(GET请求)");
            // 1. 获取所有参数
            Map<String, String> params = new HashMap<>();
            Enumeration<String> parameterNames = request.getParameterNames();
            while (parameterNames.hasMoreElements()) {
                String name = parameterNames.nextElement();
                params.put(name, request.getParameter(name));
            }
            log.info("第三方微信回调参数: {}", params);
            // 2. 第三方微信不需要验签,或者根据第三方平台的签名规则进行验证
            // 这里直接处理,如枟需要验签可以在Service层添加
            // 3. 处理回调(更新订单状态、触发业务回调)
            paymentNotifyService.processWechatThirdPartyNotify(params);
            // 4. 返回第三方微信要求的应答
            return "success";
        } catch (Exception e) {
            log.error("处理第三方微信回调失败", e);
            return "fail";
        }
    }
    /**
     * 读取请求体
     */
    private String readRequestBody(HttpServletRequest request) throws Exception {
ruoyi-admin/src/main/resources/application.yml
@@ -205,9 +205,14 @@
    mchId: 1573728151
    appName: 民航医疗快线
    mchKey: Xz0ClPK3f5sCeT6SGTaha1vpVmyUFcbp
    notifyUrl: https://dsp.966120.com/api/pay/notify/wechat
    notifyUrl: https://dsp.966120.com.cn/api/pay/notify/wechat/notify
    signType: MD5
    checkSign: false
    thirdParty:
      enabled: false
      url: https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php
      defaultNotifyUrl: https://api.966120.com.cn/weixin/DspNotifyUrl.asp
      timeout: 30000
  
  # 支付宝配置
  alipay:
@@ -215,7 +220,7 @@
    privateKey: MIIEpgIBAAKCAQEArt273bWTEPXPjCsUYwFx7CNjhcQlm1NtbNjfeIsZ2g9sbFCQP9qpyufp6zkBv6eq+6WEztkC1KwSsuDjP5LvgY/1pmGFlr8r7cjeZI4bTeIe9jG5UaHolnzbdXUlSoInzgWRvbYXxuQZciwVpokwviW27YK9wPIzz9OTiRquL8b3YWPZLO7xK0gBMa2KfFfUXxCB8gHJtidQ+FjjYXb2WpnScKLJdKfWcGWFnyGiZOknyFR9kI8cm0cYPNHtecQId0bQ1ee7YDLD8dBPd2Pd/JBC4Wn6HuOvZOLqZvIpIj+8q0zGXjUUns6MsjNL3MUKuhKy6hQGwP5sGrPcVcwqiwIDAQABAoIBAQCTeW9iSSsRx61VUlOsN+DDPQlHHCh3OcH0ZWb6e52+2Pkg1EUDhT9jX3lZJsfBwf8iofJCnKSVhdVzRNSCnkIdq7KJsn9+phW/QYPFnE+MvKJOEZtwLDNDD2PqSHS9xM0bJHlIXNTqqR6IuoM740HXa2k+H+A2ZE2r/YzUuUqkASwAYPKYzWa1wivg4CZrvoPZ5bXvYOHoV0jZEtyUQB9dHuCz+bghbR+28vGkYwEGInLsOCe6Gl5D61F0l2qAXRQky3P0jXxIPXPFmhBYutAAUufLpryruQgL3MDDm5dhBJpwp89qwFDjc8fWVS/FFYJ0KDQOpAxAI800YHgaJ4VBAoGBAOdCcoYS49Q9Iula7gFGVXeto7QSUsP0CnQZM/tsAU6TiX3XG5pxBYoVmYSIQPylgagRTBZHD5t/LGa+I+KYMHSVAH/kndPdgTO7EiwfUCzi1DmGZVWs7XJ2zRfTmYRVdsElPy3Z8Pm802jd7mhffw/5p6y9pzNJqOjmGOUbYGrrAoGBAMGS1CGHkK4nD2BsIWJoKW2Lxph3Bq5hN1iaE8ZjOAvFT9drNqfwRa6BVGyfYEXPZTvUT0FUNMdukcTCM6O3FRU8EABJIcVue2QA+BvkfwPEU3JxMA8DrWHO7IcgtG9wjbxretDsf+SZmkQgK0ZUPod5ZZSycFxM3/GiXDQjGhbhAoGBAL/Y/+j6AscvcKbmKEwmbQC7q/LWwJKPAZ0Oy3DoSK1G9+jNarjUyiOjh5fK8R6mrskekGBq0yfMeKlDU8HHP2t3sNJodgYs2+JubsTrtTeHdUfDlo1cyB8NL1d00wZVYA8bNy5yftavLzLv6bfsgRxfoBpNu0dw9A9B06U88N/BAoGBAKCN/nEJFlG8iB570Xzj1GjOJJzVLK96ZwOQWJKWPShWMhEFFkJZIhLJppKp5ppAmUD0qgAPre80oKdIRLin5E7GkKcMAXzWVHXv79qCvW8MagJkK25oqGiVzs2NrNs5yfXcV/PuFW4wkSmsXPhqa6rGYCDjmBqWkLDE8CE2dC9BAoGBAJ2+QfAAvB27mr48+vY4HxZyPRrCBA1YkokraWQ0IlfD0MKsFw0Af4Iu3oy7V6NlE2GwL/AcObyHeGZt7DLbViAQOgmb9BpUrjUZ4bXSBuGPfRe11HCu6j6W67qa76TAoy3A0Dfm0OE/m9r4H99NaLzeBm1KluySKkfYXoqyueQw
    alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl8BjXYaudmGT+sEos0AXEUrKl6+wyw6++hBoJDdY7P+P7poa34GN4YSkkavTA/HplmRM1wTLcY+NqhxhLpNrcgy08AbC/GGcLM2wxQGFa+L+DQLz34uBAShXDK8yN6O49UdbbJ2RRaJSAb+nW4ZVCPMGtMu4S3lXTymQgizM1IYo9L92U5QPRzZRSP8+AmQPzwofRqEgvkO02s66xU2G5AAdkVg5BQm34eM0Io2CmcWF9jSoWQTJdyd7tw3oec9NqD7x3CfcsN3NAJOQLz4+bWWqDWelyviyAr2reeH6AuBVjaWwAvAJx3yuLevKMXTzPC95Ja7w4XYSB9Fg2+aKmwIDAQAB
    serverUrl: https://openapi.alipay.com/gateway.do
    notifyUrl: https://dsp.966120.com/api/pay/notify/alipay
    notifyUrl: https://dsp.966120.com.cn/api/pay/notify/alipay/notify
    signType: RSA2
    checkSign: false
    # 支付方式: OFFICIAL(官方支付宝) 或 THIRD_PARTY(第三方支付宝)
@@ -224,7 +229,8 @@
    thirdParty:
      enabled: true
      url: https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php
      defaultNotifyUrl: https://dsp.966120.com.cn/alipay/pay_notify
#      defaultNotifyUrl: https://api.966120.com.cn/alipay/DspNotifyUrl.php
      defaultNotifyUrl: https://dsp.966120.com.cn/api/pay/notify/alipay/notify
      timeout: 30000
  
  # 业务回调配置
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskPaymentServiceImpl.java
@@ -262,6 +262,7 @@
            
            // 生成回调地址
            String callbackUrl = callbackBaseUrl + "/payment/callback/" + provider.toLowerCase();
            payment.setCallbackUrl(callbackUrl);
            
            try {