wlzboy
2026-03-31 61c4c3f45e4257e2e7662f033e2719e62366c632
feat: 优化申请发票,还可以修改发票信息
11个文件已修改
2个文件已添加
975 ■■■■■ 已修改文件
app/api/invoice.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/mine/invoice/detail.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/mine/invoice/edit.vue 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/mine/invoice/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysInvoiceController.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyInvoiceMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyTransferSyncMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysInvoiceService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysInvoiceServiceImpl.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/LegacyInvoiceMapper.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/LegacyTransferSyncMapper.xml 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/invoice.js
@@ -34,3 +34,29 @@
    method: 'get'
  })
}
// 更新发票申请(待审核/已驳回状态可编辑)
export function updateInvoice(data) {
  return request({
    url: '/system/invoice',
    method: 'put',
    data: data
  })
}
// App端用户编辑自己的发票(专用接口,安全校验)
export function myEditInvoice(data) {
  return request({
    url: '/system/invoice/myEdit',
    method: 'put',
    data: data
  })
}
// App端查看发票详情(专用接口,仅可查看自己的)
export function myInvoiceDetail(invoiceId) {
  return request({
    url: `/system/invoice/myDetail/${invoiceId}`,
    method: 'get'
  })
}
app/pages.json
@@ -120,6 +120,16 @@
    "style": {
      "navigationBarTitleText": "申请发票"
    }
  }, {
    "path": "pages/mine/invoice/edit",
    "style": {
      "navigationBarTitleText": "编辑发票申请"
    }
  }, {
    "path": "pages/mine/invoice/detail",
    "style": {
      "navigationBarTitleText": "发票详情"
    }
  }],
  "subPackages": [{
    "root": "pagesTask",
app/pages/mine/invoice/detail.vue
New file
@@ -0,0 +1,314 @@
<template>
  <view class="invoice-detail-container">
    <view v-if="loading" class="loading-box">
      <text class="text-gray">加载中...</text>
    </view>
    <block v-else-if="info">
      <!-- 状态横幅 -->
      <view class="status-banner" :class="statusBannerClass">
        <text class="status-icon">{{ statusIcon }}</text>
        <text class="status-text">{{ statusLabel }}</text>
      </view>
      <!-- 驳回原因 -->
      <view v-if="info.status === 2 && info.auditRemarks" class="reject-reason">
        <text class="reject-label">驳回原因:</text>
        <text class="reject-content">{{ info.auditRemarks }}</text>
      </view>
      <!-- 基本信息 -->
      <view class="section-card">
        <view class="section-title">申请信息</view>
        <view class="detail-row">
          <text class="d-label">服务单号</text>
          <text class="d-value">{{ info.serviceCode || info.legacyServiceOrderId || '—' }}</text>
        </view>
        <view class="detail-row">
          <text class="d-label">开票类型</text>
          <text class="d-value">{{ info.invoiceType == 2 ? '企业' : '个人' }}</text>
        </view>
        <view class="detail-row">
          <text class="d-label">发票抬头</text>
          <text class="d-value">{{ info.invoiceName || '—' }}</text>
        </view>
        <view class="detail-row">
          <text class="d-label">申请金额</text>
          <text class="d-value text-price">¥{{ info.invoiceMoney ? Number(info.invoiceMoney).toFixed(2) : '0.00' }}</text>
        </view>
        <view class="detail-row" v-if="info.invoiceRemarks">
          <text class="d-label">发票备注</text>
          <text class="d-value">{{ info.invoiceRemarks }}</text>
        </view>
        <view class="detail-row">
          <text class="d-label">申请时间</text>
          <text class="d-value">{{ formatTime(info.applyTime) }}</text>
        </view>
      </view>
      <!-- 企业信息 -->
      <view class="section-card" v-if="info.invoiceType == 2">
        <view class="section-title">企业信息</view>
        <view class="detail-row" v-if="info.companyAddress">
          <text class="d-label">注册地址</text>
          <text class="d-value">{{ info.companyAddress }}</text>
        </view>
        <view class="detail-row" v-if="info.companyBank">
          <text class="d-label">开户银行</text>
          <text class="d-value">{{ info.companyBank }}</text>
        </view>
        <view class="detail-row" v-if="info.companyBankNo">
          <text class="d-label">银行帐号</text>
          <text class="d-value">{{ info.companyBankNo }}</text>
        </view>
      </view>
      <!-- 邮寄信息 -->
      <view class="section-card" v-if="info.zipCode || info.mailAddress || info.contactName || info.contactPhone">
        <view class="section-title">邮寄信息</view>
        <view class="detail-row" v-if="info.zipCode">
          <text class="d-label">邮编</text>
          <text class="d-value">{{ info.zipCode }}</text>
        </view>
        <view class="detail-row" v-if="info.mailAddress">
          <text class="d-label">邮寄地址</text>
          <text class="d-value">{{ info.mailAddress }}</text>
        </view>
        <view class="detail-row" v-if="info.contactName">
          <text class="d-label">联系人</text>
          <text class="d-value">{{ info.contactName }}</text>
        </view>
        <view class="detail-row" v-if="info.contactPhone">
          <text class="d-label">联系电话</text>
          <text class="d-value">{{ info.contactPhone }}</text>
        </view>
      </view>
      <!-- 审核/发票信息 -->
      <view class="section-card" v-if="info.status === 1">
        <view class="section-title">发票信息</view>
        <view class="detail-row" v-if="info.invoiceNo">
          <text class="d-label">发票编号</text>
          <text class="d-value">{{ info.invoiceNo }}</text>
        </view>
        <view class="detail-row" v-if="info.auditTime">
          <text class="d-label">审核时间</text>
          <text class="d-value">{{ formatTime(info.auditTime) }}</text>
        </view>
      </view>
      <!-- 操作按钮 -->
      <view class="action-area">
        <button v-if="info.invoiceUrl" class="cu-btn block bg-green lg margin-bottom-sm" @click="handleDownload">
          查看发票
        </button>
        <button v-if="info.status === 0 || info.status === 2" class="cu-btn block bg-blue lg" @click="handleEdit">
          编辑申请
        </button>
      </view>
    </block>
    <view v-else class="empty-box">
      <text class="text-gray">暂无数据</text>
    </view>
  </view>
</template>
<script>
import { myInvoiceDetail } from "@/api/invoice"
import config from '@/config.js'
export default {
  data() {
    return {
      loading: true,
      info: null,
      invoiceId: null
    }
  },
  computed: {
    statusLabel() {
      const map = { 0: '待审核', 1: '已通过', 2: '已驳回' }
      return this.info ? (map[this.info.status] || '未知') : ''
    },
    statusBannerClass() {
      if (!this.info) return ''
      const map = { 0: 'banner-pending', 1: 'banner-passed', 2: 'banner-rejected' }
      return map[this.info.status] || ''
    },
    statusIcon() {
      if (!this.info) return ''
      const map = { 0: '⏳', 1: '✅', 2: '❌' }
      return map[this.info.status] || ''
    }
  },
  onLoad(options) {
    if (options && options.invoiceId) {
      this.invoiceId = Number(options.invoiceId)
      this.loadDetail()
    } else {
      this.loading = false
    }
  },
  methods: {
    loadDetail() {
      this.loading = true
      myInvoiceDetail(this.invoiceId).then(res => {
        this.info = res.data
      }).catch(err => {
        console.error('加载发票详情失败:', err)
        this.$modal.msgError('加载失败,请返回重试')
      }).finally(() => {
        this.loading = false
      })
    },
    formatTime(time) {
      if (!time) return '—'
      const date = new Date(time)
      const y = date.getFullYear()
      const m = String(date.getMonth() + 1).padStart(2, '0')
      const d = String(date.getDate()).padStart(2, '0')
      const h = String(date.getHours()).padStart(2, '0')
      const min = String(date.getMinutes()).padStart(2, '0')
      return `${y}-${m}-${d} ${h}:${min}`
    },
    handleEdit() {
      const invoiceInfo = encodeURIComponent(JSON.stringify(this.info))
      this.$tab.navigateTo(`/pages/mine/invoice/edit?invoiceInfo=${invoiceInfo}`)
    },
    handleDownload() {
      const url = this.info.invoiceUrl
      if (!url) return
      let fullUrl = url.startsWith('http') ? url : config.baseUrl + url
      const fileExt = fullUrl.match(/\.(\w+)$/)?.[1]?.toLowerCase()
      // #ifdef H5
      window.open(fullUrl)
      // #endif
      // #ifndef H5
      const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
      if (imageExts.includes(fileExt)) {
        uni.previewImage({ urls: [fullUrl], current: fullUrl })
        return
      }
      uni.showLoading({ title: '下载中...', mask: true })
      uni.downloadFile({
        url: fullUrl,
        success: (res) => {
          uni.hideLoading()
          if (res.statusCode === 200) {
            uni.openDocument({
              filePath: res.tempFilePath,
              showMenu: true,
              fail: (err) => {
                uni.showModal({ title: '提示', content: `无法打开文件: ${err.errMsg || '未知错误'}`, showCancel: false })
              }
            })
          } else {
            this.$modal.msgError(`下载失败,状态码: ${res.statusCode}`)
          }
        },
        fail: (err) => {
          uni.hideLoading()
          this.$modal.msgError(`下载失败: ${err.errMsg || '未知错误'}`)
        }
      })
      // #endif
    }
  }
}
</script>
<style lang="scss">
.invoice-detail-container {
  min-height: 100vh;
  background-color: #f5f6fa;
  padding-bottom: 60rpx;
  .loading-box, .empty-box {
    padding: 120rpx;
    text-align: center;
  }
  // 状态横幅
  .status-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 36rpx 0;
    gap: 16rpx;
    .status-icon { font-size: 44rpx; }
    .status-text { font-size: 36rpx; font-weight: bold; }
    &.banner-pending { background: #fff8e1; .status-text { color: #e6a817; } }
    &.banner-passed  { background: #e8f5e9; .status-text { color: #4caf50; } }
    &.banner-rejected{ background: #fdecea; .status-text { color: #f44336; } }
  }
  // 驳回原因
  .reject-reason {
    margin: 0 24rpx 16rpx;
    padding: 24rpx;
    background: #fdecea;
    border-radius: 12rpx;
    border-left: 6rpx solid #f44336;
    .reject-label { font-size: 26rpx; color: #f44336; font-weight: bold; }
    .reject-content { font-size: 26rpx; color: #d32f2f; display: block; margin-top: 8rpx; }
  }
  // 信息卡片
  .section-card {
    margin: 16rpx 24rpx;
    background: #fff;
    border-radius: 16rpx;
    padding: 0 30rpx;
    box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
    .section-title {
      font-size: 28rpx;
      font-weight: bold;
      color: #333;
      padding: 28rpx 0 20rpx;
      border-bottom: 1rpx solid #f0f0f0;
      margin-bottom: 4rpx;
    }
    .detail-row {
      display: flex;
      align-items: flex-start;
      padding: 22rpx 0;
      border-bottom: 1rpx solid #f8f8f8;
      &:last-child { border-bottom: none; }
      .d-label {
        font-size: 26rpx;
        color: #999;
        min-width: 160rpx;
        flex-shrink: 0;
      }
      .d-value {
        font-size: 26rpx;
        color: #333;
        flex: 1;
        word-break: break-all;
        &.text-price {
          color: #f44336;
          font-weight: bold;
          font-size: 30rpx;
        }
      }
    }
  }
  // 操作区
  .action-area {
    margin: 30rpx 24rpx 0;
  }
}
</style>
app/pages/mine/invoice/edit.vue
New file
@@ -0,0 +1,262 @@
<template>
  <view class="invoice-apply-container bg-white">
    <form>
      <!-- 关联任务(只读展示,不可更改) -->
      <view class="selected-task" v-if="form.serviceOrderId">
        <view class="label">关联任务</view>
        <view class="task-card">
          <view class="card-header">
            <text class="service-code">{{ taskDisplay }}</text>
          </view>
          <view class="task-info-detail" v-if="invoiceInfo.legacyServiceOrderId">
            <view class="info-row">
              <text class="info-label">服务单号:</text>
              <text class="info-value">{{ invoiceInfo.legacyServiceOrderId }}</text>
            </view>
          </view>
        </view>
      </view>
      <view class="cu-form-group">
        <view class="title">开票类型</view>
        <radio-group class="flex" @change="handleTypeChange">
          <view class="margin-right-sm">
            <radio value="1" :checked="form.invoiceType == 1"></radio><text class="margin-left-xs">个人</text>
          </view>
          <view>
            <radio value="2" :checked="form.invoiceType == 2"></radio><text class="margin-left-xs">企业</text>
          </view>
        </radio-group>
      </view>
      <view class="cu-form-group">
        <view class="title required">发票抬头</view>
        <input placeholder="请输入发票抬头" v-model="form.invoiceName" />
      </view>
      <view class="cu-form-group">
        <view class="title required">发票金额</view>
        <input
          type="digit"
          placeholder="请输入金额"
          v-model="form.invoiceMoney"
          @blur="validateMoney"
        />
      </view>
      <view class="cu-form-group align-start">
        <view class="title">发票备注</view>
        <textarea maxlength="-1" v-model="form.invoiceRemarks" placeholder="请输入备注信息"></textarea>
      </view>
      <block v-if="form.invoiceType == 2">
        <view class="cu-form-group">
          <view class="title">注册地址</view>
          <input placeholder="请输入企业注册地址" v-model="form.companyAddress" />
        </view>
        <view class="cu-form-group">
          <view class="title">开户银行</view>
          <input placeholder="请输入企业开户银行" v-model="form.companyBank" />
        </view>
        <view class="cu-form-group">
          <view class="title">银行帐号</view>
          <input placeholder="请输入企业银行帐号" v-model="form.companyBankNo" />
        </view>
      </block>
      <view class="cu-form-group margin-top-sm">
        <view class="title">邮编</view>
        <input placeholder="请输入邮编" v-model="form.zipCode" />
      </view>
      <view class="cu-form-group">
        <view class="title">邮寄地址</view>
        <input placeholder="请输入邮寄地址" v-model="form.mailAddress" />
      </view>
      <view class="cu-form-group">
        <view class="title">联系人</view>
        <input placeholder="请输入联系人" v-model="form.contactName" />
      </view>
      <view class="cu-form-group">
        <view class="title">联系电话</view>
        <input placeholder="请输入联系电话" v-model="form.contactPhone" />
      </view>
      <view class="padding flex flex-direction">
        <button class="cu-btn bg-blue lg" @click="submit">保存修改</button>
      </view>
    </form>
  </view>
</template>
<script>
import { myEditInvoice, myInvoiceDetail } from "@/api/invoice"
export default {
  data() {
    return {
      invoiceInfo: {},   // 原始发票信息(只读部分)
      form: {
        invoiceId: null,
        serviceOrderId: null,
        legacyServiceOrderId: null,
        invoiceType: 1,
        invoiceName: '',
        invoiceMoney: '',
        invoiceRemarks: '',
        companyAddress: '',
        companyBank: '',
        companyBankNo: '',
        zipCode: '',
        mailAddress: '',
        contactName: '',
        contactPhone: ''
      }
    }
  },
  computed: {
    taskDisplay() {
      return this.invoiceInfo.serviceCode || this.invoiceInfo.legacyServiceOrderId || '—'
    }
  },
  onLoad(options) {
    if (options && options.invoiceInfo) {
      try {
        const info = JSON.parse(decodeURIComponent(options.invoiceInfo))
        this.invoiceInfo = info
        this.fillForm(info)
      } catch (e) {
        console.error('解析发票信息失败:', e)
        this.$modal.msgError('参数解析失败,请返回重试')
      }
    }
  },
  methods: {
    fillForm(info) {
      this.form.invoiceId        = info.invoiceId
      this.form.serviceOrderId   = info.serviceOrderId
      this.form.legacyServiceOrderId = info.legacyServiceOrderId
      this.form.invoiceType      = info.invoiceType || 1
      this.form.invoiceName      = info.invoiceName || ''
      this.form.invoiceMoney     = info.invoiceMoney != null ? String(info.invoiceMoney) : ''
      this.form.invoiceRemarks   = info.invoiceRemarks || ''
      this.form.companyAddress   = info.companyAddress || ''
      this.form.companyBank      = info.companyBank || ''
      this.form.companyBankNo    = info.companyBankNo || ''
      this.form.zipCode          = info.zipCode || ''
      this.form.mailAddress      = info.mailAddress || ''
      this.form.contactName      = info.contactName || ''
      this.form.contactPhone     = info.contactPhone || ''
    },
    handleTypeChange(e) {
      this.form.invoiceType = e.detail.value
    },
    validateMoney() {
      if (!this.form.invoiceMoney) return
      const money = parseFloat(this.form.invoiceMoney)
      if (isNaN(money) || money <= 0) {
        this.$modal.msgError('请输入有效的金额')
        this.form.invoiceMoney = ''
      }
    },
    submit() {
      if (!this.form.invoiceName) {
        this.$modal.msgError('请输入发票抬头')
        return
      }
      if (!this.form.invoiceMoney) {
        this.$modal.msgError('请输入发票金额')
        return
      }
      const money = parseFloat(this.form.invoiceMoney)
      if (isNaN(money) || money <= 0) {
        this.$modal.msgError('请输入有效的金额')
        return
      }
      myEditInvoice(this.form).then(() => {
        this.$modal.msgSuccess('修改成功')
        setTimeout(() => {
          uni.navigateBack()
        }, 1200)
      }).catch(err => {
        console.error('更新发票失败:', err)
        this.$modal.msgError('保存失败,请重试')
      })
    }
  }
}
</script>
<style lang="scss">
.invoice-apply-container {
  min-height: 100vh;
  padding-bottom: 50rpx;
  .cu-form-group .title {
    min-width: calc(4em + 15px);
    &.required::before {
      content: '*';
      color: #f56c6c;
      margin-right: 4rpx;
      font-weight: bold;
    }
  }
  .selected-task {
    margin: 20rpx 30rpx;
    .label {
      font-size: 28rpx;
      color: #666;
      margin-bottom: 10rpx;
    }
    .task-card {
      padding: 20rpx;
      background: #e8f5e9;
      border-radius: 8rpx;
      border: 2rpx solid #4caf50;
      .card-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      .service-code {
        font-size: 32rpx;
        font-weight: bold;
        color: #2e7d32;
      }
      .task-info-detail {
        margin-top: 16rpx;
        .info-row {
          display: flex;
          margin-bottom: 12rpx;
          line-height: 1.6;
          .info-label {
            font-size: 26rpx;
            color: #666;
            min-width: 140rpx;
            flex-shrink: 0;
          }
          .info-value {
            font-size: 26rpx;
            color: #333;
            flex: 1;
            word-break: break-all;
          }
        }
      }
    }
  }
}
</style>
app/pages/mine/invoice/index.vue
@@ -57,7 +57,7 @@
      </view>
      
      <!-- 发票列表 -->
      <view v-for="(item, index) in list" :key="index" class="invoice-item bg-white margin-sm padding-sm radius shadow">
      <view v-for="(item, index) in list" :key="index" class="invoice-item bg-white margin-sm padding-sm radius shadow" @click="handleDetail(item)">
        <view class="flex justify-between border-bottom padding-bottom-xs margin-bottom-xs">
          <text class="text-bold">服务单号: {{ item.serviceCode || item.legacyServiceOrderId || '未知' }}</text>
          <text :class="item.status === 0 ? 'text-orange' : item.status === 1 ? 'text-green' : item.status === 2 ? 'text-red' : 'text-gray'">{{ item.status === 0 ? '待审核' : item.status === 1 ? '已通过' : item.status === 2 ? '已驳回' : '未知' }}</text>
@@ -79,8 +79,9 @@
          <text class="value">{{ item.invoiceNo }}</text>
        </view>
        
        <view class="action-bar margin-top-sm flex justify-end" v-if="item.invoiceUrl">
          <button class="cu-btn sm bg-green" @click="handleDownload" :data-url="item.invoiceUrl">查看发票</button>
        <view class="action-bar margin-top-sm flex justify-end">
          <button v-if="item.invoiceUrl" class="cu-btn sm bg-green margin-right-xs" @click.stop="handleDownload" :data-url="item.invoiceUrl">查看发票</button>
          <button v-if="item.status === 0 || item.status === 2" class="cu-btn sm bg-blue" @click.stop="handleEdit(item)">编辑</button>
        </view>
        <view v-if="item.status === 2 && item.auditRemarks" class="margin-top-xs padding-xs bg-gray radius">
          <text class="text-sm text-red">驳回原因: {{ item.auditRemarks }}</text>
@@ -165,6 +166,13 @@
    },
    handleApply() {
      this.$tab.navigateTo('/pages/mine/invoice/apply')
    },
    handleDetail(item) {
      this.$tab.navigateTo(`/pages/mine/invoice/detail?invoiceId=${item.invoiceId}`)
    },
    handleEdit(item) {
      const invoiceInfo = encodeURIComponent(JSON.stringify(item))
      this.$tab.navigateTo(`/pages/mine/invoice/edit?invoiceInfo=${invoiceInfo}`)
    },
    handleDownload(e) {
      const url = e.currentTarget.dataset.url
@@ -378,6 +386,10 @@
  }
  
  .invoice-item {
    cursor: pointer;
    transition: box-shadow 0.2s;
    &:active { opacity: 0.85; }
    .border-bottom {
      border-bottom: 1rpx solid #eee;
    }
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysInvoiceController.java
@@ -4,6 +4,10 @@
import java.util.Map;
import java.util.HashMap;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.common.utils.LongUtil;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.service.ISysTaskEmergencyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
@@ -16,6 +20,8 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -38,11 +44,17 @@
@RequestMapping("/system/invoice")
public class SysInvoiceController extends BaseController
{
    private static final Logger log = LoggerFactory.getLogger(SysInvoiceController.class);
    @Autowired
    private ISysInvoiceService sysInvoiceService;
    
    @Autowired
    private ISysDeptService sysDeptService;
    @Autowired
    private ISysTaskEmergencyService sysTaskEmergencyService;
    /**
     * 查询发票申请列表
@@ -182,6 +194,72 @@
    }
    /**
     * App端查看我的某条发票详情(无需后台权限,但只能查看自己的发票)
     */
    @GetMapping("/myDetail/{invoiceId}")
    public AjaxResult myDetail(@PathVariable("invoiceId") Long invoiceId)
    {
        Long currentUserId = SecurityUtils.getUserId();
        SysInvoice invoice = sysInvoiceService.selectSysInvoiceByInvoiceId(invoiceId);
        if (invoice == null) {
            return AjaxResult.error("发票不存在");
        }
        if (!currentUserId.equals(invoice.getApplyUserId())) {
            return AjaxResult.error("无权限查看该发票");
        }
        return AjaxResult.success(invoice);
    }
    /**
     * App端用户编辑自己的发票申请(仅待审核/已驳回可编辑,不可更改关联任务)
     */
    @Log(title = "发票申请自助修改", businessType = BusinessType.UPDATE)
    @PutMapping("/myEdit")
    public AjaxResult myEdit(@RequestBody SysInvoice sysInvoice)
    {
        Long currentUserId = SecurityUtils.getUserId();
        if (sysInvoice.getInvoiceId() == null) {
            return AjaxResult.error("发票ID不能为空");
        }
        SysInvoice exist = sysInvoiceService.selectSysInvoiceByInvoiceId(sysInvoice.getInvoiceId());
        if (exist == null) {
            return AjaxResult.error("发票不存在");
        }
        if (!currentUserId.equals(exist.getApplyUserId())) {
            return AjaxResult.error("无权限修改该发票");
        }
        if (exist.getStatus() != 0 && exist.getStatus() != 2) {
            return AjaxResult.error("只有待审核或已驳回的发票可以修改");
        }
        Long taskId = exist.getServiceOrderId();
        Long legacyServiceOrdId = exist.getLegacyServiceOrderId();
        if(LongUtil.isNotEmpty(taskId) && LongUtil.isEmpty(legacyServiceOrdId)){
           SysTaskEmergency taskEmergency= sysTaskEmergencyService.selectSysTaskEmergencyByTaskId(taskId);
           if(taskEmergency!=null){
               legacyServiceOrdId=taskEmergency.getLegacyServiceOrdId();
           }
        }
        // 保持关联任务信息不变,只更新发票内容字段
        sysInvoice.setServiceOrderId(exist.getServiceOrderId());
        sysInvoice.setLegacyServiceOrderId(legacyServiceOrdId);
        sysInvoice.setApplyUserId(exist.getApplyUserId());
        sysInvoice.setApplyTime(exist.getApplyTime());
        sysInvoice.setStatus(0); // 重新置为待审核
        int rows = sysInvoiceService.updateSysInvoice(sysInvoice);
        if (rows > 0) {
            // 异步同步到旧系统,失败不影响主流程
            try {
                sysInvoiceService.syncUpdateToLegacySystem(sysInvoice.getInvoiceId());
            } catch (Exception e) {
                log.warn("发票编辑同步旧系统失败,不影响保存结果: {}", e.getMessage());
            }
        }
        return toAjax(rows);
    }
    /**
     * 获取可申请发票的任务列表
     * @param searchKeyword 搜索关键词(支持taskCode、serviceCode、legacyServiceOrdNo)
     * @param serviceOrdClass 分公司代码(可选,默认使用用户所属分公司)
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyInvoiceMapper.java
@@ -34,4 +34,11 @@
     * @return
     */
    public List<Map<String, Object>> selectLegacyInvoiceByServiceOrderId(Long serviceOrderId);
    /**
     * 更新旧系统发票记录(编辑同步时使用)
     * @param params
     * @return
     */
    public int updateLegacyInvoice(Map<String, Object> params);
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyTransferSyncMapper.java
@@ -18,12 +18,15 @@
public interface LegacyTransferSyncMapper {
    
    /**
     * 查询指定日期范围的转运单数据
     *
     * 查询指定日期范围的转运单数据(Keyset游标分页,走主键索引)
     *
     * @param startDate 开始日期
     * @param endDate   结束日期
     * @param lastId    上一页最后一条的 ServiceOrdID,首次传 0
     * @param pageSize  每页条数
     * @return 转运单数据列表
     */
    List<Map<String, Object>> selectTransferOrders(@Param("startDate") String startDate, @Param("endDate") String endDate);
    List<Map<String, Object>> selectTransferOrders(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("lastId") long lastId, @Param("pageSize") int pageSize);
    
    /**ServiceOrdNo
     * 根据服务单ID和调度单ID查询转运单数据
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysInvoiceService.java
@@ -76,6 +76,12 @@
    public int syncToLegacySystem(Long invoiceId);
    /**
     * 编辑后同步更新到旧系统(有legacyInvoiceId则UPDATE,无则重新INSERT)
     * @param invoiceId 新系统发票ID
     */
    public void syncUpdateToLegacySystem(Long invoiceId);
    /**
     * 从旧系统同步状态
     */
    public void syncStatusFromLegacySystem();
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
@@ -90,63 +90,79 @@
            String startDateStr = DateUtils.parseDateToStr("yyyy-MM-dd", startDate);
            String endDateStr = DateUtils.parseDateToStr("yyyy-MM-dd", new Date());
            
            // 从SQL Server查询转运单数据
            List<Map<String, Object>> transferOrders = legacyTransferSyncMapper.selectTransferOrders(startDateStr, endDateStr);
            if (transferOrders == null || transferOrders.isEmpty()) {
                log.info("未查询到{}天前的转运单数据", daysAgo);
                return 0;
            }
//            log.info("查询到{}条转运单数据,开始同步...", transferOrders.size());
            // Keyset游标分页从 SQL Server 拉取转运单数据,每页 10 条,走主键索引彻底规避超时
            final int PAGE_SIZE = 10;
            long lastId = 0L;   // 游标:记录上一页最后一条的 ServiceOrdID,首次传 0
            int successCount = 0;
            int totalCount = transferOrders.size();
            int processedCount = 0;
            for (Map<String, Object> order : transferOrders) {
                processedCount++;
                try {
                    Long serviceOrdID = MapValueUtils.getLongValue(order, "ServiceOrdID");
                    Long dispatchOrdID = MapValueUtils.getLongValue(order, "DispatchOrdID");
                    // 检查参数有效性
                    if (serviceOrdID==null || serviceOrdID<=0) {
                        log.warn("第{}条数据服务单ID为空,跳过处理", processedCount);
                        continue;
                    }
//                    log.debug("正在处理第{}/{}条转运单: ServiceOrdID={}, DispatchOrdID={}",
//                             processedCount, totalCount, serviceOrdID, dispatchOrdID);
                    // 检查是否已同步
                    if (isTransferOrderSynced(serviceOrdID, dispatchOrdID)) {
//                        log.debug("转运单已同步,跳过: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                        //进行更新操作
                        updateTransferOrder(serviceOrdID, dispatchOrdID, order);
                        continue;
                    }
                    // 同步单个转运单
                    boolean success = syncSingleTransferOrder(serviceOrdID, dispatchOrdID, order);
                    if (success) {
                        successCount++;
                    }
                    // 控制同步频率,避免请求过快
                    Thread.sleep(100);
                } catch (InterruptedException ie) {
                    log.warn("同步任务被中断");
                    Thread.currentThread().interrupt();
            while (true) {
                List<Map<String, Object>> transferOrders = legacyTransferSyncMapper.selectTransferOrders(startDateStr, endDateStr, lastId, PAGE_SIZE);
                if (transferOrders == null || transferOrders.isEmpty()) {
                    break;
                } catch (Exception e) {
                    log.error("同步单个转运单失败: ServiceOrdID={}, DispatchOrdID={}",
                             MapValueUtils.getStringValue(order, "ServiceOrdID"),
                             MapValueUtils.getStringValue(order, "DispatchOrdID"), e);
                }
                int totalCount = transferOrders.size();
                int processedCount = 0;
                for (Map<String, Object> order : transferOrders) {
                    processedCount++;
                    try {
                        Long serviceOrdID = MapValueUtils.getLongValue(order, "ServiceOrdID");
                        Long dispatchOrdID = MapValueUtils.getLongValue(order, "DispatchOrdID");
                        // 检查参数有效性
                        if (serviceOrdID == null || serviceOrdID <= 0) {
                            log.warn("第{}条数据服务单ID为空,跳过处理", processedCount);
                            continue;
                        }
//                        log.debug("正在处理第{}/{}条转运单: ServiceOrdID={}, DispatchOrdID={}",
//                                 processedCount, totalCount, serviceOrdID, dispatchOrdID);
                        // 检查是否已同步
                        if (isTransferOrderSynced(serviceOrdID, dispatchOrdID)) {
//                            log.debug("转运单已同步,跳过: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                            //进行更新操作
                            updateTransferOrder(serviceOrdID, dispatchOrdID, order);
                            continue;
                        }
                        // 同步单个转运单
                        boolean success = syncSingleTransferOrder(serviceOrdID, dispatchOrdID, order);
                        if (success) {
                            successCount++;
                        }
                        // 控制同步频率,避免请求过快
                        Thread.sleep(100);
                    } catch (InterruptedException ie) {
                        log.warn("同步任务被中断");
                        Thread.currentThread().interrupt();
                        break;
                    } catch (Exception e) {
                        log.error("同步单个转运单失败: ServiceOrdID={}, DispatchOrdID={}",
                                MapValueUtils.getStringValue(order, "ServiceOrdID"),
                                MapValueUtils.getStringValue(order, "DispatchOrdID"), e);
                    }
                }
                // 更新游标为本页最后一条的 ServiceOrdID
                Map<String, Object> lastOrder = transferOrders.get(transferOrders.size() - 1);
                Long lastServiceOrdID = MapValueUtils.getLongValue(lastOrder, "ServiceOrdID");
                if (lastServiceOrdID != null && lastServiceOrdID > 0) {
                    lastId = lastServiceOrdID;
                } else {
                    break;
                }
                // 本页未满一页,说明已无更多数据
                if (totalCount < PAGE_SIZE) {
                    break;
                }
            }
//            log.info("同步完成,共处理{}条转运单,成功同步{}条转运单数据", totalCount, successCount);
//            log.info("同步完成,成功同步{}条转运单数据", successCount);
            return successCount;
            
        } catch (Exception e) {
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysInvoiceServiceImpl.java
@@ -4,6 +4,10 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.ruoyi.common.utils.LongUtil;
import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +41,9 @@
    
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private SysTaskEmergencyMapper sysTaskEmergencyMapper;
    /**
     * 查询发票申请
@@ -83,6 +90,16 @@
    @Override
    public int insertSysInvoice(SysInvoice sysInvoice)
    {
        Long taskId = sysInvoice.getServiceOrderId();
        Long legacyServiceOrdId = sysInvoice.getLegacyServiceOrderId();
        if(LongUtil.isNotEmpty(taskId) && LongUtil.isEmpty(legacyServiceOrdId)){
            SysTaskEmergency taskEmergency= sysTaskEmergencyMapper.selectSysTaskEmergencyByTaskId(taskId);
            if(taskEmergency!=null){
                legacyServiceOrdId=taskEmergency.getLegacyServiceOrdId();
            }
        }
        sysInvoice.setLegacyServiceOrderId(legacyServiceOrdId);
        sysInvoice.setApplyTime(DateUtils.getNowDate());
        sysInvoice.setStatus(0); // 待审核
        sysInvoice.setSyncStatus(0); // 未同步
@@ -191,6 +208,73 @@
    }
    /**
     * 编辑后同步更新到旧系统
     * - 已有 legacyInvoiceId:直接 UPDATE 旧系统记录
     * - 无 legacyInvoiceId(历史同步失败):重新 INSERT
     */
    @Override
    public void syncUpdateToLegacySystem(Long invoiceId) {
        SysInvoice invoice = sysInvoiceMapper.selectSysInvoiceByInvoiceId(invoiceId);
        if (invoice == null) return;
        Map<String, Object> params = new HashMap<>();
        params.put("InvoiceType",         invoice.getInvoiceType());
        params.put("InvoiceName",         invoice.getInvoiceName());
        params.put("InvoiceMakeout",      invoice.getInvoiceRemarks());
        params.put("InvoiceCompanyPhone", invoice.getContactPhone());
        params.put("InvoiceCompanyID",    "");
        params.put("InvoiceCompanyAdd",   invoice.getCompanyAddress());
        params.put("InvoiceCompanyBank",  invoice.getCompanyBank());
        params.put("InvoiceCompanyBankNo",invoice.getCompanyBankNo());
        params.put("InvoiceZipCode",      invoice.getZipCode());
        params.put("Invoice_strAdd",      invoice.getMailAddress());
        params.put("Invoice_strName",     invoice.getContactName());
        params.put("Invoice_strPhone",    invoice.getContactPhone());
        params.put("Invoice_strEmail",    invoice.getContactEmail());
        params.put("ServiceOrderIDPK",   invoice.getLegacyServiceOrderId());
        params.put("InvoiceMoney",        invoice.getInvoiceMoney());
        Integer oaUserId = 0;
        if (invoice.getApplyUserId() != null) {
            SysUser user = sysUserMapper.selectUserById(invoice.getApplyUserId());
            if (user != null && user.getOaUserId() != null) {
                oaUserId = user.getOaUserId();
            }
        }
        params.put("ApplyOAID", oaUserId);
        try {
            if (invoice.getLegacyInvoiceId() != null && invoice.getLegacyInvoiceId() > 0) {
                // 旧系统已有记录,UPDATE
                params.put("InvoiceID", invoice.getLegacyInvoiceId());
                int rows = legacyInvoiceMapper.updateLegacyInvoice(params);
                if (rows > 0) {
                    invoice.setSyncStatus(1);
                    sysInvoiceMapper.updateSysInvoice(invoice);
                    log.info("发票编辑同步旧系统成功(UPDATE), invoiceId={}, legacyId={}", invoiceId, invoice.getLegacyInvoiceId());
                }
            } else {
                // 旧系统无记录,重新 INSERT
                params.put("ServiceOrderIDPK", invoice.getLegacyServiceOrderId());
                int rows = legacyInvoiceMapper.insertLegacyInvoice(params);
                if (rows > 0) {
                    Object legacyId = params.get("InvoiceID");
                    if (legacyId != null) {
                        invoice.setLegacyInvoiceId(Integer.valueOf(legacyId.toString()));
                    }
                    invoice.setSyncStatus(1);
                    sysInvoiceMapper.updateSysInvoice(invoice);
                    log.info("发票编辑同步旧系统成功(INSERT), invoiceId={}", invoiceId);
                }
            }
        } catch (Exception e) {
            log.error("发票编辑同步旧系统异常, invoiceId={}: {}", invoiceId, e.getMessage());
            invoice.setSyncStatus(2);
            sysInvoiceMapper.updateSysInvoice(invoice);
        }
    }
    /**
     * 从旧系统同步发票状态变化
     */
    @Override
ruoyi-system/src/main/resources/mapper/system/LegacyInvoiceMapper.xml
@@ -31,4 +31,25 @@
    <select id="selectLegacyInvoiceByServiceOrderId" parameterType="Long" resultType="Map">
        SELECT * FROM InvoiceData WHERE ServiceOrderIDPK = #{serviceOrderId}
    </select>
    <!-- 编辑时更新旧系统发票记录 -->
    <update id="updateLegacyInvoice" parameterType="Map">
        UPDATE InvoiceData SET
            ServiceOrderIDPK   = #{ServiceOrderIDPK},
            InvoiceType        = #{InvoiceType},
            InvoiceName        = #{InvoiceName},
            InvoiceMakeout     = #{InvoiceMakeout},
            InvoiceCompanyPhone= #{InvoiceCompanyPhone},
            InvoiceCompanyAdd  = #{InvoiceCompanyAdd},
            InvoiceCompanyBank = #{InvoiceCompanyBank},
            InvoiceCompanyBankNo = #{InvoiceCompanyBankNo},
            InvoiceZipCode     = #{InvoiceZipCode},
            Invoice_strAdd     = #{Invoice_strAdd},
            Invoice_strName    = #{Invoice_strName},
            Invoice_strPhone   = #{Invoice_strPhone},
            Invoice_strEmail   = #{Invoice_strEmail},
            InvoiceMoney       = #{InvoiceMoney},
            AuditStatus        = 0
        WHERE InvoiceID = #{InvoiceID}
    </update>
</mapper>
ruoyi-system/src/main/resources/mapper/system/LegacyTransferSyncMapper.xml
@@ -59,9 +59,9 @@
        <result property="EntourageState" column="EntourageState" />
    </resultMap>
    
    <!-- 查询指定日期范围的转运单数据 -->
    <!-- 查询指定日期范围的转运单数据(Keyset游标分页,走主键索引,彻底规避超时) -->
    <select id="selectTransferOrders" resultMap="TransferOrderResult">
        SELECT
        SELECT TOP (${pageSize})
            a.ServiceOrdID,
            a.Old_ServiceOrdID_TXT,
            a.ServiceOrdNo,
@@ -102,15 +102,15 @@
            a.ServiceOrdPtServices,
            a.ServiceOrdPtInServices,
            a.ServiceOrdPtName,
            b.DispatchOrdState,
            b.DispatchOrdNo,
            b.DispatchOrdClass,
            a.ServiceOrdClass
        FROM ServiceOrder as a
        left JOIN DispatchOrd b on a.ServiceOrdID = b.ServiceOrdIDDt
            b.DispatchOrdClass
        FROM ServiceOrder AS a
        LEFT JOIN DispatchOrd b ON a.ServiceOrdID = b.ServiceOrdIDDt
        WHERE a.ServiceOrdState &lt;= 3
            AND a.ServiceOrd_CC_Time > #{startDate} and a.ServiceOrd_CC_Time &lt; #{endDate}
            AND a.ServiceOrd_CC_Time > #{startDate}
            AND a.ServiceOrd_CC_Time &lt; #{endDate}
            AND a.ServiceOrdID > #{lastId}
        ORDER BY a.ServiceOrdID
    </select>
    
    <!-- 根据服务单ID和调度单ID查询转运单数据 -->