| | |
| | | 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' |
| | | }) |
| | | } |
| | |
| | | "style": { |
| | | "navigationBarTitleText": "申请发票" |
| | | } |
| | | }, { |
| | | "path": "pages/mine/invoice/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "编辑发票申请" |
| | | } |
| | | }, { |
| | | "path": "pages/mine/invoice/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "发票详情" |
| | | } |
| | | }], |
| | | "subPackages": [{ |
| | | "root": "pagesTask", |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | }, |
| | | 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 |
| | |
| | | } |
| | | |
| | | .invoice-item { |
| | | cursor: pointer; |
| | | transition: box-shadow 0.2s; |
| | | &:active { opacity: 0.85; } |
| | | |
| | | .border-bottom { |
| | | border-bottom: 1rpx solid #eee; |
| | | } |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | @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; |
| | | |
| | | /** |
| | | * 查询发票申请列表 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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 分公司代码(可选,默认使用用户所属分公司) |
| | |
| | | * @return |
| | | */ |
| | | public List<Map<String, Object>> selectLegacyInvoiceByServiceOrderId(Long serviceOrderId); |
| | | |
| | | /** |
| | | * 更新旧系统发票记录(编辑同步时使用) |
| | | * @param params |
| | | * @return |
| | | */ |
| | | public int updateLegacyInvoice(Map<String, Object> params); |
| | | } |
| | |
| | | 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查询转运单数据 |
| | |
| | | public int syncToLegacySystem(Long invoiceId); |
| | | |
| | | /** |
| | | * 编辑后同步更新到旧系统(有legacyInvoiceId则UPDATE,无则重新INSERT) |
| | | * @param invoiceId 新系统发票ID |
| | | */ |
| | | public void syncUpdateToLegacySystem(Long invoiceId); |
| | | |
| | | /** |
| | | * 从旧系统同步状态 |
| | | */ |
| | | public void syncStatusFromLegacySystem(); |
| | |
| | | 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) { |
| | |
| | | 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; |
| | |
| | | |
| | | @Autowired |
| | | private SysUserMapper sysUserMapper; |
| | | |
| | | @Autowired |
| | | private SysTaskEmergencyMapper sysTaskEmergencyMapper; |
| | | |
| | | /** |
| | | * 查询发票申请 |
| | |
| | | @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); // 未同步 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 编辑后同步更新到旧系统 |
| | | * - 已有 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 |
| | |
| | | <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> |
| | |
| | | <result property="EntourageState" column="EntourageState" /> |
| | | </resultMap> |
| | | |
| | | <!-- 查询指定日期范围的转运单数据 --> |
| | | <!-- 查询指定日期范围的转运单数据(Keyset游标分页,走主键索引,彻底规避超时) --> |
| | | <select id="selectTransferOrders" resultMap="TransferOrderResult"> |
| | | SELECT |
| | | SELECT TOP (${pageSize}) |
| | | a.ServiceOrdID, |
| | | a.Old_ServiceOrdID_TXT, |
| | | a.ServiceOrdNo, |
| | |
| | | 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 <= 3 |
| | | AND a.ServiceOrd_CC_Time > #{startDate} and a.ServiceOrd_CC_Time < #{endDate} |
| | | |
| | | AND a.ServiceOrd_CC_Time > #{startDate} |
| | | AND a.ServiceOrd_CC_Time < #{endDate} |
| | | AND a.ServiceOrdID > #{lastId} |
| | | ORDER BY a.ServiceOrdID |
| | | </select> |
| | | |
| | | <!-- 根据服务单ID和调度单ID查询转运单数据 --> |