From 57e98ac3f59e9ca12d3fdbc6f89c9c0b1f86be4d Mon Sep 17 00:00:00 2001
From: wlzboy <66905212@qq.com>
Date: 星期四, 05 二月 2026 00:49:10 +0800
Subject: [PATCH] feat:增加发票申请
---
app/pages/mine/invoice/index.vue | 406 ++++
app/pages.json | 10
app/pages/mine/invoice/apply.vue | 584 ++++++
app/pages/mine/index.vue | 12
ruoyi-ui/src/api/task.js | 8
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SysInvoiceSyncTask.java | 26
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysInvoiceController.java | 234 ++
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SysInvoiceTask.java | 33
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java | 8
ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyInvoiceMapper.java | 37
ruoyi-ui/src/api/system/dept.js | 8
sql/invoice_menu.sql | 27
sql/invoice_sync_from_legacy.sql | 124 +
ruoyi-ui/src/router/index.js | 21
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysInvoice.java | 380 ++++
ruoyi-ui/src/api/system/invoice.js | 60
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysInvoiceServiceImpl.java | 243 ++
ruoyi-ui/src/views/system/invoice/audit.vue | 311 +++
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java | 49
ruoyi-ui/src/views/task/general/detail.vue | 109 +
ruoyi-admin/src/main/resources/application.yml | 2
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PaymentSyncServiceImpl.java | 6
ruoyi-system/src/main/resources/mapper/system/LegacyInvoiceMapper.xml | 34
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysInvoiceService.java | 91 +
app/api/invoice.js | 36
ruoyi-system/src/main/resources/mapper/system/SysInvoiceMapper.xml | 302 +++
ruoyi-ui/src/views/system/invoice/apply.vue | 261 ++
ruoyi-ui/src/views/task/general/index.vue | 29
sql/invoice_sys.sql | 32
ruoyi-ui/src/views/system/invoice/detail.vue | 279 +++
sql/分区优化方案说明.md | 293 +++
app/pagesTask/detail.vue | 82
sql/invoice_sync_job.sql | 248 ++
ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml | 2
ruoyi-ui/src/views/system/invoice/index.vue | 265 ++
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysInvoiceMapper.java | 91 +
sql/partition_quick_setup.sql | 260 ++
sql/partition_vehicle_gps_segment_mileage.sql | 304 +++
sql/InvoiceData.sql | 31
39 files changed, 5,329 insertions(+), 9 deletions(-)
diff --git a/app/api/invoice.js b/app/api/invoice.js
new file mode 100644
index 0000000..410749e
--- /dev/null
+++ b/app/api/invoice.js
@@ -0,0 +1,36 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鎴戠殑鍙戠エ鐢宠鍒楄〃
+export function listMyInvoice(query) {
+ return request({
+ url: '/system/invoice/myList',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鎻愪氦鍙戠エ鐢宠
+export function addInvoice(data) {
+ return request({
+ url: '/system/invoice',
+ method: 'post',
+ data: data
+ })
+}
+
+// 鑾峰彇鍙紑绁ㄧ殑浠诲姟鍒楄〃
+export function listSelectableTasks(params) {
+ return request({
+ url: '/system/invoice/selectableTasks',
+ method: 'get',
+ params: params
+ })
+}
+
+// 妫�鏌ヤ换鍔℃槸鍚﹀凡鐢宠鍙戠エ
+export function checkTaskInvoice(taskId) {
+ return request({
+ url: `/system/invoice/checkTaskInvoice/${taskId}`,
+ method: 'get'
+ })
+}
diff --git a/app/pages.json b/app/pages.json
index bb9fff8..e6b4e4e 100644
--- a/app/pages.json
+++ b/app/pages.json
@@ -110,6 +110,16 @@
"style": {
"navigationBarTitleText": "娑堟伅涓績"
}
+ }, {
+ "path": "pages/mine/invoice/index",
+ "style": {
+ "navigationBarTitleText": "鎴戠殑鍙戠エ"
+ }
+ }, {
+ "path": "pages/mine/invoice/apply",
+ "style": {
+ "navigationBarTitleText": "鐢宠鍙戠エ"
+ }
}],
"subPackages": [{
"root": "pagesTask",
diff --git a/app/pages/mine/index.vue b/app/pages/mine/index.vue
index e6ccd74..130f57e 100644
--- a/app/pages/mine/index.vue
+++ b/app/pages/mine/index.vue
@@ -58,6 +58,14 @@
<view>{{ boundVehicle && boundVehicle !== '鏈粦瀹�' ? '鏇存崲杞﹁締' : '缁戝畾杞﹁締' }}</view>
</view>
</view>
+
+ <!-- 鎴戠殑鍙戠エ -->
+ <view class="list-cell list-cell-arrow" @click="handleToInvoice">
+ <view class="menu-item-box">
+ <view class="iconfont icon-edit menu-icon"></view>
+ <view>鎴戠殑鍙戠エ</view>
+ </view>
+ </view>
<!-- 閫�鍑虹櫥褰� -->
<view class="list-cell list-cell-arrow" @click="handleLogout">
@@ -144,6 +152,10 @@
this.$tab.navigateTo('/pages/bind-vehicle')
},
+ handleToInvoice() {
+ this.$tab.navigateTo('/pages/mine/invoice/index')
+ },
+
handleToInfo() {
this.$tab.navigateTo('/pages/mine/info/index')
},
diff --git a/app/pages/mine/invoice/apply.vue b/app/pages/mine/invoice/apply.vue
new file mode 100644
index 0000000..0c40475
--- /dev/null
+++ b/app/pages/mine/invoice/apply.vue
@@ -0,0 +1,584 @@
+<template>
+ <view class="invoice-apply-container bg-white">
+ <form>
+ <view class="cu-form-group margin-top">
+ <view class="title required">閫夋嫨浠诲姟</view>
+ <view class="combo-box-wrapper">
+ <input
+ class="combo-input"
+ placeholder="璇疯緭鍏ユ湇鍔″崟鍙锋垨浠诲姟缂栧彿鎼滅储"
+ v-model="searchKeyword"
+ @input="handleInput"
+ @focus="handleFocus"
+ @blur="handleBlur"
+ />
+ <text class="cuIcon-search search-icon"></text>
+
+ <!-- 涓嬫媺鍒楄〃 -->
+ <view class="dropdown-list" v-if="showDropdown && filteredTaskList.length > 0">
+ <view class="dropdown-item"
+ v-for="task in filteredTaskList"
+ :key="task.taskId"
+ @click="selectTask(task)"
+ >
+ <view class="item-main">
+ <text class="service-code">{{ task.serviceCode || task.taskCode }}</text>
+ <text class="task-code">{{ task.taskCode }}</text>
+ </view>
+ <view class="item-sub">
+
+ <text class="text-gray margin-left-sm">{{ task.completionTime }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鏃犵粨鏋滄彁绀� -->
+ <view class="dropdown-list" v-if="showDropdown && searchKeyword && filteredTaskList.length === 0 && !loading">
+ <view class="empty-result">
+ <text class="text-gray">鏈壘鍒板尮閰嶇殑浠诲姟</text>
+ </view>
+ </view>
+
+ <!-- 鍔犺浇鎻愮ず -->
+ <view class="dropdown-list" v-if="showDropdown && loading">
+ <view class="loading-result">
+ <text class="cuIcon-loading2 cu-spin"></text>
+ <text class="text-gray margin-left-sm">鎼滅储涓�...</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 宸查�変换鍔℃樉绀� -->
+ <view class="selected-task" v-if="form.serviceOrderId">
+ <view class="label">宸查�変换鍔�</view>
+ <view class="task-card">
+ <view class="card-header">
+ <text class="service-code">{{ selectedTask.serviceCode || selectedTask.taskCode }}</text>
+ <text class="cuIcon-close remove-btn" @click="clearSelection"></text>
+ </view>
+ <view class="task-info-detail">
+ <view class="info-row">
+ <text class="info-label">鏈嶅姟鍗曞彿:</text>
+ <text class="info-value">{{ selectedTask.legacyServiceOrderId }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">鍑哄彂鍦�:</text>
+ <text class="info-value">{{ selectedTask.departure || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">鐩殑鍦�:</text>
+ <text class="info-value">{{ selectedTask.destination || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">瀹屾垚鏃堕棿:</text>
+ <text class="info-value">{{ selectedTask.completionTime }}</text>
+ </view>
+ <view class="info-row" v-if="selectedTask.transferPrice">
+ <text class="info-label">浠诲姟閲戦:</text>
+ <text class="info-value text-price">锟{ selectedTask.transferPrice }}</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="money-hint" v-if="selectedTask && selectedTask.transferPrice">
+ <text class="text-gray text-sm">鏈�澶у彲寮�绁ㄩ噾棰�: 锟{ selectedTask.transferPrice }}</text>
+ </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 { addInvoice, listSelectableTasks } from "@/api/invoice"
+
+export default {
+ data() {
+ return {
+ searchKeyword: '',
+ filteredTaskList: [],
+ selectedTask: null,
+ showDropdown: false,
+ loading: false,
+ searchTimer: null,
+ maxInvoiceMoney: null, // 鏈�澶у彲寮�绁ㄩ噾棰�
+ form: {
+ serviceOrderId: null,
+ legacyServiceOrderId: null,
+ invoiceType: 1,
+ invoiceName: '',
+ invoiceMoney: '',
+ invoiceRemarks: '',
+ companyAddress: '',
+ companyBank: '',
+ companyBankNo: '',
+ zipCode: '',
+ mailAddress: '',
+ contactName: '',
+ contactPhone: ''
+ },
+ // 浠庝换鍔¤鎯呴〉浼犲叆鐨勪换鍔′俊鎭�
+ taskInfoFromDetail: null
+ }
+ },
+ onLoad(options) {
+ // 妫�鏌ユ槸鍚︿粠浠诲姟璇︽儏椤典紶鍏ヤ簡浠诲姟淇℃伅
+ if (options && options.taskInfo) {
+ try {
+ const taskInfo = JSON.parse(decodeURIComponent(options.taskInfo));
+ this.taskInfoFromDetail = taskInfo;
+ this.preFillTaskInfo();
+ } catch (e) {
+ console.error('瑙f瀽浠诲姟淇℃伅澶辫触:', e);
+ }
+ }
+ // 涓嶅啀鑷姩鍔犺浇浠诲姟鍒楄〃
+ },
+ methods: {
+ // 棰勫~鍏呬换鍔′俊鎭�
+ preFillTaskInfo() {
+ if (!this.taskInfoFromDetail) return;
+
+ const taskInfo = this.taskInfoFromDetail;
+
+ // 濉厖琛ㄥ崟
+ this.form.serviceOrderId = taskInfo.taskId;
+ // 鍚屾椂璁剧疆鏃х郴缁熸湇鍔″崟ID
+ if (taskInfo.legacyServiceOrderId) {
+ this.form.legacyServiceOrderId = taskInfo.legacyServiceOrderId;
+ }
+ this.selectedTask = { ...taskInfo };
+
+ // 璁剧疆鏈�澶у彲寮�绁ㄩ噾棰�
+ if (taskInfo.transferPrice) {
+ this.maxInvoiceMoney = parseFloat(taskInfo.transferPrice);
+ // 鑷姩濉叆浠诲姟閲戦
+ this.form.invoiceMoney = this.maxInvoiceMoney.toString();
+ }
+
+ console.log('浠诲姟淇℃伅宸查濉厖:', taskInfo);
+ },
+
+ handleInput(e) {
+ const keyword = e.detail.value.trim()
+
+ // 娓呴櫎涔嬪墠鐨勫畾鏃跺櫒
+ if (this.searchTimer) {
+ clearTimeout(this.searchTimer)
+ }
+
+ if (!keyword) {
+ this.showDropdown = false
+ this.filteredTaskList = []
+ return
+ }
+
+ // 闃叉姈锛氬欢杩�300ms鎼滅储
+ this.searchTimer = setTimeout(() => {
+ this.searchTasks(keyword)
+ }, 300)
+ },
+
+ handleFocus() {
+ // 濡傛灉鏈夎緭鍏ュ唴瀹癸紝鏄剧ず涓嬫媺鍒楄〃
+ if (this.searchKeyword && this.searchKeyword.trim()) {
+ this.showDropdown = true
+ }
+ },
+
+ handleBlur() {
+ // 寤惰繜鍏抽棴涓嬫媺鍒楄〃锛岀‘淇濈偣鍑讳簨浠惰兘瑙﹀彂
+ setTimeout(() => {
+ this.showDropdown = false
+ }, 200)
+ },
+
+ searchTasks(keyword) {
+ this.loading = true
+ this.showDropdown = true
+
+ listSelectableTasks({
+ searchKeyword: keyword
+ }).then(res => {
+ this.filteredTaskList = this.formatTaskList(res.data)
+ }).catch(err => {
+ this.$modal.msgError('鎼滅储澶辫触锛岃閲嶈瘯')
+ this.filteredTaskList = []
+ }).finally(() => {
+ this.loading = false
+ })
+ },
+ formatTaskList(data) {
+ return data.map(item => {
+ // 瀹屾暣鏄剧ず yyyy-MM-dd HH:mm
+ const time = item.completionTime ? item.completionTime.substring(0, 16) : '';
+ // 鍏煎涓ょ瀛楁鍚嶏細transferPrice 鍜� transfer_price
+ const transferPrice = item.transferPrice !== undefined ? item.transferPrice : item.transfer_price;
+ return {
+ taskId: item.taskId,
+ taskCode: item.taskCode,
+ serviceCode: item.serviceCode,
+ legacyServiceOrderId: item.legacyServiceOrderId || item.taskCode,
+ completionTime: time,
+ departure: item.departure,
+ destination: item.destination,
+ transferPrice: transferPrice
+ }
+ })
+ },
+ handleSearch() {
+ if (!this.searchKeyword || this.searchKeyword.trim() === '') {
+ // 鏈緭鍏ユ椂鏄剧ず鎵�鏈�
+ this.getCompletedTasks()
+ } else {
+ // 璋冪敤鍚庣鎼滅储鎺ュ彛
+ listSelectableTasks({
+ searchKeyword: this.searchKeyword.trim()
+ }).then(res => {
+ this.filteredTaskList = this.formatTaskList(res.data)
+ })
+ }
+ },
+ selectTask(task) {
+ console.log('閫変腑鐨勪换鍔�:', task)
+ console.log('浠诲姟閲戦:', task.transferPrice)
+
+ this.selectedTask = task
+ // serviceOrderId 瀛樺偍鏃х郴缁熸湇鍔″崟ID
+ this.form.serviceOrderId = task.taskId;
+ this.form.legacyServiceOrderId = task.legacyServiceOrderId
+ // 閫変腑鍚庢樉绀烘湇鍔″崟鍙凤紝鍏抽棴涓嬫媺
+ this.searchKeyword = task.serviceCode || task.taskCode
+ this.showDropdown = false
+
+ // 鑷姩甯﹀叆浠诲姟閲戦鍒板彂绁ㄩ噾棰�
+ if (task.transferPrice !== null && task.transferPrice !== undefined) {
+ // 纭繚閲戦鏄暟瀛楃被鍨�
+ const price = Number(task.transferPrice)
+ if (!isNaN(price) && price > 0) {
+ this.form.invoiceMoney = price.toString()
+ this.maxInvoiceMoney = price
+ console.log('宸茶缃彂绁ㄩ噾棰�:', this.form.invoiceMoney)
+ } else {
+ console.log('浠诲姟閲戦鏃犳晥:', task.transferPrice)
+ }
+ } else {
+ console.log('浠诲姟閲戦涓虹┖')
+ }
+ },
+
+ clearSelection() {
+ this.selectedTask = null
+ this.form.serviceOrderId = null
+ this.form.legacyServiceOrderId = null
+ this.form.invoiceMoney = ''
+ this.searchKeyword = ''
+ this.filteredTaskList = []
+ this.maxInvoiceMoney = null
+ },
+
+ validateMoney() {
+ if (!this.form.invoiceMoney) return
+
+ const money = parseFloat(this.form.invoiceMoney)
+
+ if (isNaN(money) || money <= 0) {
+ this.$modal.msgError('璇疯緭鍏ユ湁鏁堢殑閲戦')
+ this.form.invoiceMoney = ''
+ return
+ }
+
+ // 鏍¢獙鏄惁瓒呰繃浠诲姟閲戦
+ if (this.maxInvoiceMoney && money > this.maxInvoiceMoney) {
+ this.$modal.msgError(`鍙戠エ閲戦涓嶈兘瓒呰繃浠诲姟閲戦锟�${this.maxInvoiceMoney}`)
+ this.form.invoiceMoney = this.maxInvoiceMoney.toString()
+ }
+ },
+ handleTypeChange(e) {
+ this.form.invoiceType = e.detail.value
+ },
+ submit() {
+ if (!this.form.serviceOrderId) {
+ this.$modal.msgError("璇烽�夋嫨浠诲姟")
+ return
+ }
+ 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
+ }
+ if (this.maxInvoiceMoney && money > this.maxInvoiceMoney) {
+ this.$modal.msgError(`鍙戠エ閲戦涓嶈兘瓒呰繃浠诲姟閲戦锟�${this.maxInvoiceMoney}`)
+ return
+ }
+
+ addInvoice(this.form).then(res => {
+ this.$modal.msgSuccess("鎻愪氦鎴愬姛")
+ setTimeout(() => {
+ // 璺宠浆鍒板彂绁ㄥ垪琛ㄩ〉闈�
+ uni.redirectTo({
+ url: '/pages/mine/invoice/index'
+ })
+ }, 1500)
+ })
+ }
+ }
+}
+</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;
+ }
+ }
+
+ // ComboBox鏍峰紡
+ .combo-box-wrapper {
+ position: relative;
+ flex: 1;
+
+ .combo-input {
+ width: 100%;
+ padding-right: 60rpx;
+ }
+
+ .search-icon {
+ position: absolute;
+ right: 20rpx;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #8799a3;
+ font-size: 36rpx;
+ }
+
+ .dropdown-list {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ max-height: 500rpx;
+ overflow-y: auto;
+ background: white;
+ border: 2rpx solid #e1e1e1;
+ border-radius: 8rpx;
+ margin-top: 10rpx;
+ box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
+ z-index: 999;
+
+ .dropdown-item {
+ padding: 20rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+ transition: background 0.2s;
+
+ &:active {
+ background: #f5f5f5;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .item-main {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8rpx;
+
+ .service-code {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #1976d2;
+ }
+
+ .task-code {
+ font-size: 22rpx;
+ color: #999;
+ }
+ }
+
+ .item-sub {
+ display: flex;
+ font-size: 24rpx;
+ color: #666;
+ }
+ }
+
+ .empty-result, .loading-result {
+ padding: 40rpx 20rpx;
+ text-align: center;
+ color: #999;
+ }
+
+ .loading-result {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+ }
+
+ .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;
+ margin-bottom: 20rpx;
+ padding-bottom: 16rpx;
+ border-bottom: 1rpx solid #c8e6c9;
+ }
+
+ .service-code {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #2e7d32;
+ }
+
+ .remove-btn {
+ font-size: 40rpx;
+ color: #999;
+ padding: 10rpx;
+ }
+
+ .task-info-detail {
+ .info-row {
+ display: flex;
+ margin-bottom: 12rpx;
+ line-height: 1.6;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .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;
+
+ &.text-price {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .money-hint {
+ margin: -10rpx 30rpx 20rpx 30rpx;
+ padding-left: calc(4em + 15px);
+ }
+}
+</style>
diff --git a/app/pages/mine/invoice/index.vue b/app/pages/mine/invoice/index.vue
new file mode 100644
index 0000000..d6b6018
--- /dev/null
+++ b/app/pages/mine/invoice/index.vue
@@ -0,0 +1,406 @@
+<template>
+ <view class="invoice-list-container">
+ <view class="add-btn-box">
+ <button class="cu-btn block bg-blue lg" @click="handleApply">鐢宠鍙戠エ</button>
+ </view>
+
+ <!-- 鎼滅储妗� -->
+ <view class="search-box">
+ <view class="search-input-wrapper">
+ <input
+ class="search-input"
+ placeholder="璇疯緭鍏ユ湇鍔″崟鍙锋悳绱�"
+ v-model="searchKeyword"
+ @confirm="handleSearch"
+ />
+ <text class="cuIcon-search search-icon" @click="handleSearch"></text>
+ <text v-if="searchKeyword" class="cuIcon-close clear-icon" @click="clearSearch"></text>
+ </view>
+ </view>
+
+ <!-- 鐘舵�乀ab -->
+ <view class="tabs-wrapper">
+ <view
+ class="tab-item"
+ :class="currentTab === null ? 'active' : ''"
+ @click="switchTab(null)"
+ >
+ <text>鍏ㄩ儴</text>
+ </view>
+ <view
+ class="tab-item"
+ :class="currentTab === 0 ? 'active' : ''"
+ @click="switchTab(0)"
+ >
+ <text>寰呭鏍�</text>
+ </view>
+ <view
+ class="tab-item"
+ :class="currentTab === 1 ? 'active' : ''"
+ @click="switchTab(1)"
+ >
+ <text>宸查�氳繃</text>
+ </view>
+ <view
+ class="tab-item"
+ :class="currentTab === 2 ? 'active' : ''"
+ @click="switchTab(2)"
+ >
+ <text>宸查┏鍥�</text>
+ </view>
+ </view>
+
+ <scroll-view scroll-y="true" class="list-scroll" @scrolltolower="loadMore">
+ <!-- 绌哄垪琛ㄦ彁绀� -->
+ <view v-if="list.length === 0" class="empty-box">
+ <text class="text-gray">鏆傛棤鍙戠エ鐢宠璁板綍</text>
+ </view>
+
+ <!-- 鍙戠エ鍒楄〃 -->
+ <view v-for="(item, index) in list" :key="index" class="invoice-item bg-white margin-sm padding-sm radius shadow">
+ <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>
+ </view>
+ <view class="info-row">
+ <text class="label">鍙戠エ鎶ご:</text>
+ <text class="value">{{ item.invoiceName }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">鐢宠閲戦:</text>
+ <text class="value text-red">锟{ item.invoiceMoney ? Number(item.invoiceMoney).toFixed(2) : '0.00' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">鐢宠鏃堕棿:</text>
+ <text class="value">{{ formatApplyTime(item.applyTime) }}</text>
+ </view>
+ <view v-if="item.invoiceNo" class="info-row">
+ <text class="label">鍙戠エ缂栧彿:</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>
+ <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>
+ </view>
+ </view>
+ </scroll-view>
+ </view>
+</template>
+
+<script>
+import { listMyInvoice } from "@/api/invoice"
+import config from '@/config.js'
+
+export default {
+ data() {
+ return {
+ list: [],
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ serviceCode: null, // 鏈嶅姟鍗曞彿鎼滅储
+ status: null // 鐘舵�佺瓫閫�
+ },
+ total: 0,
+ searchKeyword: '', // 鎼滅储鍏抽敭璇�
+ currentTab: null // 褰撳墠Tab锛歯ull=鍏ㄩ儴, 0=寰呭鏍�, 1=宸查�氳繃, 2=宸查┏鍥�
+ }
+ },
+ onShow() {
+ this.queryParams.pageNum = 1
+ this.list = []
+ this.getList()
+ },
+ methods: {
+ getList() {
+ listMyInvoice(this.queryParams).then(res => {
+ if (res.rows) {
+ let rows = res.rows;
+ // 鎸夌敵璇锋椂闂村�掑簭鎺掑簭
+ rows.sort((a, b) => {
+ // 濡傛灉鏈� applyTime锛屽垯姣旇緝鏃堕棿锛屽惁鍒欐斁鍦ㄦ渶鍚�
+ if (!a.applyTime) return 1;
+ if (!b.applyTime) return -1;
+ return new Date(b.applyTime) - new Date(a.applyTime);
+ });
+ this.list = this.list.concat(rows);
+ this.total = res.total || 0;
+ }
+ }).catch(err => {
+ console.error('鑾峰彇鍙戠エ鍒楄〃澶辫触:', err)
+ this.$modal.msgError('鍔犺浇澶辫触锛岃閲嶈瘯')
+ })
+ },
+ // 鎼滅储
+ handleSearch() {
+ this.queryParams.serviceCode = this.searchKeyword.trim() || null
+ this.resetList()
+ },
+ // 娓呯┖鎼滅储
+ clearSearch() {
+ this.searchKeyword = ''
+ this.queryParams.serviceCode = null
+ this.resetList()
+ },
+ // 鍒囨崲Tab
+ switchTab(status) {
+ this.currentTab = status
+ this.queryParams.status = status
+ this.resetList()
+ },
+ // 閲嶇疆鍒楄〃
+ resetList() {
+ this.queryParams.pageNum = 1
+ this.list = []
+ this.getList()
+ },
+ loadMore() {
+ if (this.list.length < this.total) {
+ this.queryParams.pageNum++
+ this.getList()
+ }
+ },
+ handleApply() {
+ this.$tab.navigateTo('/pages/mine/invoice/apply')
+ },
+ handleDownload(e) {
+ const url = e.currentTarget.dataset.url
+ console.log('=== 鍙戠エ涓嬭浇璋冭瘯淇℃伅 ===');
+ console.log('1. 鍘熷URL:', url)
+
+ if (!url) {
+ this.$modal.msgError('鍙戠エ鏂囦欢鍦板潃涓虹┖')
+ return
+ }
+
+ // 澶勭悊URL锛岀‘淇濇槸瀹屾暣鐨勫湴鍧�
+ let fullUrl = url
+ if (!url.startsWith('http')) {
+ // 濡傛灉鏄浉瀵硅矾寰勶紝鎷兼帴瀹屾暣鍦板潃
+ fullUrl = config.baseUrl + url
+ }
+
+ console.log('2. baseUrl:', config.baseUrl)
+ console.log('3. 瀹屾暣URL:', fullUrl)
+
+ const fileExt = fullUrl.match(/\.(\w+)$/)?.[1]?.toLowerCase()
+ console.log('4. 鏂囦欢鎵╁睍鍚�:', fileExt)
+
+ // #ifdef H5
+ console.log('5. H5鐜锛岀洿鎺ユ墦寮�')
+ window.open(fullUrl)
+ // #endif
+
+ // #ifndef H5
+ console.log('5. 灏忕▼搴�/App鐜')
+
+ // 鍏堥瑙堝浘鐗囷紝濡傛灉鏄浘鐗囨牸寮�
+ const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
+ if (imageExts.includes(fileExt)) {
+ console.log('6. 鍥剧墖鏂囦欢锛屼娇鐢╬reviewImage')
+ uni.previewImage({
+ urls: [fullUrl],
+ current: fullUrl,
+ success: () => {
+ console.log('鍥剧墖棰勮鎴愬姛')
+ },
+ fail: (err) => {
+ console.error('鍥剧墖棰勮澶辫触:', err)
+ this.$modal.msgError(`鏃犳硶棰勮鍥剧墖: ${err.errMsg || '鏈煡閿欒'}`)
+ }
+ })
+ return
+ }
+
+ // PDF鏂囦欢涓嬭浇骞舵墦寮�
+ console.log('6. PDF鏂囦欢锛屼娇鐢╠ownloadFile')
+ uni.showLoading({ title: '涓嬭浇涓�...', mask: true })
+
+ uni.downloadFile({
+ url: fullUrl,
+ success: (res) => {
+ console.log('7. 涓嬭浇鎴愬姛:', res)
+ uni.hideLoading()
+
+ if (res.statusCode === 200) {
+ console.log('8. 鎵撳紑鏂囨。:', res.tempFilePath)
+ uni.openDocument({
+ filePath: res.tempFilePath,
+ showMenu: true,
+ success: function (openRes) {
+ console.log('9. 鏂囨。鎵撳紑鎴愬姛:', openRes)
+ },
+ fail: function (err) {
+ console.error('9. 鏂囨。鎵撳紑澶辫触:', err)
+ uni.showModal({
+ title: '鎻愮ず',
+ content: `鏃犳硶鎵撳紑鏂囦欢: ${err.errMsg || '鏈煡閿欒'}`,
+ showCancel: false
+ })
+ }
+ })
+ } else {
+ console.error('8. 涓嬭浇澶辫触锛岀姸鎬佺爜:', res.statusCode)
+ this.$modal.msgError(`涓嬭浇澶辫触锛岀姸鎬佺爜: ${res.statusCode}`)
+ }
+ },
+ fail: (err) => {
+ console.error('7. 涓嬭浇澶辫触:', err)
+ uni.hideLoading()
+ uni.showModal({
+ title: '涓嬭浇澶辫触',
+ content: `閿欒淇℃伅: ${err.errMsg || '鏈煡閿欒'}\n\n璇锋鏌�:\n1. 缃戠粶杩炴帴鏄惁姝e父\n2. 鏂囦欢鏄惁瀛樺湪\n3. 鏈嶅姟鍣ㄦ槸鍚﹀彲璁块棶`,
+ showCancel: false
+ })
+ }
+ })
+ // #endif
+ console.log('=== 璋冭瘯淇℃伅缁撴潫 ===');
+ },
+ statusText(status) {
+ const map = { 0: '寰呭鏍�', 1: '宸查�氳繃', 2: '宸查┏鍥�' }
+ return map[status] || '鏈煡'
+ },
+ statusClass(status) {
+ const map = { 0: 'text-orange', 1: 'text-green', 2: 'text-red' }
+ return map[status] || 'text-gray'
+ },
+ formatMoney(money) {
+ if (money === null || money === undefined) return '0.00'
+ return Number(money).toFixed(2)
+ },
+ // 鏍煎紡鍖栫敵璇锋椂闂�
+ formatApplyTime(time) {
+ if (!time) return ''
+ // 灏嗘椂闂村瓧绗︿覆鏍煎紡鍖栦负 yyyy-MM-dd HH:mm
+ const date = new Date(time)
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ const hours = String(date.getHours()).padStart(2, '0')
+ const minutes = String(date.getMinutes()).padStart(2, '0')
+ return `${year}-${month}-${day} ${hours}:${minutes}`
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+.invoice-list-container {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-color: #f8f8f8;
+
+ .add-btn-box {
+ padding: 20rpx;
+ background-color: #fff;
+ }
+
+ // 鎼滅储妗嗘牱寮�
+ .search-box {
+ padding: 20rpx;
+ background-color: #fff;
+ border-bottom: 1rpx solid #e5e5e5;
+
+ .search-input-wrapper {
+ position: relative;
+ background: #f5f5f5;
+ border-radius: 40rpx;
+ padding: 0 80rpx 0 40rpx;
+
+ .search-input {
+ height: 70rpx;
+ line-height: 70rpx;
+ font-size: 28rpx;
+ }
+
+ .search-icon {
+ position: absolute;
+ right: 40rpx;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #999;
+ font-size: 36rpx;
+ }
+
+ .clear-icon {
+ position: absolute;
+ right: 80rpx;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #999;
+ font-size: 32rpx;
+ padding: 10rpx;
+ }
+ }
+ }
+
+ // Tab鏍峰紡
+ .tabs-wrapper {
+ display: flex;
+ background-color: #fff;
+ border-bottom: 1rpx solid #e5e5e5;
+
+ .tab-item {
+ flex: 1;
+ text-align: center;
+ padding: 25rpx 0;
+ font-size: 28rpx;
+ color: #666;
+ position: relative;
+
+ &.active {
+ color: #0081ff;
+ font-weight: bold;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 60rpx;
+ height: 4rpx;
+ background-color: #0081ff;
+ border-radius: 2rpx;
+ }
+ }
+ }
+ }
+
+ .list-scroll {
+ flex: 1;
+ overflow: hidden;
+ }
+
+ .invoice-item {
+ .border-bottom {
+ border-bottom: 1rpx solid #eee;
+ }
+
+ .info-row {
+ display: flex;
+ line-height: 1.8;
+ font-size: 26rpx;
+
+ .label {
+ color: #666;
+ width: 140rpx;
+ }
+ .value {
+ color: #333;
+ flex: 1;
+ }
+ }
+ }
+
+ .empty-box {
+ padding: 100rpx;
+ text-align: center;
+ }
+}
+</style>
diff --git a/app/pagesTask/detail.vue b/app/pagesTask/detail.vue
index 38a10c8..4bd3c08 100644
--- a/app/pagesTask/detail.vue
+++ b/app/pagesTask/detail.vue
@@ -237,7 +237,17 @@
<!-- 杞繍 - 璐圭敤淇℃伅 -->
<view class="detail-section" v-if="taskDetail.taskType === 'EMERGENCY_TRANSFER' && taskDetail.emergencyInfo">
- <view class="section-title">璐圭敤淇℃伅</view>
+ <view class="section-title">
+ 璐圭敤淇℃伅
+ <!-- 宸插畬鎴愪笖鏈敵璇峰彂绁ㄦ椂鏄剧ず鐢宠鍙戠エ鎸夐挳 -->
+ <button
+ v-if="canApplyInvoice"
+ class="apply-invoice-btn"
+ @click="handleApplyInvoice"
+ >
+ <text class="cuIcon-form"></text> 鐢宠鍙戠エ
+ </button>
+ </view>
<view class="info-item" v-if="taskDetail.emergencyInfo.transferDistance">
<view class="label">杞繍鍏噷鏁�</view>
<view class="value">{{ taskDetail.emergencyInfo.transferDistance }}鍏噷</view>
@@ -566,6 +576,7 @@
import { checkVehicleActiveTasks } from '@/api/task'
import { getPaymentInfo } from '@/api/payment'
import { getDicts } from '@/api/dict'
+ import { checkTaskInvoice } from '@/api/invoice'
import { formatDateTime } from '@/utils/common'
import { validateTaskForDepart, validateTaskForSettlement, getTaskVehicleId, checkTaskCanDepart } from '@/utils/taskValidator'
import AttachmentUpload from './components/AttachmentUpload.vue'
@@ -587,7 +598,9 @@
forceCompleteForm: {
actualStartTime: '',
actualEndTime: ''
- }
+ },
+ hasInvoiceApplied: false, // 鏄惁宸茬敵璇峰彂绁�
+ invoiceStatus: null // 鍙戠エ鐘舵�侊細0-寰呭鏍�, 1-宸查�氳繃, 2-宸查┏鍥�
}
},
computed: {
@@ -597,6 +610,16 @@
return false
}
return ['COMPLETED', 'CANCELLED'].includes(this.taskDetail.taskStatus)
+ },
+
+ // 鏄惁鍙互鐢宠鍙戠エ
+ canApplyInvoice() {
+ // 浠呮�ユ晳杞繍浠诲姟
+ if (this.taskDetail?.taskType !== 'EMERGENCY_TRANSFER') return false
+ // 浠诲姟蹇呴』宸插畬鎴�
+ if (this.taskDetail?.taskStatus !== 'COMPLETED') return false
+ // 鏈敵璇疯繃鍙戠エ锛屾垨鏇捐椹冲洖
+ return !this.hasInvoiceApplied || this.invoiceStatus === 2
},
// 鐢熸垚鎵ц浜哄憳瑙掕壊鏍囩鐨勭被鍚�
@@ -699,6 +722,8 @@
this.taskId = options.id
this.loadTaskDetail()
this.loadCancelReasonDict() // 鍔犺浇鍙栨秷鍘熷洜瀛楀吀
+ // 妫�鏌ュ彂绁ㄧ敵璇风姸鎬�
+ this.checkInvoiceStatus()
},
onShow() {
// 姣忔椤甸潰鏄剧ず鏃堕噸鏂板姞杞芥暟鎹紝纭繚浠庣紪杈戦〉闈㈣繑鍥炲悗鑳界湅鍒版渶鏂版暟鎹�
@@ -1058,6 +1083,45 @@
}
return null;
+ },
+
+ // 妫�鏌ュ彂绁ㄧ敵璇风姸鎬�
+ checkInvoiceStatus() {
+ if (!this.taskId) return;
+
+ // 璋冪敤鍚庣鎺ュ彛妫�鏌ヨ浠诲姟鏄惁宸茬敵璇峰彂绁�
+ checkTaskInvoice(this.taskId).then(response => {
+ if (response.code === 200 && response.data) {
+ this.hasInvoiceApplied = true;
+ this.invoiceStatus = response.data.status;
+ }
+ }).catch(error => {
+ console.error('妫�鏌ュ彂绁ㄧ敵璇风姸鎬佸け璐�:', error);
+ // 蹇界暐閿欒锛岄粯璁ゆ湭鐢宠
+ });
+ },
+
+ // 鐢宠鍙戠エ
+ handleApplyInvoice() {
+ // 鍑嗗浠诲姟淇℃伅
+ const taskInfo = {
+ taskId: this.taskDetail.taskId,
+ taskCode: this.taskDetail.showTaskCode || this.taskDetail.taskCode,
+ legacyServiceOrderId: this.taskDetail.emergencyInfo?.legacyServiceOrdId,
+ serviceCode: this.taskDetail.emergencyInfo?.serviceCode,
+ departure: this.taskDetail.departureAddress,
+ destination: this.taskDetail.destinationAddress,
+ completionTime: this.formatTime(this.taskDetail.actualEndTime),
+ transferPrice: this.paymentInfo?.transferPrice || this.paymentInfo?.totalAmount
+ };
+
+ // 灏嗕换鍔′俊鎭簭鍒楀寲涓� URL 鍙傛暟
+ const taskInfoParam = encodeURIComponent(JSON.stringify(taskInfo));
+
+ // 璺宠浆鍒板彂绁ㄧ敵璇烽〉闈紝浼犻�掍换鍔′俊鎭�
+ uni.navigateTo({
+ url: `/pages/mine/invoice/apply?taskInfo=${taskInfoParam}`
+ });
},
// 鏇存柊浠诲姟鐘舵��
@@ -2310,6 +2374,20 @@
margin-left: 10rpx;
vertical-align: middle;
}
+
+ .apply-invoice-btn {
+ padding: 8rpx 16rpx;
+ font-size: 24rpx;
+ color: #fff;
+ background-color: #34C759;
+ border: none;
+ border-radius: 6rpx;
+ margin-left: 20rpx;
+ }
+
+ .apply-invoice-btn::after {
+ border: none;
+ }
// 鍙栨秷鍘熷洜瀵硅瘽妗嗘牱寮�
.cancel-dialog {
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysInvoiceController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysInvoiceController.java
new file mode 100644
index 0000000..088027a
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysInvoiceController.java
@@ -0,0 +1,234 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import javax.servlet.http.HttpServletResponse;
+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.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.apache.commons.lang3.StringUtils;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.system.service.ISysDeptService;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.system.domain.SysInvoice;
+import com.ruoyi.system.service.ISysInvoiceService;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.utils.SecurityUtils;
+
+/**
+ * 鍙戠エ鐢宠Controller
+ *
+ * @author ruoyi
+ * @date 2026-02-02
+ */
+@RestController
+@RequestMapping("/system/invoice")
+public class SysInvoiceController extends BaseController
+{
+ @Autowired
+ private ISysInvoiceService sysInvoiceService;
+
+ @Autowired
+ private ISysDeptService sysDeptService;
+
+ /**
+ * 鏌ヨ鍙戠エ鐢宠鍒楄〃
+ */
+ @PreAuthorize("@ss.hasPermi('system:invoice:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysInvoice sysInvoice)
+ {
+ startPage();
+ List<SysInvoice> list = sysInvoiceService.selectSysInvoiceList(sysInvoice);
+ return getDataTable(list);
+ }
+
+ /**
+ * App绔煡璇㈡垜鐨勫彂绁ㄧ敵璇峰垪琛�
+ */
+ @GetMapping("/myList")
+ public TableDataInfo myList(SysInvoice sysInvoice)
+ {
+ sysInvoice.setApplyUserId(SecurityUtils.getUserId());
+ startPage();
+ List<Map<String, Object>> list = sysInvoiceService.selectMyInvoiceList(sysInvoice);
+ return getDataTable(list);
+ }
+
+ /**
+ * 瀵煎嚭鍙戠エ鐢宠鍒楄〃
+ */
+ @PreAuthorize("@ss.hasPermi('system:invoice:export')")
+ @Log(title = "鍙戠エ鐢宠", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysInvoice sysInvoice)
+ {
+ List<SysInvoice> list = sysInvoiceService.selectSysInvoiceList(sysInvoice);
+ ExcelUtil<SysInvoice> util = new ExcelUtil<SysInvoice>(SysInvoice.class);
+ util.exportExcel(response, list, "鍙戠エ鐢宠鏁版嵁");
+ }
+
+ /**
+ * 鑾峰彇鍙戠エ鐢宠璇︾粏淇℃伅
+ */
+ @PreAuthorize("@ss.hasAnyPermi('system:invoice:query, system:invoice:edit')")
+ @GetMapping(value = "/{invoiceId}")
+ public AjaxResult getInfo(@PathVariable("invoiceId") Long invoiceId)
+ {
+ return AjaxResult.success(sysInvoiceService.selectSysInvoiceByInvoiceId(invoiceId));
+ }
+
+ /**
+ * 鏂板鍙戠エ鐢宠
+ */
+ @Log(title = "鍙戠エ鐢宠", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody SysInvoice sysInvoice)
+ {
+ sysInvoice.setApplyUserId(SecurityUtils.getUserId());
+ return toAjax(sysInvoiceService.insertSysInvoice(sysInvoice));
+ }
+
+ /**
+ * 淇敼鍙戠エ鐢宠
+ */
+ @PreAuthorize("@ss.hasPermi('system:invoice:edit')")
+ @Log(title = "鍙戠エ鐢宠", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody SysInvoice sysInvoice)
+ {
+ return toAjax(sysInvoiceService.updateSysInvoice(sysInvoice));
+ }
+
+ /**
+ * 鍒犻櫎鍙戠エ鐢宠
+ */
+ @PreAuthorize("@ss.hasPermi('system:invoice:remove')")
+ @Log(title = "鍙戠エ鐢宠", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{invoiceIds}")
+ public AjaxResult remove(@PathVariable Long[] invoiceIds)
+ {
+ return toAjax(sysInvoiceService.deleteSysInvoiceByInvoiceIds(invoiceIds));
+ }
+
+ /**
+ * 鎵嬪姩瑙﹀彂鍚屾 (浠呴檺绠$悊鍛�)
+ */
+ @PreAuthorize("@ss.hasRole('admin')")
+ @GetMapping("/syncStatus")
+ public AjaxResult syncStatus() {
+ sysInvoiceService.syncStatusFromLegacySystem();
+ return AjaxResult.success("鍚屾浠诲姟宸茶Е鍙�");
+ }
+
+ /**
+ * 鍚屾鍗曚釜鍙戠エ鍒版棫绯荤粺
+ * @param invoiceId 鍙戠エID
+ */
+ @PreAuthorize("@ss.hasPermi('system:invoice:edit')")
+ @Log(title = "鍙戠エ鐢宠", businessType = BusinessType.UPDATE)
+ @PostMapping("/syncToLegacy/{invoiceId}")
+ public AjaxResult syncToLegacy(@PathVariable Long invoiceId) {
+ SysInvoice invoice = sysInvoiceService.selectSysInvoiceByInvoiceId(invoiceId);
+ if (invoice == null) {
+ return AjaxResult.error("鍙戠エ涓嶅瓨鍦�");
+ }
+ if (invoice.getStatus() != 1) {
+ return AjaxResult.error("鍙湁瀹℃牳閫氳繃鐨勫彂绁ㄦ墠鑳藉悓姝�");
+ }
+
+ try {
+ sysInvoiceService.syncToLegacySystem(invoice.getInvoiceId());
+ return AjaxResult.success("鍚屾鎴愬姛");
+ } catch (Exception e) {
+ return AjaxResult.error("鍚屾澶辫触: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 妫�鏌ヤ换鍔℃槸鍚﹀凡鐢宠鍙戠エ
+ * @param taskId 浠诲姟ID
+ */
+ @GetMapping("/checkTaskInvoice/{taskId}")
+ public AjaxResult checkTaskInvoice(@PathVariable Long taskId) {
+ SysInvoice query = new SysInvoice();
+ query.setServiceOrderId(taskId);
+ List<SysInvoice> list = sysInvoiceService.selectSysInvoiceList(query);
+
+ if (list != null && !list.isEmpty()) {
+ // 鍙繑鍥炴渶鏂扮殑涓�鏉¤褰�
+ SysInvoice invoice = list.get(0);
+ Map<String, Object> result = new HashMap<>();
+ result.put("hasApplied", true);
+ result.put("status", invoice.getStatus());
+ result.put("invoiceId", invoice.getInvoiceId());
+ return AjaxResult.success(result);
+ }
+
+ return AjaxResult.success(null);
+ }
+
+ /**
+ * 鑾峰彇鍙敵璇峰彂绁ㄧ殑浠诲姟鍒楄〃
+ * @param searchKeyword 鎼滅储鍏抽敭璇嶏紙鏀寔taskCode銆乻erviceCode銆乴egacyServiceOrdNo锛�
+ * @param serviceOrdClass 鍒嗗叕鍙镐唬鐮侊紙鍙�夛紝榛樿浣跨敤鐢ㄦ埛鎵�灞炲垎鍏徃锛�
+ */
+ @GetMapping("/selectableTasks")
+ public AjaxResult getSelectableTasks(
+ @RequestParam(required = false) String searchKeyword,
+ @RequestParam(required = false) String serviceOrdClass
+ )
+ {
+ Long userId = SecurityUtils.getUserId();
+ // 鍘婚櫎鎼滅储鍏抽敭璇嶇殑绌烘牸
+ if (StringUtils.isNotBlank(searchKeyword)) {
+ searchKeyword = searchKeyword.trim();
+ }
+
+ // 濡傛灉鏈寚瀹氬垎鍏徃锛岃嚜鍔ㄨ幏鍙栫敤鎴锋墍灞炲垎鍏徃
+ if (StringUtils.isBlank(serviceOrdClass)) {
+ try {
+ Long deptId = SecurityUtils.getLoginUser().getUser().getDeptId();
+ if (deptId != null) {
+ SysDept dept = sysDeptService.selectDeptById(deptId);
+ if (dept != null) {
+ // 鍒ゆ柇鏄惁涓哄垎鍏徃锛坧arent_id = 100锛�
+ if (dept.getParentId() != null && dept.getParentId() == 100) {
+ serviceOrdClass = dept.getServiceOrderClass();
+ } else if (dept.getAncestors() != null) {
+ // 浠� ancestors 瑙f瀽鍒嗗叕鍙窱D
+ String[] ancestorIds = dept.getAncestors().split(",");
+ for (int i = 0; i < ancestorIds.length; i++) {
+ if ("100".equals(ancestorIds[i]) && i + 1 < ancestorIds.length) {
+ Long branchId = Long.parseLong(ancestorIds[i + 1]);
+ SysDept branchDept = sysDeptService.selectDeptById(branchId);
+ if (branchDept != null) {
+ serviceOrdClass = branchDept.getServiceOrderClass();
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ // 鑾峰彇澶辫触涓嶅奖鍝嶆煡璇紝鍙槸涓嶈繃婊ゅ垎鍏徃
+ }
+ }
+
+ return AjaxResult.success(sysInvoiceService.selectSelectableTasks(userId, searchKeyword, serviceOrdClass));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
index 36a79da..bb566c9 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskController.java
@@ -78,6 +78,9 @@
@Autowired
private ITaskDispatchSyncService taskDispatchSyncService;
+
+ @Autowired
+ private ITaskStatusPushService taskStatusPushService;
/**
* 鏌ヨ浠诲姟绠$悊鍒楄〃锛堝悗鍙扮鐞嗙锛�
@@ -704,4 +707,50 @@
return error("鍚屾寮傚父: " + e.getMessage());
}
}
+
+ /**
+ * 鎵嬪姩鍚屾浠诲姟鐘舵�佸埌鏃х郴缁�
+ * 褰撲换鍔$姸鎬佸彉鏇村悗鐢变簬缃戠粶绛夊師鍥犳湭鍚屾鍒版棫绯荤粺鏃讹紝鍙互閫氳繃姝ゆ帴鍙f墜鍔ㄨЕ鍙戝悓姝�
+ */
+// @PreAuthorize("@ss.hasPermi('task:general:edit')")
+ @Log(title = "鎵嬪姩鍚屾浠诲姟鐘舵��", businessType = BusinessType.UPDATE)
+ @PostMapping("/syncTaskStatus/{taskId}")
+ public AjaxResult syncTaskStatus(@PathVariable Long taskId) {
+ try {
+ // 鏌ヨ浠诲姟淇℃伅
+ SysTask task = sysTaskService.selectSysTaskByTaskId(taskId);
+ if (task == null) {
+ return error("浠诲姟涓嶅瓨鍦�");
+ }
+
+ // 鍙敮鎸佹�ユ晳杞繍浠诲姟
+ if (!"EMERGENCY_TRANSFER".equals(task.getTaskType())) {
+ return error("鍙湁鎬ユ晳杞繍浠诲姟鎵嶈兘鍚屾鍒版棫绯荤粺");
+ }
+
+ // 鏌ヨ鎬ユ晳杞繍鎵╁睍淇℃伅
+ SysTaskEmergency emergency = sysTaskEmergencyService.selectSysTaskEmergencyByTaskId(taskId);
+ if (emergency == null) {
+ return error("鎬ユ晳杞繍鎵╁睍淇℃伅涓嶅瓨鍦�");
+ }
+
+ // 蹇呴』鍏堟湁璋冨害鍗�
+ if (emergency.getLegacyDispatchOrdId() == null || emergency.getLegacyDispatchOrdId() <= 0) {
+ return error("璇峰厛鍚屾璋冨害鍗曪紝浠诲姟鐘舵�佷俊鎭悓姝ュ埌鏃х郴缁熺殑璋冨害鍗曚腑");
+ }
+
+ // 璋冪敤鐘舵�佸悓姝ユ湇鍔�
+ boolean success = taskStatusPushService.pushTaskStatusToLegacy(taskId);
+
+ if (success) {
+ return success("浠诲姟鐘舵�佸悓姝ユ垚鍔�");
+ } else {
+ return error("浠诲姟鐘舵�佸悓姝ュけ璐ワ紝璇锋煡鐪嬫棩蹇楄幏鍙栬缁嗕俊鎭�");
+ }
+
+ } catch (Exception e) {
+ logger.error("鎵嬪姩鍚屾浠诲姟鐘舵�佸紓甯革紝taskId: {}", taskId, e);
+ return error("鍚屾寮傚父: " + e.getMessage());
+ }
+ }
}
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index fed985b..ffcd5be 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -58,7 +58,7 @@
basename: i18n/messages
profiles:
# 鐜 dev|test|prod
- active: dev
+ active: prod
# 鏂囦欢涓婁紶
servlet:
multipart:
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SysInvoiceSyncTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SysInvoiceSyncTask.java
new file mode 100644
index 0000000..0f03d16
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SysInvoiceSyncTask.java
@@ -0,0 +1,26 @@
+package com.ruoyi.quartz.task;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import com.ruoyi.system.service.ISysInvoiceService;
+
+/**
+ * 鍙戠エ鍚屾瀹氭椂浠诲姟 - 鍚屾鏃х郴缁熷彂绁ㄤ俊鎭埌鏂扮郴缁�
+ *
+ * @author ruoyi
+ */
+@Component("sysInvoiceSyncTask")
+public class SysInvoiceSyncTask
+{
+ @Autowired
+ private ISysInvoiceService sysInvoiceService;
+
+
+ /**
+ * 鍚屾鏃х郴缁熷彂绁ㄧ姸鎬�
+ */
+ public void syncStatus()
+ {
+ sysInvoiceService.syncStatusFromLegacySystem();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SysInvoiceTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SysInvoiceTask.java
new file mode 100644
index 0000000..1e75c5d
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SysInvoiceTask.java
@@ -0,0 +1,33 @@
+package com.ruoyi.quartz.task;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import com.ruoyi.system.service.ISysInvoiceService;
+
+/**
+ * 鍙戠エ鍚屾瀹氭椂浠诲姟
+ *
+ * @author ruoyi
+ */
+@Component("sysInvoiceTask")
+public class SysInvoiceTask
+{
+ @Autowired
+ private ISysInvoiceService sysInvoiceService;
+
+ /**
+ * 鍚屾鏃х郴缁熷彂绁ㄧ姸鎬�
+ */
+ public void syncStatus()
+ {
+ sysInvoiceService.syncStatusFromLegacySystem();
+ }
+
+ /**
+ * 浠庢棫绯荤粺鍚屾鍙戠エ淇℃伅鍒版柊绯荤粺
+ */
+ public void syncInvoiceFromLegacySystem()
+ {
+// sysInvoiceService.syncInvoiceFromLegacySystem();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysInvoice.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysInvoice.java
new file mode 100644
index 0000000..30e6fa9
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysInvoice.java
@@ -0,0 +1,380 @@
+package com.ruoyi.system.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 鍙戠エ鐢宠瀵硅薄 sys_invoice
+ *
+ * @author ruoyi
+ * @date 2026-02-02
+ */
+public class SysInvoice extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** 鍙戠エID */
+ private Long invoiceId;
+
+ /** 鏈嶅姟鍗曞彿(鏂扮郴缁烮D) */
+ @Excel(name = "鏈嶅姟鍗曞彿(鏂�)")
+ private Long serviceOrderId;
+
+ /** 鏃х郴缁熸湇鍔″崟鍙� */
+ @Excel(name = "鏈嶅姟鍗曞彿(鏃�)")
+ private Long legacyServiceOrderId;
+
+ /** 寮�绁ㄧ被鍨�(1-涓汉, 2-浼佷笟) */
+ @Excel(name = "寮�绁ㄧ被鍨�", readConverterExp = "1=涓汉,2=浼佷笟")
+ private Integer invoiceType;
+
+ /** 鍙戠エ鎶ご */
+ @Excel(name = "鍙戠エ鎶ご")
+ private String invoiceName;
+
+ /** 鍙戠エ閲戦 */
+ @Excel(name = "鍙戠エ閲戦")
+ private BigDecimal invoiceMoney;
+
+ /** 鍙戠エ澶囨敞 */
+ @Excel(name = "鍙戠エ澶囨敞")
+ private String invoiceRemarks;
+
+ /** 浼佷笟娉ㄥ唽鍦板潃 */
+ @Excel(name = "浼佷笟娉ㄥ唽鍦板潃")
+ private String companyAddress;
+
+ /** 浼佷笟寮�鎴烽摱琛� */
+ @Excel(name = "浼佷笟寮�鎴烽摱琛�")
+ private String companyBank;
+
+ /** 浼佷笟閾惰甯愬彿 */
+ @Excel(name = "浼佷笟閾惰甯愬彿")
+ private String companyBankNo;
+
+ /** 閭紪 */
+ @Excel(name = "閭紪")
+ private String zipCode;
+
+ /** 閭瘎鍦板潃 */
+ @Excel(name = "閭瘎鍦板潃")
+ private String mailAddress;
+
+ /** 鑱旂郴浜� */
+ @Excel(name = "鑱旂郴浜�")
+ private String contactName;
+
+ /** 鑱旂郴鐢佃瘽 */
+ @Excel(name = "鑱旂郴鐢佃瘽")
+ private String contactPhone;
+
+ /** 鑱旂郴閭 */
+ @Excel(name = "鑱旂郴閭")
+ private String contactEmail;
+
+ /** 鐢宠鐘舵��(0-寰呭鏍�, 1-宸查�氳繃, 2-宸查┏鍥�) */
+ @Excel(name = "鐢宠鐘舵��", readConverterExp = "0=寰呭鏍�,1=宸查�氳繃,2=宸查┏鍥�")
+ private Integer status;
+
+ /** 鍙戠エ缂栧彿 */
+ @Excel(name = "鍙戠エ缂栧彿")
+ private String invoiceNo;
+
+ /** 鍙戠エ閾炬帴 */
+ @Excel(name = "鍙戠エ閾炬帴")
+ private String invoiceUrl;
+
+ /** 鐢宠浜篒D */
+ private Long applyUserId;
+
+ /** 鐢宠鏃堕棿 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "鐢宠鏃堕棿", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date applyTime;
+
+ /** 瀹℃牳浜篒D */
+ private Long auditUserId;
+
+ /** 瀹℃牳鏃堕棿 */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "瀹℃牳鏃堕棿", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+ private Date auditTime;
+
+ /** 瀹℃牳澶囨敞 */
+ private String auditRemarks;
+
+ /** 鍚屾鐘舵��(0-鏈悓姝�, 1-宸插悓姝�, 2-澶辫触) */
+ private Integer syncStatus;
+
+ /** 鏃х郴缁熷彂绁↖D */
+ private Integer legacyInvoiceId;
+
+ /** 鏈嶅姟鍗曞彿锛堟牸寮忓寲锛屽GZ20260202-001锛�- 浠呯敤浜庢煡璇㈣繑鍥� */
+ private String serviceCode;
+
+ public void setInvoiceId(Long invoiceId)
+ {
+ this.invoiceId = invoiceId;
+ }
+
+ public Long getInvoiceId()
+ {
+ return invoiceId;
+ }
+ public void setServiceOrderId(Long serviceOrderId)
+ {
+ this.serviceOrderId = serviceOrderId;
+ }
+
+ public Long getServiceOrderId()
+ {
+ return serviceOrderId;
+ }
+ public void setLegacyServiceOrderId(Long legacyServiceOrderId)
+ {
+ this.legacyServiceOrderId = legacyServiceOrderId;
+ }
+
+ public Long getLegacyServiceOrderId()
+ {
+ return legacyServiceOrderId;
+ }
+ public void setInvoiceType(Integer invoiceType)
+ {
+ this.invoiceType = invoiceType;
+ }
+
+ public Integer getInvoiceType()
+ {
+ return invoiceType;
+ }
+ public void setInvoiceName(String invoiceName)
+ {
+ this.invoiceName = invoiceName;
+ }
+
+ public String getInvoiceName()
+ {
+ return invoiceName;
+ }
+ public void setInvoiceMoney(BigDecimal invoiceMoney)
+ {
+ this.invoiceMoney = invoiceMoney;
+ }
+
+ public BigDecimal getInvoiceMoney()
+ {
+ return invoiceMoney;
+ }
+ public void setInvoiceRemarks(String invoiceRemarks)
+ {
+ this.invoiceRemarks = invoiceRemarks;
+ }
+
+ public String getInvoiceRemarks()
+ {
+ return invoiceRemarks;
+ }
+ public void setCompanyAddress(String companyAddress)
+ {
+ this.companyAddress = companyAddress;
+ }
+
+ public String getCompanyAddress()
+ {
+ return companyAddress;
+ }
+ public void setCompanyBank(String companyBank)
+ {
+ this.companyBank = companyBank;
+ }
+
+ public String getCompanyBank()
+ {
+ return companyBank;
+ }
+ public void setCompanyBankNo(String companyBankNo)
+ {
+ this.companyBankNo = companyBankNo;
+ }
+
+ public String getCompanyBankNo()
+ {
+ return companyBankNo;
+ }
+ public void setZipCode(String zipCode)
+ {
+ this.zipCode = zipCode;
+ }
+
+ public String getZipCode()
+ {
+ return zipCode;
+ }
+ public void setMailAddress(String mailAddress)
+ {
+ this.mailAddress = mailAddress;
+ }
+
+ public String getMailAddress()
+ {
+ return mailAddress;
+ }
+ public void setContactName(String contactName)
+ {
+ this.contactName = contactName;
+ }
+
+ public String getContactName()
+ {
+ return contactName;
+ }
+ public void setContactPhone(String contactPhone)
+ {
+ this.contactPhone = contactPhone;
+ }
+
+ public String getContactPhone()
+ {
+ return contactPhone;
+ }
+ public void setContactEmail(String contactEmail)
+ {
+ this.contactEmail = contactEmail;
+ }
+
+ public String getContactEmail()
+ {
+ return contactEmail;
+ }
+ public void setStatus(Integer status)
+ {
+ this.status = status;
+ }
+
+ public Integer getStatus()
+ {
+ return status;
+ }
+ public void setInvoiceNo(String invoiceNo)
+ {
+ this.invoiceNo = invoiceNo;
+ }
+
+ public String getInvoiceNo()
+ {
+ return invoiceNo;
+ }
+ public void setInvoiceUrl(String invoiceUrl)
+ {
+ this.invoiceUrl = invoiceUrl;
+ }
+
+ public String getInvoiceUrl()
+ {
+ return invoiceUrl;
+ }
+ public void setApplyUserId(Long applyUserId)
+ {
+ this.applyUserId = applyUserId;
+ }
+
+ public Long getApplyUserId()
+ {
+ return applyUserId;
+ }
+ public void setApplyTime(Date applyTime)
+ {
+ this.applyTime = applyTime;
+ }
+
+ public Date getApplyTime()
+ {
+ return applyTime;
+ }
+ public void setAuditUserId(Long auditUserId)
+ {
+ this.auditUserId = auditUserId;
+ }
+
+ public Long getAuditUserId()
+ {
+ return auditUserId;
+ }
+ public void setAuditTime(Date auditTime)
+ {
+ this.auditTime = auditTime;
+ }
+
+ public Date getAuditTime()
+ {
+ return auditTime;
+ }
+ public void setAuditRemarks(String auditRemarks)
+ {
+ this.auditRemarks = auditRemarks;
+ }
+
+ public String getAuditRemarks()
+ {
+ return auditRemarks;
+ }
+
+ public Integer getSyncStatus() {
+ return syncStatus;
+ }
+
+ public void setSyncStatus(Integer syncStatus) {
+ this.syncStatus = syncStatus;
+ }
+
+ public Integer getLegacyInvoiceId() {
+ return legacyInvoiceId;
+ }
+
+ public void setLegacyInvoiceId(Integer legacyInvoiceId) {
+ this.legacyInvoiceId = legacyInvoiceId;
+ }
+
+ public String getServiceCode() {
+ return serviceCode;
+ }
+
+ public void setServiceCode(String serviceCode) {
+ this.serviceCode = serviceCode;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("invoiceId", getInvoiceId())
+ .append("serviceOrderId", getServiceOrderId())
+ .append("legacyServiceOrderId", getLegacyServiceOrderId())
+ .append("invoiceType", getInvoiceType())
+ .append("invoiceName", getInvoiceName())
+ .append("invoiceMoney", getInvoiceMoney())
+ .append("invoiceRemarks", getInvoiceRemarks())
+ .append("companyAddress", getCompanyAddress())
+ .append("companyBank", getCompanyBank())
+ .append("companyBankNo", getCompanyBankNo())
+ .append("zipCode", getZipCode())
+ .append("mailAddress", getMailAddress())
+ .append("contactName", getContactName())
+ .append("contactPhone", getContactPhone())
+ .append("contactEmail", getContactEmail())
+ .append("status", getStatus())
+ .append("invoiceNo", getInvoiceNo())
+ .append("invoiceUrl", getInvoiceUrl())
+ .append("applyUserId", getApplyUserId())
+ .append("applyTime", getApplyTime())
+ .append("auditUserId", getAuditUserId())
+ .append("auditTime", getAuditTime())
+ .append("auditRemarks", getAuditRemarks())
+ .toString();
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java
index 80e7a9f..6b4ecd7 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysTaskEmergency.java
@@ -166,6 +166,10 @@
/** 鍙栨秷鏃堕棿 */
private java.util.Date cancelTime;
+ /**
+ * 鏈嶅姟鍗曠紪鍙�
+ * @return
+ */
public String getServiceCode(){
if(this.legacyServiceOrdClass!=null && this.legacyServiceNsTime!=null && this.legacyServiceOrdNo!=null) {
String nstime = DateUtils.parseDateToStr(DateUtils.YYYYMMDD, this.legacyServiceNsTime);
@@ -174,6 +178,10 @@
return null;
}
+ /**
+ * 璋冨害鍗曠紪鍙�
+ * @return
+ */
public String getDispatchCode(){
if(this.legacyDispatchOrdClass!=null && this.legacyDispatchNsTime!=null && this.legacyDispatchOrdNo!=null) {
String nstime = DateUtils.parseDateToStr(DateUtils.YYYYMMDD, this.legacyDispatchNsTime);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyInvoiceMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyInvoiceMapper.java
new file mode 100644
index 0000000..de782d9
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/LegacyInvoiceMapper.java
@@ -0,0 +1,37 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import java.util.Map;
+import com.ruoyi.common.annotation.DataSource;
+import com.ruoyi.common.enums.DataSourceType;
+
+/**
+ * 鏃х郴缁熷彂绁ㄦ暟鎹甅apper鎺ュ彛 (SQL Server)
+ *
+ * @author ruoyi
+ * @date 2026-02-02
+ */
+@DataSource(DataSourceType.SQLSERVER)
+public interface LegacyInvoiceMapper
+{
+ /**
+ * 鎻掑叆鍙戠エ鐢宠鍒版棫绯荤粺
+ * @param params
+ * @return
+ */
+ public int insertLegacyInvoice(Map<String, Object> params);
+
+ /**
+ * 鏌ヨ鏃х郴缁熷彂绁ㄧ姸鎬佸彉鍖栫殑鏁版嵁
+ * @param lastSyncTime
+ * @return
+ */
+ public List<Map<String, Object>> selectUpdatedInvoices(String lastSyncTime);
+
+ /**
+ * 鏍规嵁鏈嶅姟鍗旾D鏌ヨ鏃х郴缁熷彂绁ㄤ俊鎭�
+ * @param serviceOrderId
+ * @return
+ */
+ public List<Map<String, Object>> selectLegacyInvoiceByServiceOrderId(Long serviceOrderId);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysInvoiceMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysInvoiceMapper.java
new file mode 100644
index 0000000..c416ffa
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysInvoiceMapper.java
@@ -0,0 +1,91 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.system.domain.SysInvoice;
+
+/**
+ * 鍙戠エ鐢宠Mapper鎺ュ彛
+ *
+ * @author ruoyi
+ * @date 2026-02-02
+ */
+public interface SysInvoiceMapper
+{
+ /**
+ * 鏌ヨ鍙戠エ鐢宠
+ *
+ * @param invoiceId 鍙戠エ鐢宠涓婚敭
+ * @return 鍙戠エ鐢宠
+ */
+ public SysInvoice selectSysInvoiceByInvoiceId(Long invoiceId);
+
+ /**
+ * 鏌ヨ鍙戠エ鐢宠鍒楄〃
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 鍙戠エ鐢宠闆嗗悎
+ */
+ public List<SysInvoice> selectSysInvoiceList(SysInvoice sysInvoice);
+
+ /**
+ * 鏌ヨ鎴戠殑鍙戠エ鐢宠鍒楄〃锛圓pp绔紝杩斿洖Map鍖呭惈serviceCode锛�
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 鍙戠エ鐢宠闆嗗悎
+ */
+ public List<Map<String, Object>> selectMyInvoiceList(SysInvoice sysInvoice);
+
+ /**
+ * 鏂板鍙戠エ鐢宠
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 缁撴灉
+ */
+ public int insertSysInvoice(SysInvoice sysInvoice);
+
+ /**
+ * 淇敼鍙戠エ鐢宠
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 缁撴灉
+ */
+ public int updateSysInvoice(SysInvoice sysInvoice);
+
+ /**
+ * 鍒犻櫎鍙戠エ鐢宠
+ *
+ * @param invoiceId 鍙戠エ鐢宠涓婚敭
+ * @return 缁撴灉
+ */
+ public int deleteSysInvoiceByInvoiceId(Long invoiceId);
+
+ /**
+ * 鎵归噺鍒犻櫎鍙戠エ鐢宠
+ *
+ * @param invoiceIds 闇�瑕佸垹闄ょ殑鏁版嵁涓婚敭闆嗗悎
+ * @return 缁撴灉
+ */
+ public int deleteSysInvoiceByInvoiceIds(Long[] invoiceIds);
+
+ /**
+ * 鏍规嵁鏃х郴缁熷彂绁↖D鏌ヨ
+ * @param legacyInvoiceId
+ * @return
+ */
+ public SysInvoice selectSysInvoiceByLegacyId(Integer legacyInvoiceId);
+
+ /**
+ * 鏌ヨ鍙敵璇峰彂绁ㄧ殑浠诲姟鍒楄〃
+ * @param userId 鐢ㄦ埛ID
+ * @param searchKeyword 鎼滅储鍏抽敭璇�
+ * @param serviceOrdClass 鍒嗗叕鍙镐唬鐮�
+ * @return
+ */
+ public List<Map<String, Object>> selectSelectableTasks(
+ @Param("userId") Long userId,
+ @Param("searchKeyword") String searchKeyword,
+ @Param("serviceOrdClass") String serviceOrdClass
+ );
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysInvoiceService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysInvoiceService.java
new file mode 100644
index 0000000..5e0b1da
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysInvoiceService.java
@@ -0,0 +1,91 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import java.util.Map;
+import com.ruoyi.system.domain.SysInvoice;
+
+/**
+ * 鍙戠エ鐢宠Service鎺ュ彛
+ *
+ * @author ruoyi
+ * @date 2026-02-02
+ */
+public interface ISysInvoiceService
+{
+ /**
+ * 鏌ヨ鍙戠エ鐢宠
+ *
+ * @param invoiceId 鍙戠エ鐢宠涓婚敭
+ * @return 鍙戠エ鐢宠
+ */
+ public SysInvoice selectSysInvoiceByInvoiceId(Long invoiceId);
+
+ /**
+ * 鏌ヨ鍙戠エ鐢宠鍒楄〃
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 鍙戠エ鐢宠闆嗗悎
+ */
+ public List<SysInvoice> selectSysInvoiceList(SysInvoice sysInvoice);
+
+ /**
+ * 鏌ヨ鎴戠殑鍙戠エ鐢宠鍒楄〃锛圓pp绔紝杩斿洖Map鍖呭惈serviceCode锛�
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 鍙戠エ鐢宠闆嗗悎
+ */
+ public List<Map<String, Object>> selectMyInvoiceList(SysInvoice sysInvoice);
+
+ /**
+ * 鏂板鍙戠エ鐢宠
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 缁撴灉
+ */
+ public int insertSysInvoice(SysInvoice sysInvoice);
+
+ /**
+ * 淇敼鍙戠エ鐢宠
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 缁撴灉
+ */
+ public int updateSysInvoice(SysInvoice sysInvoice);
+
+ /**
+ * 鎵归噺鍒犻櫎鍙戠エ鐢宠
+ *
+ * @param invoiceIds 闇�瑕佸垹闄ょ殑鍙戠エ鐢宠涓婚敭闆嗗悎
+ * @return 缁撴灉
+ */
+ public int deleteSysInvoiceByInvoiceIds(Long[] invoiceIds);
+
+ /**
+ * 鍒犻櫎鍙戠エ鐢宠淇℃伅
+ *
+ * @param invoiceId 鍙戠エ鐢宠涓婚敭
+ * @return 缁撴灉
+ */
+ public int deleteSysInvoiceByInvoiceId(Long invoiceId);
+
+ /**
+ * 鍚屾鍒版棫绯荤粺
+ * @param invoiceId
+ * @return
+ */
+ public int syncToLegacySystem(Long invoiceId);
+
+ /**
+ * 浠庢棫绯荤粺鍚屾鐘舵��
+ */
+ public void syncStatusFromLegacySystem();
+
+ /**
+ * 鏌ヨ鍙敵璇峰彂绁ㄧ殑浠诲姟鍒楄〃
+ * @param userId 鐢ㄦ埛ID
+ * @param searchKeyword 鎼滅储鍏抽敭璇�
+ * @param serviceOrdClass 鍒嗗叕鍙镐唬鐮�
+ * @return
+ */
+ public List<Map<String, Object>> selectSelectableTasks(Long userId, String searchKeyword, String serviceOrdClass);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PaymentSyncServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PaymentSyncServiceImpl.java
index 499a94b..f927f3c 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PaymentSyncServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PaymentSyncServiceImpl.java
@@ -120,7 +120,11 @@
}
paidMoney.setPaidMoney(payment.getSettlementAmount());
paidMoney.setPaidMoneyType(convertPaymentMethodToLegacy(payment.getPaymentMethod()));
- paidMoney.setPaidMoneyMono(payment.getTradeNo() != null ? payment.getTradeNo() : payment.getOutTradeNo());
+ String outTradeNo = payment.getTradeNo() != null ? payment.getTradeNo() : payment.getOutTradeNo();
+ if(!outTradeNo.contains("[鏀粯涓撶敤]")){
+ outTradeNo=outTradeNo+"[鏀粯涓撶敤]";
+ }
+ paidMoney.setPaidMoneyMono(outTradeNo);
paidMoney.setPaidMoneyTime(payment.getPayTime() != null ? payment.getPayTime() : new Date());
paidMoney.setPaidMoneyOaID(oaUserId);
paidMoney.setPaidMoneyUnitID(0); // 榛樿涓�0
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysInvoiceServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysInvoiceServiceImpl.java
new file mode 100644
index 0000000..ee2c483
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysInvoiceServiceImpl.java
@@ -0,0 +1,243 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.system.domain.SysInvoice;
+import com.ruoyi.system.mapper.SysInvoiceMapper;
+import com.ruoyi.system.mapper.LegacyInvoiceMapper;
+import com.ruoyi.system.mapper.SysUserMapper;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.system.service.ISysInvoiceService;
+
+/**
+ * 鍙戠エ鐢宠Service涓氬姟灞傚鐞�
+ *
+ * @author ruoyi
+ * @date 2026-02-02
+ */
+@Service
+public class SysInvoiceServiceImpl implements ISysInvoiceService
+{
+ private static final Logger log = LoggerFactory.getLogger(SysInvoiceServiceImpl.class);
+
+ @Autowired
+ private SysInvoiceMapper sysInvoiceMapper;
+
+ @Autowired
+ private LegacyInvoiceMapper legacyInvoiceMapper;
+
+ @Autowired
+ private SysUserMapper sysUserMapper;
+
+ /**
+ * 鏌ヨ鍙戠エ鐢宠
+ *
+ * @param invoiceId 鍙戠エ鐢宠涓婚敭
+ * @return 鍙戠エ鐢宠
+ */
+ @Override
+ public SysInvoice selectSysInvoiceByInvoiceId(Long invoiceId)
+ {
+ return sysInvoiceMapper.selectSysInvoiceByInvoiceId(invoiceId);
+ }
+
+ /**
+ * 鏌ヨ鍙戠エ鐢宠鍒楄〃
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 鍙戠エ鐢宠
+ */
+ @Override
+ public List<SysInvoice> selectSysInvoiceList(SysInvoice sysInvoice)
+ {
+ return sysInvoiceMapper.selectSysInvoiceList(sysInvoice);
+ }
+
+ /**
+ * 鏌ヨ鎴戠殑鍙戠エ鐢宠鍒楄〃锛圓pp绔紝杩斿洖Map鍖呭惈serviceCode锛�
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 鍙戠エ鐢宠
+ */
+ @Override
+ public List<Map<String, Object>> selectMyInvoiceList(SysInvoice sysInvoice)
+ {
+ return sysInvoiceMapper.selectMyInvoiceList(sysInvoice);
+ }
+
+ /**
+ * 鏂板鍙戠エ鐢宠
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 缁撴灉
+ */
+ @Override
+ public int insertSysInvoice(SysInvoice sysInvoice)
+ {
+ sysInvoice.setApplyTime(DateUtils.getNowDate());
+ sysInvoice.setStatus(0); // 寰呭鏍�
+ sysInvoice.setSyncStatus(0); // 鏈悓姝�
+ int rows = sysInvoiceMapper.insertSysInvoice(sysInvoice);
+
+ // 鑷姩灏濊瘯鍚屾鍒版棫绯荤粺
+ if (rows > 0) {
+ try {
+ syncToLegacySystem(sysInvoice.getInvoiceId());
+ } catch (Exception e) {
+ log.error("鍚屾鍙戠エ鐢宠鍒版棫绯荤粺澶辫触", e);
+ }
+ }
+ return rows;
+ }
+
+ /**
+ * 淇敼鍙戠エ鐢宠
+ *
+ * @param sysInvoice 鍙戠エ鐢宠
+ * @return 缁撴灉
+ */
+ @Override
+ public int updateSysInvoice(SysInvoice sysInvoice)
+ {
+ return sysInvoiceMapper.updateSysInvoice(sysInvoice);
+ }
+
+ /**
+ * 鎵归噺鍒犻櫎鍙戠エ鐢宠
+ *
+ * @param invoiceIds 闇�瑕佸垹闄ょ殑鍙戠エ鐢宠涓婚敭
+ * @return 缁撴灉
+ */
+ @Override
+ public int deleteSysInvoiceByInvoiceIds(Long[] invoiceIds)
+ {
+ return sysInvoiceMapper.deleteSysInvoiceByInvoiceIds(invoiceIds);
+ }
+
+ /**
+ * 鍒犻櫎鍙戠エ鐢宠淇℃伅
+ *
+ * @param invoiceId 鍙戠エ鐢宠涓婚敭
+ * @return 缁撴灉
+ */
+ @Override
+ public int deleteSysInvoiceByInvoiceId(Long invoiceId)
+ {
+ return sysInvoiceMapper.deleteSysInvoiceByInvoiceId(invoiceId);
+ }
+
+ /**
+ * 鍚屾鍙戠エ鐢宠鍒版棫绯荤粺 (SQL Server)
+ */
+ @Override
+ public int syncToLegacySystem(Long invoiceId) {
+ SysInvoice invoice = sysInvoiceMapper.selectSysInvoiceByInvoiceId(invoiceId);
+ if (invoice == null) return 0;
+
+ Map<String, Object> params = new HashMap<>();
+ params.put("ServiceOrderIDPK", invoice.getLegacyServiceOrderId());
+ 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("InvoiceMoney", invoice.getInvoiceMoney());
+
+ // 閫氳繃鍒涘缓浜築ID鏌ヨOA鐢ㄦ埛ID
+ 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 {
+ int rows = legacyInvoiceMapper.insertLegacyInvoice(params);
+ if (rows > 0) {
+ // SQL Server insert 浼氶�氳繃 useGeneratedKeys 杩斿洖鑷 ID 鍒� Map 鐨� keyProperty
+ Object legacyId = params.get("InvoiceID");
+ if (legacyId != null) {
+ invoice.setLegacyInvoiceId(Integer.valueOf(legacyId.toString()));
+ invoice.setSyncStatus(1); // 宸插悓姝�
+ sysInvoiceMapper.updateSysInvoice(invoice);
+ }
+ }
+ return rows;
+ } catch (Exception e) {
+ log.error("鍚屾鍙戠エ鍒版棫绯荤粺寮傚父: {}", e.getMessage());
+ invoice.setSyncStatus(2); // 鍚屾澶辫触
+ sysInvoiceMapper.updateSysInvoice(invoice);
+ throw e;
+ }
+ }
+
+ /**
+ * 浠庢棫绯荤粺鍚屾鍙戠エ鐘舵�佸彉鍖�
+ */
+ @Override
+ public void syncStatusFromLegacySystem() {
+ // 鏌ヨ鏈�杩�3澶╂湁鍙樺寲鐨勬暟鎹�
+ String lastSyncTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.addDays(new Date(), -3));
+ List<Map<String, Object>> updatedList = legacyInvoiceMapper.selectUpdatedInvoices(lastSyncTime);
+
+ for (Map<String, Object> legacyData : updatedList) {
+ Integer legacyId = (Integer) legacyData.get("InvoiceID");
+ SysInvoice invoice = sysInvoiceMapper.selectSysInvoiceByLegacyId(legacyId);
+
+ if (invoice != null) {
+ // 鐘舵�佹槧灏勶細鏃х郴缁� AuditStatus (鍋囪 3=閫氳繃, 4=鎷掔粷, 鍏朵粬=鐢宠涓�)
+ Integer auditStatus = (Integer) legacyData.get("AuditStatus");
+ if (auditStatus != null) {
+ if (auditStatus == 3) invoice.setStatus(1); // 宸查�氳繃
+ else if (auditStatus == 4) invoice.setStatus(2); // 宸查┏鍥�
+ }
+
+ // 鏇存柊鍏朵粬淇℃伅
+ if (legacyData.get("InvoiceNo") != null) {
+ invoice.setInvoiceNo(legacyData.get("InvoiceNo").toString());
+ }
+
+ // 鍙戠エ鏂囦欢閾炬帴 (浼樺厛鍙� EleCloud_PDF)
+ String pdf = legacyData.get("EleCloud_PDF") != null ? legacyData.get("EleCloud_PDF").toString() : null;
+ String url = legacyData.get("InvoiceURL") != null ? legacyData.get("InvoiceURL").toString() : null;
+ invoice.setInvoiceUrl(pdf != null && !pdf.isEmpty() ? pdf : url);
+
+ if (legacyData.get("AuditTime") != null) {
+ invoice.setAuditTime((Date) legacyData.get("AuditTime"));
+ }
+ if (legacyData.get("AuditMakeout") != null) {
+ invoice.setAuditRemarks(legacyData.get("AuditMakeout").toString());
+ }
+
+ sysInvoiceMapper.updateSysInvoice(invoice);
+ }
+ }
+ }
+
+ /**
+ * 鏌ヨ鍙�夋嫨鐨勪换鍔″垪琛�
+ */
+ @Override
+ public List<Map<String, Object>> selectSelectableTasks(Long userId, String searchKeyword, String serviceOrdClass) {
+ return sysInvoiceMapper.selectSelectableTasks(userId, searchKeyword, serviceOrdClass);
+ }
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/LegacyInvoiceMapper.xml b/ruoyi-system/src/main/resources/mapper/system/LegacyInvoiceMapper.xml
new file mode 100644
index 0000000..b36a98b
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/LegacyInvoiceMapper.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.LegacyInvoiceMapper">
+
+ <insert id="insertLegacyInvoice" parameterType="Map" useGeneratedKeys="true" keyProperty="InvoiceID">
+ INSERT INTO InvoiceData (
+ ServiceOrderIDPK, InvoiceType, InvoiceName, InvoiceMakeout,
+ InvoiceCompanyPhone, InvoiceCompanyID, InvoiceCompanyAdd,
+ InvoiceCompanyBank, InvoiceCompanyBankNo, InvoiceZipCode,
+ Invoice_strAdd, Invoice_strName, Invoice_strPhone, Invoice_strEmail,
+ ApplicationTime, AuditStatus, InvoiceMoney, ApplyOAID
+ ) VALUES (
+ #{ServiceOrderIDPK}, #{InvoiceType}, #{InvoiceName}, #{InvoiceMakeout},
+ #{InvoiceCompanyPhone}, #{InvoiceCompanyID}, #{InvoiceCompanyAdd},
+ #{InvoiceCompanyBank}, #{InvoiceCompanyBankNo}, #{InvoiceZipCode},
+ #{Invoice_strAdd}, #{Invoice_strName}, #{Invoice_strPhone}, #{Invoice_strEmail},
+ GETDATE(), 0, #{InvoiceMoney}, #{ApplyOAID}
+ )
+ </insert>
+
+ <select id="selectUpdatedInvoices" parameterType="String" resultType="Map">
+ SELECT
+ InvoiceID, ServiceOrderIDPK, AuditStatus, AuditTime, AuditMakeout,
+ InvoiceNo, InvoiceURL, InvoiceOddNo, EleCloud_PDF, EleCloud_Time
+ FROM InvoiceData
+ WHERE AuditTime > #{lastSyncTime} OR EleCloud_Time > #{lastSyncTime}
+ </select>
+
+ <select id="selectLegacyInvoiceByServiceOrderId" parameterType="Long" resultType="Map">
+ SELECT * FROM InvoiceData WHERE ServiceOrderIDPK = #{serviceOrderId}
+ </select>
+</mapper>
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysInvoiceMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysInvoiceMapper.xml
new file mode 100644
index 0000000..a885373
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysInvoiceMapper.xml
@@ -0,0 +1,302 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysInvoiceMapper">
+
+ <resultMap type="SysInvoice" id="SysInvoiceResult">
+ <result property="invoiceId" column="invoice_id" />
+ <result property="serviceOrderId" column="service_order_id" />
+ <result property="legacyServiceOrderId" column="legacy_service_order_id" />
+ <result property="invoiceType" column="invoice_type" />
+ <result property="invoiceName" column="invoice_name" />
+ <result property="invoiceMoney" column="invoice_money" />
+ <result property="invoiceRemarks" column="invoice_remarks" />
+ <result property="companyAddress" column="company_address" />
+ <result property="companyBank" column="company_bank" />
+ <result property="companyBankNo" column="company_bank_no" />
+ <result property="zipCode" column="zip_code" />
+ <result property="mailAddress" column="mail_address" />
+ <result property="contactName" column="contact_name" />
+ <result property="contactPhone" column="contact_phone" />
+ <result property="contactEmail" column="contact_email" />
+ <result property="status" column="status" />
+ <result property="invoiceNo" column="invoice_no" />
+ <result property="invoiceUrl" column="invoice_url" />
+ <result property="applyUserId" column="apply_user_id" />
+ <result property="applyTime" column="apply_time" />
+ <result property="auditUserId" column="audit_user_id" />
+ <result property="auditTime" column="audit_time" />
+ <result property="auditRemarks" column="audit_remarks" />
+ <result property="syncStatus" column="sync_status" />
+ <result property="legacyInvoiceId" column="legacy_invoice_id" />
+ <result property="serviceCode" column="serviceCode" />
+ </resultMap>
+
+ <sql id="selectSysInvoiceVo">
+ select invoice_id, service_order_id, legacy_service_order_id, invoice_type, invoice_name, invoice_money, invoice_remarks, company_address, company_bank, company_bank_no, zip_code, mail_address, contact_name, contact_phone, contact_email, status, invoice_no, invoice_url, apply_user_id, apply_time, audit_user_id, audit_time, audit_remarks, sync_status, legacy_invoice_id from sys_invoice
+ </sql>
+
+ <select id="selectSysInvoiceList" parameterType="SysInvoice" resultMap="SysInvoiceResult">
+ select i.invoice_id, i.service_order_id, i.legacy_service_order_id, i.invoice_type, i.invoice_name,
+ i.invoice_money, i.invoice_remarks, i.company_address, i.company_bank, i.company_bank_no,
+ i.zip_code, i.mail_address, i.contact_name, i.contact_phone, i.contact_email,
+ i.status, i.invoice_no, i.invoice_url, i.apply_user_id, i.apply_time,
+ i.audit_user_id, i.audit_time, i.audit_remarks, i.sync_status, i.legacy_invoice_id,
+ CONCAT(
+ IFNULL(e.legacy_service_ord_class, ''),
+ DATE_FORMAT(e.legacy_service_ns_time, '%Y%m%d'),
+ '-',
+ LPAD(IFNULL(e.legacy_service_ord_no, ''), 3, '0')
+ ) as serviceCode
+ from sys_invoice i
+ LEFT JOIN sys_task_emergency e ON i.service_order_id = e.task_id
+ <where>
+ <if test="serviceOrderId != null "> and i.service_order_id = #{serviceOrderId}</if>
+ <if test="legacyServiceOrderId != null "> and i.legacy_service_order_id = #{legacyServiceOrderId}</if>
+ <if test="invoiceType != null "> and i.invoice_type = #{invoiceType}</if>
+ <if test="invoiceName != null and invoiceName != ''"> and i.invoice_name like concat('%', #{invoiceName}, '%')</if>
+ <if test="status != null "> and i.status = #{status}</if>
+ <!-- serviceCode鏌ヨ -->
+ <if test="params.serviceCode != null and params.serviceCode != ''">
+ AND CONCAT(
+ IFNULL(e.legacy_service_ord_class, ''),
+ DATE_FORMAT(e.legacy_service_ns_time, '%Y%m%d'),
+ '-',
+ LPAD(IFNULL(e.legacy_service_ord_no, ''), 3, '0')
+ ) LIKE CONCAT('%', #{params.serviceCode}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 寮�濮嬫椂闂存绱� -->
+ AND date_format(i.apply_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 缁撴潫鏃堕棿妫�绱� -->
+ AND date_format(i.apply_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
+ </if>
+ <!-- 鍒嗗叕鍙告煡璇㈤�昏緫闇�瑕佸叧鑱旀湇鍔″崟琛� -->
+ <if test="params.serviceOrdClass != null and params.serviceOrdClass != ''">
+ AND i.service_order_id IN (SELECT ServiceOrdID FROM service_order WHERE ServiceOrdClass = #{params.serviceOrdClass})
+ </if>
+ </where>
+ </select>
+
+ <!-- App绔煡璇㈡垜鐨勫彂绁ㄥ垪琛紝杩斿洖Map鍖呭惈serviceCode -->
+ <select id="selectMyInvoiceList" parameterType="SysInvoice" resultType="map">
+ SELECT
+ i.invoice_id as invoiceId,
+ i.service_order_id as serviceOrderId,
+ i.legacy_service_order_id as legacyServiceOrderId,
+ i.invoice_type as invoiceType,
+ i.invoice_name as invoiceName,
+ i.invoice_money as invoiceMoney,
+ i.invoice_remarks as invoiceRemarks,
+ i.company_address as companyAddress,
+ i.company_bank as companyBank,
+ i.company_bank_no as companyBankNo,
+ i.zip_code as zipCode,
+ i.mail_address as mailAddress,
+ i.contact_name as contactName,
+ i.contact_phone as contactPhone,
+ i.contact_email as contactEmail,
+ i.status,
+ i.invoice_no as invoiceNo,
+ i.invoice_url as invoiceUrl,
+ i.apply_user_id as applyUserId,
+ i.apply_time as applyTime,
+ i.audit_user_id as auditUserId,
+ i.audit_time as auditTime,
+ i.audit_remarks as auditRemarks,
+ i.sync_status as syncStatus,
+ i.legacy_invoice_id as legacyInvoiceId,
+ -- 鏋勫缓 serviceCode
+ CONCAT(
+ IFNULL(e.legacy_service_ord_class, ''),
+ DATE_FORMAT(e.legacy_service_ns_time, '%Y%m%d'),
+ '-',
+ LPAD(IFNULL(e.legacy_service_ord_no, ''), 3, '0')
+ ) as serviceCode
+ FROM sys_invoice i
+ LEFT JOIN sys_task_emergency e ON i.service_order_id = e.task_id
+ <where>
+ <if test="serviceOrderId != null "> and i.service_order_id = #{serviceOrderId}</if>
+ <if test="legacyServiceOrderId != null "> and i.legacy_service_order_id = #{legacyServiceOrderId}</if>
+ <if test="invoiceType != null "> and i.invoice_type = #{invoiceType}</if>
+ <if test="invoiceName != null and invoiceName != ''"> and i.invoice_name like concat('%', #{invoiceName}, '%')</if>
+ <if test="status != null "> and i.status = #{status}</if>
+ <!-- 鏈嶅姟鍗曞彿鎼滅储 -->
+ <if test="params.serviceCode != null and params.serviceCode != ''">
+ AND CONCAT(
+ IFNULL(e.legacy_service_ord_class, ''),
+ DATE_FORMAT(e.legacy_service_ns_time, '%Y%m%d'),
+ '-',
+ IFNULL(e.legacy_service_ord_no, '')
+ ) LIKE CONCAT('%', #{params.serviceCode}, '%')
+ </if>
+ <if test="params.beginTime != null and params.beginTime != ''"><!-- 寮�濮嬫椂闂存绱� -->
+ AND date_format(i.apply_time,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
+ </if>
+ <if test="params.endTime != null and params.endTime != ''"><!-- 缁撴潫鏃堕棿妫�绱� -->
+ AND date_format(i.apply_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
+ </if>
+ <!-- 鍒嗗叕鍙告煡璇㈤�昏緫闇�瑕佸叧鑱旀湇鍔″崟琛� -->
+ <if test="params.serviceOrdClass != null and params.serviceOrdClass != ''">
+ AND i.service_order_id IN (SELECT ServiceOrdID FROM service_order WHERE ServiceOrdClass = #{params.serviceOrdClass})
+ </if>
+ </where>
+ </select>
+
+ <select id="selectSysInvoiceByInvoiceId" parameterType="Long" resultMap="SysInvoiceResult">
+ <include refid="selectSysInvoiceVo"/>
+ where invoice_id = #{invoiceId}
+ </select>
+
+ <select id="selectSysInvoiceByLegacyId" parameterType="Integer" resultMap="SysInvoiceResult">
+ <include refid="selectSysInvoiceVo"/>
+ where legacy_invoice_id = #{legacyInvoiceId}
+ </select>
+
+ <select id="selectSelectableTasks" resultType="Map">
+ SELECT
+ t.task_id as taskId,
+ t.task_code as taskCode,
+ e.legacy_service_ord_id as legacyServiceOrderId,
+ t.actual_end_time as completionTime,
+ t.departure_address as departure,
+ t.destination_address as destination,
+ e.legacy_service_ord_class as legacyServiceOrdClass,
+ e.legacy_service_ns_time as legacyServiceNsTime,
+ e.legacy_service_ord_no as legacyServiceOrdNo,
+ IFNULL(e.transfer_price, 0) as transferPrice,
+ CONCAT(
+ IFNULL(e.legacy_service_ord_class, ''),
+ DATE_FORMAT(e.legacy_service_ns_time, '%Y%m%d'),
+ '-',
+ LPAD(IFNULL(e.legacy_service_ord_no, ''), 3, '0')
+ ) as serviceCode
+ FROM sys_task t
+ INNER JOIN sys_task_emergency e ON t.task_id = e.task_id
+ WHERE t.task_type = 'EMERGENCY_TRANSFER'
+ AND t.task_status = 'COMPLETED'
+ AND (t.creator_id = #{userId} OR t.assignee_id = #{userId})
+ AND t.task_id NOT IN (
+ SELECT service_order_id
+ FROM sys_invoice
+ WHERE status IN (0, 1)
+ )
+ <if test="searchKeyword != null and searchKeyword != ''">
+ AND (
+ t.task_code LIKE CONCAT('%', #{searchKeyword}, '%')
+ OR e.legacy_service_ord_no LIKE CONCAT('%', #{searchKeyword}, '%')
+ OR CONCAT(
+ IFNULL(e.legacy_service_ord_class, ''),
+ DATE_FORMAT(e.legacy_service_ns_time, '%Y%m%d'),
+ '-',
+ LPAD(IFNULL(e.legacy_service_ord_no, ''), 3, '0')
+
+ ) LIKE CONCAT('%', #{searchKeyword}, '%')
+ )
+ </if>
+ <if test="serviceOrdClass != null and serviceOrdClass != ''">
+ AND e.legacy_service_ord_class = #{serviceOrdClass}
+ </if>
+ ORDER BY t.actual_end_time DESC
+ LIMIT 100
+ </select>
+
+ <insert id="insertSysInvoice" parameterType="SysInvoice" useGeneratedKeys="true" keyProperty="invoiceId">
+ insert into sys_invoice
+ <trim prefix="(" suffix=")" suffixOverrides=",">
+ <if test="serviceOrderId != null">service_order_id,</if>
+ <if test="legacyServiceOrderId != null">legacy_service_order_id,</if>
+ <if test="invoiceType != null">invoice_type,</if>
+ <if test="invoiceName != null and invoiceName != ''">invoice_name,</if>
+ <if test="invoiceMoney != null">invoice_money,</if>
+ <if test="invoiceRemarks != null">invoice_remarks,</if>
+ <if test="companyAddress != null">company_address,</if>
+ <if test="companyBank != null">company_bank,</if>
+ <if test="companyBankNo != null">company_bank_no,</if>
+ <if test="zipCode != null">zip_code,</if>
+ <if test="mailAddress != null">mail_address,</if>
+ <if test="contactName != null">contact_name,</if>
+ <if test="contactPhone != null">contact_phone,</if>
+ <if test="contactEmail != null">contact_email,</if>
+ <if test="status != null">status,</if>
+ <if test="invoiceNo != null">invoice_no,</if>
+ <if test="invoiceUrl != null">invoice_url,</if>
+ <if test="applyUserId != null">apply_user_id,</if>
+ <if test="applyTime != null">apply_time,</if>
+ <if test="auditUserId != null">audit_user_id,</if>
+ <if test="auditTime != null">audit_time,</if>
+ <if test="auditRemarks != null">audit_remarks,</if>
+ <if test="syncStatus != null">sync_status,</if>
+ <if test="legacyInvoiceId != null">legacy_invoice_id,</if>
+ </trim>
+ <trim prefix="values (" suffix=")" suffixOverrides=",">
+ <if test="serviceOrderId != null">#{serviceOrderId},</if>
+ <if test="legacyServiceOrderId != null">#{legacyServiceOrderId},</if>
+ <if test="invoiceType != null">#{invoiceType},</if>
+ <if test="invoiceName != null and invoiceName != ''">#{invoiceName},</if>
+ <if test="invoiceMoney != null">#{invoiceMoney},</if>
+ <if test="invoiceRemarks != null">#{invoiceRemarks},</if>
+ <if test="companyAddress != null">#{companyAddress},</if>
+ <if test="companyBank != null">#{companyBank},</if>
+ <if test="companyBankNo != null">#{companyBankNo},</if>
+ <if test="zipCode != null">#{zipCode},</if>
+ <if test="mailAddress != null">#{mailAddress},</if>
+ <if test="contactName != null">#{contactName},</if>
+ <if test="contactPhone != null">#{contactPhone},</if>
+ <if test="contactEmail != null">#{contactEmail},</if>
+ <if test="status != null">#{status},</if>
+ <if test="invoiceNo != null">#{invoiceNo},</if>
+ <if test="invoiceUrl != null">#{invoiceUrl},</if>
+ <if test="applyUserId != null">#{applyUserId},</if>
+ <if test="applyTime != null">#{applyTime},</if>
+ <if test="auditUserId != null">#{auditUserId},</if>
+ <if test="auditTime != null">#{auditTime},</if>
+ <if test="auditRemarks != null">#{auditRemarks},</if>
+ <if test="syncStatus != null">#{syncStatus},</if>
+ <if test="legacyInvoiceId != null">#{legacyInvoiceId},</if>
+ </trim>
+ </insert>
+
+ <update id="updateSysInvoice" parameterType="SysInvoice">
+ update sys_invoice
+ <trim prefix="SET" suffixOverrides=",">
+ <if test="serviceOrderId != null">service_order_id = #{serviceOrderId},</if>
+ <if test="legacyServiceOrderId != null">legacy_service_order_id = #{legacyServiceOrderId},</if>
+ <if test="invoiceType != null">invoice_type = #{invoiceType},</if>
+ <if test="invoiceName != null and invoiceName != ''">invoice_name = #{invoiceName},</if>
+ <if test="invoiceMoney != null">invoice_money = #{invoiceMoney},</if>
+ <if test="invoiceRemarks != null">invoice_remarks = #{invoiceRemarks},</if>
+ <if test="companyAddress != null">company_address = #{companyAddress},</if>
+ <if test="companyBank != null">company_bank = #{companyBank},</if>
+ <if test="companyBankNo != null">company_bank_no = #{companyBankNo},</if>
+ <if test="zipCode != null">zip_code = #{zipCode},</if>
+ <if test="mailAddress != null">mail_address = #{mailAddress},</if>
+ <if test="contactName != null">contact_name = #{contactName},</if>
+ <if test="contactPhone != null">contact_phone = #{contactPhone},</if>
+ <if test="contactEmail != null">contact_email = #{contactEmail},</if>
+ <if test="status != null">status = #{status},</if>
+ <if test="invoiceNo != null">invoice_no = #{invoiceNo},</if>
+ <if test="invoiceUrl != null">invoice_url = #{invoiceUrl},</if>
+ <if test="applyUserId != null">apply_user_id = #{applyUserId},</if>
+ <if test="applyTime != null">apply_time = #{applyTime},</if>
+ <if test="auditUserId != null">audit_user_id = #{auditUserId},</if>
+ <if test="auditTime != null">audit_time = #{auditTime},</if>
+ <if test="auditRemarks != null">audit_remarks = #{auditRemarks},</if>
+ <if test="syncStatus != null">sync_status = #{syncStatus},</if>
+ <if test="legacyInvoiceId != null">legacy_invoice_id = #{legacyInvoiceId},</if>
+ </trim>
+ where invoice_id = #{invoiceId}
+ </update>
+
+ <delete id="deleteSysInvoiceByInvoiceId" parameterType="Long">
+ delete from sys_invoice where invoice_id = #{invoiceId}
+ </delete>
+
+ <delete id="deleteSysInvoiceByInvoiceIds" parameterType="String">
+ delete from sys_invoice where invoice_id in
+ <foreach item="invoiceId" collection="array" open="(" separator="," close=")">
+ #{invoiceId}
+ </foreach>
+ </delete>
+</mapper>
diff --git a/ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml b/ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml
index 73671e3..c56d5d6 100644
--- a/ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/VehicleGpsSegmentMileageMapper.xml
@@ -79,7 +79,7 @@
segment_distance, gps_point_count, gps_ids, task_id, task_code, calculate_method
FROM tb_vehicle_gps_segment_mileage
WHERE vehicle_id = #{vehicleId}
- AND segment_start_time <= #{endTime}
+ AND segment_start_time between #{startDate} and #{endDate}
AND segment_end_time >= #{startTime}
AND segment_distance > 0
ORDER BY segment_start_time
diff --git a/ruoyi-ui/src/api/system/dept.js b/ruoyi-ui/src/api/system/dept.js
index 7817d19..71aef07 100644
--- a/ruoyi-ui/src/api/system/dept.js
+++ b/ruoyi-ui/src/api/system/dept.js
@@ -59,6 +59,14 @@
})
}
+// 鎸塐A璁㈠崟绫诲埆鏌ヨ鍒嗗叕鍙稿垪琛紙鍒悕锛�
+export function listBranchByOaOrderClass() {
+ return request({
+ url: '/system/dept/branch/by-oa',
+ method: 'get'
+ })
+}
+
// 鎸夊閮ㄤ紶鍏ョ殑鐢ㄦ埛ID杩斿洖鍏跺彲绠$悊鍒嗗叕鍙稿垪琛�
export function listBranchByUser(userId) {
return request({
diff --git a/ruoyi-ui/src/api/system/invoice.js b/ruoyi-ui/src/api/system/invoice.js
new file mode 100644
index 0000000..dd4c35e
--- /dev/null
+++ b/ruoyi-ui/src/api/system/invoice.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鍙戠エ鐢宠鍒楄〃
+export function listInvoice(query) {
+ return request({
+ url: '/system/invoice/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ鍙戠エ鐢宠璇︾粏
+export function getInvoice(invoiceId) {
+ return request({
+ url: '/system/invoice/' + invoiceId,
+ method: 'get'
+ })
+}
+
+// 鏂板鍙戠エ鐢宠
+export function addInvoice(data) {
+ return request({
+ url: '/system/invoice',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼鍙戠エ鐢宠
+export function updateInvoice(data) {
+ return request({
+ url: '/system/invoice',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎鍙戠エ鐢宠
+export function delInvoice(invoiceId) {
+ return request({
+ url: '/system/invoice/' + invoiceId,
+ method: 'delete'
+ })
+}
+
+// 鎵嬪姩鍚屾鐘舵��
+export function syncInvoiceStatus() {
+ return request({
+ url: '/system/invoice/syncStatus',
+ method: 'get'
+ })
+}
+
+// 鍚屾鍗曚釜鍙戠エ鍒版棫绯荤粺
+export function syncInvoiceToLegacy(invoiceId) {
+ return request({
+ url: '/system/invoice/syncToLegacy/' + invoiceId,
+ method: 'post'
+ })
+}
diff --git a/ruoyi-ui/src/api/task.js b/ruoyi-ui/src/api/task.js
index b72da8d..1bb98c6 100644
--- a/ruoyi-ui/src/api/task.js
+++ b/ruoyi-ui/src/api/task.js
@@ -302,4 +302,12 @@
url: '/task/syncDispatchOrder/' + taskId,
method: 'post'
})
+}
+
+// 鎵嬪姩鍚屾浠诲姟鐘舵�佸埌鏃х郴缁�
+export function syncTaskStatus(taskId) {
+ return request({
+ url: '/task/syncTaskStatus/' + taskId,
+ method: 'post'
+ })
}
\ No newline at end of file
diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js
index 6266b3e..59f7fee 100644
--- a/ruoyi-ui/src/router/index.js
+++ b/ruoyi-ui/src/router/index.js
@@ -145,6 +145,27 @@
hidden: false,
name: 'H5TaskCreate',
meta: { title: '鍒涘缓浠诲姟' }
+ },
+ {
+ path: '/system/invoice/detail',
+ component: () => import('@/views/system/invoice/detail'),
+ name: 'InvoiceDetail',
+ hidden: true,
+ meta: { title: '鍙戠エ璇︽儏', activeMenu: '/system/invoice' }
+ },
+ {
+ path: '/system/invoice/audit',
+ component: () => import('@/views/system/invoice/audit'),
+ name: 'InvoiceAudit',
+ hidden: true,
+ meta: { title: '瀹℃牳鍙戠エ', activeMenu: '/system/invoice' }
+ },
+ {
+ path: '/system/invoice/apply',
+ component: () => import('@/views/system/invoice/apply'),
+ name: 'InvoiceApply',
+ hidden: true,
+ meta: { title: '鐢宠鍙戠エ', activeMenu: '/system/invoice' }
}
]
diff --git a/ruoyi-ui/src/views/system/invoice/apply.vue b/ruoyi-ui/src/views/system/invoice/apply.vue
new file mode 100644
index 0000000..06ed00c
--- /dev/null
+++ b/ruoyi-ui/src/views/system/invoice/apply.vue
@@ -0,0 +1,261 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span class="card-title">鍙戠エ鐢宠</span>
+ <el-button style="float: right; padding: 3px 0" type="text" @click="goBack">杩斿洖</el-button>
+ </div>
+
+ <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+ <!-- 浠诲姟淇℃伅灞曠ず -->
+ <el-divider>浠诲姟淇℃伅</el-divider>
+ <el-descriptions :column="2" border v-if="taskInfo.taskId">
+ <el-descriptions-item label="浠诲姟缂栧彿">{{ taskInfo.taskCode }}</el-descriptions-item>
+ <el-descriptions-item label="鏈嶅姟鍗曞彿">{{ taskInfo.serviceCode || taskInfo.legacyServiceOrderId || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍑哄彂鍦�" :span="2">{{ taskInfo.departure }}</el-descriptions-item>
+ <el-descriptions-item label="鐩殑鍦�" :span="2">{{ taskInfo.destination }}</el-descriptions-item>
+ <el-descriptions-item label="瀹屾垚鏃堕棿">{{ taskInfo.completionTime }}</el-descriptions-item>
+ <el-descriptions-item label="浠诲姟閲戦">
+ <span style="color: #F56C6C; font-weight: bold;">楼{{ taskInfo.transferPrice || '0.00' }}</span>
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鍙戠エ淇℃伅 -->
+ <el-divider>鍙戠エ淇℃伅</el-divider>
+
+ <el-form-item label="寮�绁ㄧ被鍨�" prop="invoiceType">
+ <el-radio-group v-model="form.invoiceType">
+ <el-radio :label="1">涓汉</el-radio>
+ <el-radio :label="2">浼佷笟</el-radio>
+ </el-radio-group>
+ </el-form-item>
+
+ <el-form-item label="鍙戠エ鎶ご" prop="invoiceName">
+ <el-input v-model="form.invoiceName" placeholder="璇疯緭鍏ュ彂绁ㄦ姮澶�" maxlength="200" />
+ </el-form-item>
+
+ <el-form-item label="鍙戠エ閲戦" prop="invoiceMoney">
+ <el-input
+ v-model="form.invoiceMoney"
+ placeholder="璇疯緭鍏ュ彂绁ㄩ噾棰�"
+ type="number"
+ :max="maxInvoiceMoney"
+ >
+ <template slot="append">鍏�</template>
+ </el-input>
+ <div v-if="maxInvoiceMoney" style="color: #909399; font-size: 12px; margin-top: 4px;">
+ 鍙敵璇烽噾棰濅笂闄愶細楼{{ maxInvoiceMoney }}
+ </div>
+ </el-form-item>
+
+ <el-form-item label="鍙戠エ澶囨敞">
+ <el-input v-model="form.invoiceRemarks" type="textarea" placeholder="璇疯緭鍏ュ娉�" maxlength="500" />
+ </el-form-item>
+
+ <!-- 浼佷笟淇℃伅锛堜粎浼佷笟绫诲瀷鏄剧ず锛� -->
+ <template v-if="form.invoiceType === 2">
+ <el-divider>浼佷笟淇℃伅</el-divider>
+
+ <el-form-item label="娉ㄥ唽鍦板潃" prop="companyAddress">
+ <el-input v-model="form.companyAddress" placeholder="璇疯緭鍏ヤ紒涓氭敞鍐屽湴鍧�" maxlength="200" />
+ </el-form-item>
+
+ <el-form-item label="寮�鎴烽摱琛�" prop="companyBank">
+ <el-input v-model="form.companyBank" placeholder="璇疯緭鍏ュ紑鎴烽摱琛�" maxlength="100" />
+ </el-form-item>
+
+ <el-form-item label="閾惰璐﹀彿" prop="companyBankNo">
+ <el-input v-model="form.companyBankNo" placeholder="璇疯緭鍏ラ摱琛岃处鍙�" maxlength="50" />
+ </el-form-item>
+ </template>
+
+ <!-- 閭瘎淇℃伅 -->
+ <el-divider>閭瘎淇℃伅</el-divider>
+
+ <el-form-item label="閭瘎鍦板潃" prop="mailAddress">
+ <el-input v-model="form.mailAddress" placeholder="璇疯緭鍏ラ偖瀵勫湴鍧�" maxlength="200" />
+ </el-form-item>
+
+ <el-form-item label="閭紪">
+ <el-input v-model="form.zipCode" placeholder="璇疯緭鍏ラ偖缂�" maxlength="10" />
+ </el-form-item>
+
+ <el-form-item label="鑱旂郴浜�" prop="contactName">
+ <el-input v-model="form.contactName" placeholder="璇疯緭鍏ヨ仈绯讳汉" maxlength="50" />
+ </el-form-item>
+
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="contactPhone">
+ <el-input v-model="form.contactPhone" placeholder="璇疯緭鍏ヨ仈绯荤數璇�" maxlength="20" />
+ </el-form-item>
+
+ <el-form-item label="鑱旂郴閭">
+ <el-input v-model="form.contactEmail" placeholder="璇疯緭鍏ヨ仈绯婚偖绠�" maxlength="100" />
+ </el-form-item>
+ </el-form>
+
+ <div class="action-bar" style="margin-top: 20px; text-align: center;">
+ <el-button @click="goBack">鍙栨秷</el-button>
+ <el-button type="primary" @click="submitForm">鎻愪氦鐢宠</el-button>
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script>
+import { addInvoice } from "@/api/system/invoice";
+
+export default {
+ name: "InvoiceApply",
+ data() {
+ return {
+ // 浠诲姟淇℃伅
+ taskInfo: {
+ taskId: null,
+ taskCode: null,
+ legacyServiceOrderId: null,
+ serviceCode: null,
+ departure: null,
+ destination: null,
+ completionTime: null,
+ transferPrice: null
+ },
+ // 琛ㄥ崟鏁版嵁
+ form: {
+ serviceOrderId: null,
+ legacyServiceOrderId: null, // 鏃х郴缁熸湇鍔″崟ID
+ invoiceType: 1,
+ invoiceName: null,
+ invoiceMoney: null,
+ invoiceRemarks: null,
+ companyAddress: null,
+ companyBank: null,
+ companyBankNo: null,
+ zipCode: null,
+ mailAddress: null,
+ contactName: null,
+ contactPhone: null,
+ contactEmail: null
+ },
+ // 鏈�澶у彂绁ㄩ噾棰�
+ maxInvoiceMoney: null,
+ // 琛ㄥ崟鏍¢獙
+ rules: {
+ invoiceType: [
+ { required: true, message: "璇烽�夋嫨寮�绁ㄧ被鍨�", trigger: "change" }
+ ],
+ invoiceName: [
+ { required: true, message: "璇疯緭鍏ュ彂绁ㄦ姮澶�", trigger: "blur" }
+ ],
+ invoiceMoney: [
+ { required: true, message: "璇疯緭鍏ュ彂绁ㄩ噾棰�", trigger: "blur" },
+ {
+ validator: (rule, value, callback) => {
+ if (!value || value <= 0) {
+ callback(new Error('鍙戠エ閲戦蹇呴』澶т簬0'));
+ } else if (this.maxInvoiceMoney && parseFloat(value) > this.maxInvoiceMoney) {
+ callback(new Error(`鍙戠エ閲戦涓嶈兘瓒呰繃浠诲姟閲戦楼${this.maxInvoiceMoney}`));
+ } else {
+ callback();
+ }
+ },
+ trigger: 'blur'
+ }
+ ],
+ companyAddress: [
+ { required: true, message: "璇疯緭鍏ヤ紒涓氭敞鍐屽湴鍧�", trigger: "blur" }
+ ],
+ companyBank: [
+ { required: true, message: "璇疯緭鍏ュ紑鎴烽摱琛�", trigger: "blur" }
+ ],
+ companyBankNo: [
+ { required: true, message: "璇疯緭鍏ラ摱琛岃处鍙�", trigger: "blur" }
+ ],
+ mailAddress: [
+ { required: true, message: "璇疯緭鍏ラ偖瀵勫湴鍧�", trigger: "blur" }
+ ],
+ contactName: [
+ { required: true, message: "璇疯緭鍏ヨ仈绯讳汉", trigger: "blur" }
+ ],
+ contactPhone: [
+ { required: true, message: "璇疯緭鍏ヨ仈绯荤數璇�", trigger: "blur" },
+ { pattern: /^1[3-9]\d{9}$/, message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", trigger: "blur" }
+ ]
+ }
+ };
+ },
+ created() {
+ // 浠� sessionStorage 鑾峰彇浠诲姟淇℃伅
+ const taskInfoStr = sessionStorage.getItem('invoiceTaskInfo');
+ if (taskInfoStr) {
+ this.taskInfo = JSON.parse(taskInfoStr);
+ this.form.serviceOrderId = this.taskInfo.taskId;
+ // 濡傛灉浠诲姟淇℃伅涓湁鏃х郴缁熸湇鍔″崟ID锛屼篃瑕佷繚瀛�
+ if (this.taskInfo.legacyServiceOrderId) {
+ this.form.legacyServiceOrderId = this.taskInfo.legacyServiceOrderId;
+ }
+
+ // 璁剧疆鏈�澶у彂绁ㄩ噾棰�
+ if (this.taskInfo.transferPrice) {
+ this.maxInvoiceMoney = parseFloat(this.taskInfo.transferPrice);
+ // 鑷姩甯﹀叆浠诲姟閲戦
+ this.form.invoiceMoney = this.maxInvoiceMoney;
+ }
+
+ // 娓呴櫎 sessionStorage
+ sessionStorage.removeItem('invoiceTaskInfo');
+ } else if (this.$route.query.taskId) {
+ // 濡傛灉娌℃湁 sessionStorage锛屽皾璇曚粠 query 鑾峰彇
+ this.$modal.msgError("缂哄皯浠诲姟淇℃伅锛岃浠庝换鍔¤鎯呴〉杩涘叆");
+ this.goBack();
+ }
+ },
+ methods: {
+ /** 鎻愪氦琛ㄥ崟 */
+ submitForm() {
+ this.$refs["form"].validate(valid => {
+ if (valid) {
+ // 浼佷笟绫诲瀷闇�瑕侀獙璇佷紒涓氫俊鎭�
+ if (this.form.invoiceType === 2) {
+ if (!this.form.companyAddress || !this.form.companyBank || !this.form.companyBankNo) {
+ this.$modal.msgError("璇峰畬鍠勪紒涓氫俊鎭�");
+ return;
+ }
+ }
+
+ // 鍐嶆楠岃瘉閲戦
+ const money = parseFloat(this.form.invoiceMoney);
+ if (this.maxInvoiceMoney && money > this.maxInvoiceMoney) {
+ this.$modal.msgError(`鍙戠エ閲戦涓嶈兘瓒呰繃浠诲姟閲戦楼${this.maxInvoiceMoney}`);
+ return;
+ }
+
+ addInvoice(this.form).then(response => {
+ this.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ setTimeout(() => {
+ this.goBack();
+ }, 1500);
+ });
+ }
+ });
+ },
+
+ /** 杩斿洖 */
+ goBack() {
+ this.$tab.closePage();
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+.card-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+}
+
+.action-bar {
+ border-top: 1px solid #EBEEF5;
+ padding-top: 20px;
+}
+</style>
diff --git a/ruoyi-ui/src/views/system/invoice/audit.vue b/ruoyi-ui/src/views/system/invoice/audit.vue
new file mode 100644
index 0000000..b8d47b3
--- /dev/null
+++ b/ruoyi-ui/src/views/system/invoice/audit.vue
@@ -0,0 +1,311 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span class="card-title">瀹℃牳鍙戠エ鐢宠</span>
+ <el-button style="float: right; padding: 3px 0" type="text" @click="goBack">杩斿洖</el-button>
+ </div>
+
+ <!-- 鐢宠淇℃伅锛堝彧璇伙級 -->
+ <el-form ref="viewForm" :model="invoice" label-width="120px" disabled>
+ <el-divider content-position="left">鐢宠淇℃伅</el-divider>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏈嶅姟鍗曞彿">
+ <el-input v-model="invoice.serviceCode" placeholder="鏆傛棤" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�绁ㄧ被鍨�">
+ <el-radio-group v-model="invoice.invoiceType">
+ <el-radio :label="1">涓汉</el-radio>
+ <el-radio :label="2">浼佷笟</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鍙戠エ鎶ご">
+ <el-input v-model="invoice.invoiceName" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ閲戦">
+ <el-input v-model="invoice.invoiceMoney">
+ <template slot="prepend">楼</template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽">
+ <el-input v-model="invoice.contactPhone" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20" v-if="invoice.invoiceType === 2">
+ <el-col :span="24">
+ <el-form-item label="娉ㄥ唽鍦板潃">
+ <el-input v-model="invoice.companyAddress" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20" v-if="invoice.invoiceType === 2">
+ <el-col :span="12">
+ <el-form-item label="寮�鎴烽摱琛�">
+ <el-input v-model="invoice.companyBank" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閾惰璐﹀彿">
+ <el-input v-model="invoice.companyBankNo" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="閭瘎鍦板潃">
+ <el-input v-model="invoice.mailAddress" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞">
+ <el-input type="textarea" v-model="invoice.invoiceRemarks" :rows="3" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <!-- 瀹℃牳淇℃伅 -->
+ <el-form ref="auditForm" :model="auditForm" :rules="rules" label-width="120px">
+ <el-divider content-position="left">瀹℃牳淇℃伅</el-divider>
+
+ <el-form-item label="瀹℃牳缁撴灉" prop="status">
+ <el-radio-group v-model="auditForm.status" @change="handleStatusChange">
+ <el-radio :label="1">閫氳繃</el-radio>
+ <el-radio :label="2">椹冲洖</el-radio>
+ </el-radio-group>
+ </el-form-item>
+
+ <!-- 瀹℃牳閫氳繃鏃朵笂浼犲彂绁� -->
+ <el-form-item
+ v-if="auditForm.status === 1"
+ label="鍙戠エ鏂囦欢"
+ prop="invoiceUrl"
+ :rules="[{ required: true, message: '璇蜂笂浼犲彂绁ㄦ枃浠�', trigger: 'change' }]"
+ >
+ <el-upload
+ class="upload-demo"
+ :action="uploadAction"
+ :headers="uploadHeaders"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :before-upload="beforeUpload"
+ :file-list="fileList"
+ :limit="1"
+ accept=".pdf,.jpg,.jpeg,.png"
+ >
+ <el-button size="small" type="primary">鐐瑰嚮涓婁紶</el-button>
+ <div slot="tip" class="el-upload__tip">鏀寔PDF銆丣PG銆丳NG鏍煎紡锛屼笖涓嶈秴杩�10MB</div>
+ </el-upload>
+ <div v-if="auditForm.invoiceUrl" style="margin-top: 10px;">
+ <el-link type="primary" :href="auditForm.invoiceUrl" target="_blank">鏌ョ湅宸蹭笂浼犲彂绁�</el-link>
+ </div>
+ </el-form-item>
+
+ <el-form-item v-if="auditForm.status === 1" label="鍙戠エ缂栧彿" prop="invoiceNo">
+ <el-input v-model="auditForm.invoiceNo" placeholder="璇疯緭鍏ュ彂绁ㄧ紪鍙�" />
+ </el-form-item>
+
+ <!-- 椹冲洖鏃跺繀濉┏鍥炲師鍥� -->
+ <el-form-item
+ label="瀹℃牳澶囨敞"
+ prop="auditRemarks"
+ :rules="auditForm.status === 2 ? [{ required: true, message: '璇疯緭鍏ラ┏鍥炲師鍥�', trigger: 'blur' }] : []"
+ >
+ <el-input
+ type="textarea"
+ v-model="auditForm.auditRemarks"
+ :rows="4"
+ :placeholder="auditForm.status === 2 ? '璇疯緭鍏ラ┏鍥炲師鍥狅紙蹇呭~锛�' : '璇疯緭鍏ュ鏍告剰瑙侊紙鍙�夛級'"
+ />
+ </el-form-item>
+ </el-form>
+
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="action-bar">
+ <el-button @click="goBack">鍙� 娑�</el-button>
+ <el-button type="primary" @click="submitAudit" :loading="submitting">鎻愪氦瀹℃牳</el-button>
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script>
+import { getInvoice, updateInvoice } from "@/api/system/invoice";
+import { getToken } from "@/utils/auth";
+import { Message } from 'element-ui';
+
+export default {
+ name: "InvoiceAudit",
+ data() {
+ return {
+ // 鍙戠エ淇℃伅
+ invoice: {},
+ // 瀹℃牳琛ㄥ崟
+ auditForm: {
+ invoiceId: null,
+ status: 1,
+ invoiceNo: null,
+ invoiceUrl: null,
+ auditRemarks: null
+ },
+ // 涓婁紶鐩稿叧
+ uploadAction: process.env.VUE_APP_BASE_API + "/common/upload",
+ uploadHeaders: { Authorization: "Bearer " + getToken() },
+ fileList: [],
+ // 鎻愪氦鐘舵��
+ submitting: false,
+ // 琛ㄥ崟楠岃瘉
+ rules: {
+ status: [{ required: true, message: "璇烽�夋嫨瀹℃牳缁撴灉", trigger: "change" }]
+ }
+ };
+ },
+ created() {
+ const invoiceId = this.$route.query.invoiceId;
+ if (invoiceId) {
+ this.getDetail(invoiceId);
+ } else {
+ this.$modal.msgError("缂哄皯鍙戠エID鍙傛暟");
+ this.goBack();
+ }
+ },
+ methods: {
+ /** 鑾峰彇鍙戠エ璇︽儏 */
+ getDetail(invoiceId) {
+ getInvoice(invoiceId).then(response => {
+ this.invoice = response.data;
+ this.auditForm.invoiceId = response.data.invoiceId;
+
+ // 濡傛灉宸叉湁鍙戠エ鏂囦欢锛屾樉绀哄湪鏂囦欢鍒楄〃涓�
+ if (response.data.invoiceUrl) {
+ this.auditForm.invoiceUrl = response.data.invoiceUrl;
+ this.fileList = [{
+ name: '宸蹭笂浼犲彂绁�',
+ url: response.data.invoiceUrl
+ }];
+ }
+ }).catch(() => {
+ this.$modal.msgError("鑾峰彇鍙戠エ璇︽儏澶辫触");
+ this.goBack();
+ });
+ },
+
+ /** 鐘舵�佸彉鍖栧鐞� */
+ handleStatusChange(value) {
+ // 鍒囨崲鐘舵�佹椂娓呯┖瀹℃牳澶囨敞
+ this.auditForm.auditRemarks = '';
+ },
+
+ /** 鏂囦欢涓婁紶鍓嶉獙璇� */
+ beforeUpload(file) {
+ const isLt10M = file.size / 1024 / 1024 < 10;
+ if (!isLt10M) {
+ Message.error('涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!');
+ return false;
+ }
+ const fileType = file.type;
+ const isPDF = fileType === 'application/pdf';
+ const isImage = fileType.startsWith('image/');
+ if (!isPDF && !isImage) {
+ Message.error('鍙兘涓婁紶PDF鎴栧浘鐗囨枃浠�!');
+ return false;
+ }
+ return true;
+ },
+
+ /** 鏂囦欢涓婁紶鎴愬姛 */
+ handleUploadSuccess(response, file, fileList) {
+ if (response.code === 200) {
+ this.auditForm.invoiceUrl = response.url || response.fileName;
+ this.fileList = fileList;
+ Message.success('涓婁紶鎴愬姛');
+ } else {
+ Message.error(response.msg || '涓婁紶澶辫触');
+ }
+ },
+
+ /** 鏂囦欢涓婁紶澶辫触 */
+ handleUploadError(err, file, fileList) {
+ Message.error('涓婁紶澶辫触锛岃閲嶈瘯');
+ console.error(err);
+ },
+
+ /** 鎻愪氦瀹℃牳 */
+ submitAudit() {
+ this.$refs["auditForm"].validate(valid => {
+ if (valid) {
+ // 瀹℃牳閫氳繃鏃跺繀椤讳笂浼犲彂绁�
+ if (this.auditForm.status === 1 && !this.auditForm.invoiceUrl) {
+ this.$modal.msgError('瀹℃牳閫氳繃鏃跺繀椤讳笂浼犲彂绁ㄦ枃浠�');
+ return;
+ }
+ // 椹冲洖鏃跺繀椤诲~鍐欓┏鍥炲師鍥�
+ if (this.auditForm.status === 2 && !this.auditForm.auditRemarks) {
+ this.$modal.msgError('椹冲洖鏃跺繀椤诲~鍐欓┏鍥炲師鍥�');
+ return;
+ }
+
+ this.submitting = true;
+ updateInvoice(this.auditForm).then(response => {
+ this.$modal.msgSuccess("瀹℃牳鎴愬姛");
+ this.goBack();
+ }).catch(() => {
+ this.submitting = false;
+ });
+ }
+ });
+ },
+
+ /** 杩斿洖鍒楄〃 */
+ goBack() {
+ this.$tab.closePage();
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+.card-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+}
+
+.action-bar {
+ border-top: 1px solid #EBEEF5;
+ padding-top: 20px;
+ text-align: center;
+}
+
+::v-deep .el-form-item__label {
+ font-weight: 500;
+}
+
+::v-deep .el-divider__text {
+ font-size: 16px;
+ font-weight: bold;
+ color: #409EFF;
+}
+</style>
diff --git a/ruoyi-ui/src/views/system/invoice/detail.vue b/ruoyi-ui/src/views/system/invoice/detail.vue
new file mode 100644
index 0000000..f7ed8f3
--- /dev/null
+++ b/ruoyi-ui/src/views/system/invoice/detail.vue
@@ -0,0 +1,279 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span class="card-title">鍙戠エ鐢宠璇︽儏</span>
+ <el-button style="float: right; padding: 3px 0" type="text" @click="goBack">杩斿洖</el-button>
+ </div>
+
+ <el-descriptions :column="2" border>
+ <!-- 鍩烘湰淇℃伅 -->
+ <el-descriptions-item label="鍙戠エID">{{ invoice.invoiceId }}</el-descriptions-item>
+ <el-descriptions-item label="鏈嶅姟鍗曞彿">{{ invoice.serviceCode || invoice.legacyServiceOrderId || '-' }}</el-descriptions-item>
+
+ <el-descriptions-item label="寮�绁ㄧ被鍨�">
+ <el-tag :type="invoice.invoiceType === 2 ? 'success' : 'info'" size="small">
+ {{ invoice.invoiceType === 2 ? '浼佷笟' : '涓汉' }}
+ </el-tag>
+ </el-descriptions-item>
+
+ <el-descriptions-item label="鐢宠鐘舵��">
+ <el-tag :type="invoice.status === 1 ? 'success' : (invoice.status === 2 ? 'danger' : 'warning')" size="small">
+ {{ statusFormat(invoice.status) }}
+ </el-tag>
+ </el-descriptions-item>
+
+ <!-- 鍙戠エ淇℃伅 -->
+ <el-descriptions-item label="鍙戠エ鎶ご" :span="2">{{ invoice.invoiceName }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戠エ閲戦">
+ <span class="text-price">楼{{ formatMoney(invoice.invoiceMoney) }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戠エ缂栧彿">{{ invoice.invoiceNo || '-' }}</el-descriptions-item>
+
+ <!-- 鑱旂郴淇℃伅 -->
+ <el-descriptions-item label="鑱旂郴浜�">{{ invoice.contactName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴鐢佃瘽">{{ invoice.contactPhone || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴閭" :span="2">{{ invoice.contactEmail || '-' }}</el-descriptions-item>
+
+ <!-- 浼佷笟淇℃伅锛堜粎浼佷笟绫诲瀷鏄剧ず锛� -->
+ <template v-if="invoice.invoiceType === 2">
+ <el-descriptions-item label="娉ㄥ唽鍦板潃" :span="2">{{ invoice.companyAddress || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="寮�鎴烽摱琛�">{{ invoice.companyBank || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閾惰璐﹀彿">{{ invoice.companyBankNo || '-' }}</el-descriptions-item>
+ </template>
+
+ <!-- 閭瘎淇℃伅 -->
+ <el-descriptions-item label="閭瘎鍦板潃" :span="2">{{ invoice.mailAddress || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閭紪">{{ invoice.zipCode || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞" :span="2">{{ invoice.invoiceRemarks || '-' }}</el-descriptions-item>
+
+ <!-- 鏃堕棿淇℃伅 -->
+ <el-descriptions-item label="鐢宠鏃堕棿">{{ parseTime(invoice.applyTime) }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳鏃堕棿">{{ parseTime(invoice.auditTime) || '-' }}</el-descriptions-item>
+
+ <!-- 瀹℃牳淇℃伅 -->
+ <el-descriptions-item label="瀹℃牳澶囨敞" :span="2">
+ <span :class="invoice.status === 2 ? 'text-danger' : ''">{{ invoice.auditRemarks || '-' }}</span>
+ </el-descriptions-item>
+
+ <!-- 鍚屾鐘舵�� -->
+ <el-descriptions-item label="鍚屾鐘舵��">
+ <el-tag
+ :type="invoice.syncStatus === 1 ? 'success' : (invoice.syncStatus === 2 ? 'danger' : 'info')"
+ size="small"
+ >
+ {{ syncStatusFormat(invoice.syncStatus) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏃х郴缁熷彂绁↖D">
+ <span v-if="invoice.legacyInvoiceId">{{ invoice.legacyInvoiceId }}</span>
+ <span v-else class="text-gray">鏈悓姝�</span>
+ </el-descriptions-item>
+
+ <!-- 鍙戠エ鏂囦欢 -->
+ <el-descriptions-item label="鍙戠エ鏂囦欢" :span="2" v-if="invoice.invoiceUrl">
+ <el-link type="primary" :href="getFullUrl(invoice.invoiceUrl)" target="_blank" icon="el-icon-view">
+ 鏌ョ湅鍙戠エ鏂囦欢
+ </el-link>
+ <el-link type="success" :href="getFullUrl(invoice.invoiceUrl)" :download="getFileName(invoice.invoiceUrl)" icon="el-icon-download" style="margin-left: 10px;">
+ 涓嬭浇鍙戠エ
+ </el-link>
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="action-bar" style="margin-top: 20px; text-align: center;">
+ <el-button @click="goBack">杩斿洖鍒楄〃</el-button>
+ <el-button
+ type="primary"
+ v-if="invoice.status === 0"
+ @click="handleAudit"
+ v-hasPermi="['system:invoice:edit']"
+ >
+ 瀹℃牳
+ </el-button>
+ <el-button
+ type="success"
+ v-if="invoice.invoiceUrl"
+ @click="handleDownload"
+ icon="el-icon-download"
+ >
+ 涓嬭浇鍙戠エ
+ </el-button>
+ <!-- 鍚屾鎸夐挳锛氬彧鏈夊凡閫氳繃涓旀湭鍚屾鎴栧悓姝ュけ璐ユ椂鏄剧ず -->
+ <el-button
+ type="warning"
+ v-if="invoice.status === 1 && (!invoice.legacyInvoiceId || invoice.syncStatus === 2)"
+ @click="handleSync"
+ :loading="syncing"
+ icon="el-icon-refresh"
+ v-hasPermi="['system:invoice:edit']"
+ >
+ {{ syncing ? '鍚屾涓�...' : '鍚屾鍒版棫绯荤粺' }}
+ </el-button>
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script>
+import { getInvoice, syncInvoiceToLegacy } from "@/api/system/invoice";
+
+export default {
+ name: "InvoiceDetail",
+ data() {
+ return {
+ // 鍙戠エ璇︽儏鏁版嵁
+ invoice: {
+ invoiceId: null,
+ serviceOrderId: null,
+ legacyServiceOrderId: null,
+ serviceCode: null,
+ invoiceType: null,
+ invoiceName: null,
+ invoiceMoney: null,
+ invoiceRemarks: null,
+ companyAddress: null,
+ companyBank: null,
+ companyBankNo: null,
+ zipCode: null,
+ mailAddress: null,
+ contactName: null,
+ contactPhone: null,
+ contactEmail: null,
+ status: null,
+ invoiceNo: null,
+ invoiceUrl: null,
+ applyTime: null,
+ auditTime: null,
+ auditRemarks: null,
+ syncStatus: null,
+ legacyInvoiceId: null
+ },
+ // 鍚屾鐘舵��
+ syncing: false
+ };
+ },
+ created() {
+ const invoiceId = this.$route.params.invoiceId || this.$route.query.invoiceId;
+ if (invoiceId) {
+ this.getDetail(invoiceId);
+ } else {
+ this.$modal.msgError("缂哄皯鍙戠エID鍙傛暟");
+ this.goBack();
+ }
+ },
+ methods: {
+ /** 鑾峰彇鍙戠エ璇︽儏 */
+ getDetail(invoiceId) {
+ getInvoice(invoiceId).then(response => {
+ this.invoice = response.data;
+ }).catch(() => {
+ this.$modal.msgError("鑾峰彇鍙戠エ璇︽儏澶辫触");
+ this.goBack();
+ });
+ },
+
+ /** 鐘舵�佹牸寮忓寲 */
+ statusFormat(status) {
+ const map = { 0: "寰呭鏍�", 1: "宸查�氳繃", 2: "宸查┏鍥�" };
+ return map[status] || "鏈煡";
+ },
+
+ /** 閲戦鏍煎紡鍖� */
+ formatMoney(money) {
+ if (money === null || money === undefined) return '0.00';
+ return Number(money).toFixed(2);
+ },
+
+ /** 鍚屾鐘舵�佹牸寮忓寲 */
+ syncStatusFormat(status) {
+ const map = { 0: "鏈悓姝�", 1: "宸插悓姝�", 2: "鍚屾澶辫触" };
+ return map[status] || "鏈煡";
+ },
+
+ /** 鑾峰彇瀹屾暣鐨勬枃浠禪RL */
+ getFullUrl(url) {
+ if (!url) return '';
+ if (url.startsWith('http')) {
+ return url;
+ }
+ return process.env.VUE_APP_BASE_API + url;
+ },
+
+ /** 鑾峰彇鏂囦欢鍚� */
+ getFileName(url) {
+ if (!url) return '鍙戠エ鏂囦欢';
+ const parts = url.split('/');
+ return parts[parts.length - 1] || '鍙戠エ鏂囦欢';
+ },
+
+ /** 杩斿洖鍒楄〃 */
+ goBack() {
+ this.$tab.closePage();
+ },
+
+ /** 璺宠浆鍒板鏍搁〉闈� */
+ handleAudit() {
+ this.$router.push({
+ path: '/system/invoice/audit',
+ query: { invoiceId: this.invoice.invoiceId }
+ });
+ },
+
+ /** 涓嬭浇鍙戠エ */
+ handleDownload() {
+ const url = this.getFullUrl(this.invoice.invoiceUrl);
+ window.open(url);
+ },
+
+ /** 鍚屾鍒版棫绯荤粺 */
+ handleSync() {
+ this.$modal.confirm('纭灏嗚鍙戠エ鐢宠鍚屾鍒版棫绯荤粺鍚楋紵').then(() => {
+ this.syncing = true;
+ syncInvoiceToLegacy(this.invoice.invoiceId).then(response => {
+ this.$modal.msgSuccess('鍚屾鎴愬姛');
+ // 閲嶆柊鍔犺浇璇︽儏
+ this.getDetail(this.invoice.invoiceId);
+ }).catch(() => {
+ this.$modal.msgError('鍚屾澶辫触');
+ }).finally(() => {
+ this.syncing = false;
+ });
+ }).catch(() => {});
+ }
+ }
+};
+</script>
+
+<style scoped lang="scss">
+.card-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+}
+
+.text-price {
+ color: #F56C6C;
+ font-size: 16px;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #F56C6C;
+}
+
+.text-gray {
+ color: #909399;
+}
+
+.action-bar {
+ border-top: 1px solid #EBEEF5;
+ padding-top: 20px;
+}
+
+::v-deep .el-descriptions-item__label {
+ font-weight: bold;
+ width: 120px;
+}
+</style>
diff --git a/ruoyi-ui/src/views/system/invoice/index.vue b/ruoyi-ui/src/views/system/invoice/index.vue
new file mode 100644
index 0000000..7ba45ca
--- /dev/null
+++ b/ruoyi-ui/src/views/system/invoice/index.vue
@@ -0,0 +1,265 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
+ <el-form-item label="鏈嶅姟鍗曞彿" prop="serviceCode">
+ <el-input
+ v-model="queryParams.serviceCode"
+ placeholder="璇疯緭鍏ユ湇鍔″崟鍙凤紙濡侴Z202602锛�"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍒嗗叕鍙�" prop="serviceOrdClass">
+ <el-select v-model="queryParams.serviceOrdClass" placeholder="璇烽�夋嫨鍒嗗叕鍙�" clearable>
+ <el-option
+ v-for="dept in branchOptions"
+ :key="dept.serviceOrderClass"
+ :label="dept.deptName"
+ :value="dept.serviceOrderClass"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�绁ㄦ姮澶�" prop="invoiceName">
+ <el-input
+ v-model="queryParams.invoiceName"
+ placeholder="璇疯緭鍏ュ彂绁ㄦ姮澶�"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐢宠鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable>
+ <el-option label="寰呭鏍�" :value="0" />
+ <el-option label="宸查�氳繃" :value="1" />
+ <el-option label="宸查┏鍥�" :value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢宠鏃堕棿">
+ <el-date-picker
+ v-model="dateRange"
+ style="width: 240px"
+ value-format="yyyy-MM-dd"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="el-icon-download"
+ size="mini"
+ @click="handleExport"
+ v-hasPermi="['system:invoice:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="el-icon-refresh"
+ size="mini"
+ @click="handleSync"
+ v-hasRole="['admin']"
+ >鍚屾鏃х郴缁熺姸鎬�</el-button>
+ </el-col>
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="invoiceList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="ID" align="center" prop="invoiceId" width="80" />
+ <el-table-column label="鏈嶅姟鍗曞彿" align="center" prop="serviceCode" width="150" />
+ <el-table-column label="寮�绁ㄦ姮澶�" align="center" prop="invoiceName" width="150" />
+ <el-table-column label="閲戦" align="center" prop="invoiceMoney" width="100" />
+ <el-table-column label="绫诲瀷" align="center" prop="invoiceType">
+ <template slot-scope="scope">
+ <el-tag :type="scope.row.invoiceType === 2 ? 'success' : 'info'">
+ {{ scope.row.invoiceType === 2 ? '浼佷笟' : '涓汉' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template slot-scope="scope">
+ <el-tag :type="scope.row.status === 1 ? 'success' : (scope.row.status === 2 ? 'danger' : 'warning')">
+ {{ statusFormat(scope.row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙戠エ缂栧彿" align="center" prop="invoiceNo" />
+ <el-table-column label="鐢宠鏃堕棿" align="center" prop="applyTime" width="180">
+ <template slot-scope="scope">
+ <span>{{ parseTime(scope.row.applyTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template slot-scope="scope">
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-view"
+ @click="handleView(scope.row)"
+ >璇︽儏</el-button>
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-edit"
+ @click="handleUpdate(scope.row)"
+ v-hasPermi="['system:invoice:edit']"
+ v-if="scope.row.status === 0"
+ >瀹℃牳</el-button>
+ <el-button
+ size="mini"
+ type="text"
+ icon="el-icon-download"
+ v-if="scope.row.invoiceUrl"
+ @click="handleDownload(scope.row)"
+ >涓嬭浇鍙戠エ</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total>0"
+ :total="total"
+ :page.sync="queryParams.pageNum"
+ :limit.sync="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </div>
+</template>
+
+<script>
+import { listInvoice, syncInvoiceStatus } from "@/api/system/invoice";
+import { listBranchByOaOrderClass } from "@/api/system/dept";
+
+export default {
+ name: "Invoice",
+ data() {
+ return {
+ // 閬僵灞�
+ loading: true,
+ // 閫変腑鏁扮粍
+ ids: [],
+ // 闈炲崟涓鐢�
+ single: true,
+ // 闈炲涓鐢�
+ multiple: true,
+ // 鏄剧ず鎼滅储鏉′欢
+ showSearch: true,
+ // 鎬绘潯鏁�
+ total: 0,
+ // 鍙戠エ鐢宠琛ㄦ牸鏁版嵁
+ invoiceList: [],
+ // 鍒嗗叕鍙搁�夐」
+ branchOptions: [],
+ // 鏃ユ湡鑼冨洿
+ dateRange: [],
+ // 鏌ヨ鍙傛暟
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ serviceCode: null,
+ invoiceName: null,
+ status: null,
+ serviceOrdClass: null
+ }
+ };
+ },
+ created() {
+ this.getList();
+ this.getBranchList();
+ },
+ methods: {
+ /** 鏌ヨ鍒嗗叕鍙稿垪琛� */
+ getBranchList() {
+ listBranchByOaOrderClass().then(response => {
+ this.branchOptions = response.data;
+ });
+ },
+ /** 鏌ヨ鍙戠エ鐢宠鍒楄〃 */
+ getList() {
+ this.loading = true;
+ const params = this.addDateRange(this.queryParams, this.dateRange);
+ // 灏� serviceOrdClass 鏀惧叆 params 瀛楁涓紝瀵瑰簲鍚庡彴 XML 涓殑 params.serviceOrdClass
+ if (this.queryParams.serviceOrdClass) {
+ params['params[serviceOrdClass]'] = this.queryParams.serviceOrdClass;
+ }
+ // serviceCode 鏌ヨ
+ if (this.queryParams.serviceCode) {
+ params['params[serviceCode]'] = this.queryParams.serviceCode;
+ }
+ listInvoice(params).then(response => {
+ this.invoiceList = response.rows;
+ this.total = response.total;
+ this.loading = false;
+ });
+ },
+ // 澶氶�夋閫変腑鏁版嵁
+ handleSelectionChange(selection) {
+ this.ids = selection.map(item => item.invoiceId)
+ this.single = selection.length!==1
+ this.multiple = !selection.length
+ },
+ // 鐘舵�佸瓧鍏歌浆涔�
+ statusFormat(status) {
+ const map = { 0: "寰呭鏍�", 1: "宸查�氳繃", 2: "宸查┏鍥�" };
+ return map[status] || "鏈煡";
+ },
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ handleQuery() {
+ this.queryParams.pageNum = 1;
+ this.getList();
+ },
+ /** 閲嶇疆鎸夐挳鎿嶄綔 */
+ resetQuery() {
+ this.dateRange = [];
+ this.resetForm("queryForm");
+ this.handleQuery();
+ },
+ /** 璇︽儏鎸夐挳鎿嶄綔 */
+ handleView(row) {
+ this.$router.push({
+ path: '/system/invoice/detail',
+ query: { invoiceId: row.invoiceId }
+ });
+ },
+ /** 瀹℃牳鎸夐挳鎿嶄綔 */
+ handleUpdate(row) {
+ this.$router.push({
+ path: '/system/invoice/audit',
+ query: { invoiceId: row.invoiceId }
+ });
+ },
+ /** 瀵煎嚭鎸夐挳鎿嶄綔 */
+ handleExport() {
+ this.download('system/invoice/export', {
+ ...this.queryParams
+ }, `invoice_${new Date().getTime()}.xlsx`)
+ },
+ /** 鍚屾鎸夐挳鎿嶄綔 */
+ handleSync() {
+ this.loading = true;
+ syncInvoiceStatus().then(() => {
+ this.$modal.msgSuccess("鍚屾鎴愬姛");
+ this.getList();
+ }).finally(() => {
+ this.loading = false;
+ });
+ },
+ /** 涓嬭浇鍙戠エ鎿嶄綔 */
+ handleDownload(row) {
+ window.open(row.invoiceUrl);
+ }
+ }
+};
+</script>
diff --git a/ruoyi-ui/src/views/task/general/detail.vue b/ruoyi-ui/src/views/task/general/detail.vue
index bd5a736..e0b0bd8 100644
--- a/ruoyi-ui/src/views/task/general/detail.vue
+++ b/ruoyi-ui/src/views/task/general/detail.vue
@@ -150,12 +150,42 @@
<span v-if="taskDetail.emergencyInfo.dispatchSyncErrorMsg" style="color: #F56C6C;">{{ taskDetail.emergencyInfo.dispatchSyncErrorMsg }}</span>
<span v-else style="color: #C0C4CC;">--</span>
</el-descriptions-item>
+ <el-descriptions-item label="浠诲姟鐘舵�佸悓姝�" :span="2">
+ <el-alert
+ title="鎻愮ず锛氫换鍔$姸鎬佷細鑷姩鍚屾鍒版棫绯荤粺鐨勮皟搴﹀崟涓紝濡傛灉鍥犵綉缁滅瓑鍘熷洜鏈悓姝ワ紝鍙偣鍑讳笅鏂规寜閽墜鍔ㄥ悓姝ャ��"
+ type="info"
+ :closable="false"
+ show-icon
+ style="margin-bottom: 10px;">
+ </el-alert>
+ <el-button
+ v-if="taskDetail.emergencyInfo.legacyDispatchOrdId && taskDetail.emergencyInfo.legacyDispatchOrdId > 0"
+ type="warning"
+ size="small"
+ icon="el-icon-refresh"
+ :loading="syncingTaskStatus"
+ @click="syncTaskStatus"
+ >鍚屾浠诲姟鐘舵�佸埌鏃х郴缁�</el-button>
+ <el-tag v-else type="info" size="small">
+ <i class="el-icon-warning"></i> 璇峰厛鍚屾璋冨害鍗�
+ </el-tag>
+ </el-descriptions-item>
</el-descriptions>
<!-- 鏀粯淇℃伅锛堜粎鎬ユ晳杞繍浠诲姟鏄剧ず锛� -->
<el-card v-if="taskDetail.taskType === 'EMERGENCY_TRANSFER' && paymentInfo" class="box-card" style="margin-top: 20px;">
<div slot="header" class="clearfix">
<span>鏀粯淇℃伅</span>
+ <!-- 宸插畬鎴愪笖鏈敵璇峰彂绁ㄦ椂鏄剧ず鐢宠鍙戠エ鎸夐挳 -->
+ <el-button
+ v-if="canApplyInvoice"
+ style="float: right; padding: 3px 0"
+ type="text"
+ @click="handleApplyInvoice"
+ v-hasPermi="['system:invoice:add']"
+ >
+ <i class="el-icon-document-add"></i> 鐢宠鍙戠エ
+ </el-button>
</div>
<!-- 鏀粯姒傝 -->
@@ -758,7 +788,7 @@
</template>
<script>
-import { getTask, updateTask, assignTask, changeTaskStatus, uploadAttachment, deleteAttachment, getTaskVehicles, getAvailableVehicles, assignVehiclesToTask, unassignVehicleFromTask, getPaymentInfo, syncServiceOrder, syncDispatchOrder } from "@/api/task";
+import { getTask, updateTask, assignTask, changeTaskStatus, uploadAttachment, deleteAttachment, getTaskVehicles, getAvailableVehicles, assignVehiclesToTask, unassignVehicleFromTask, getPaymentInfo, syncServiceOrder, syncDispatchOrder, syncTaskStatus } from "@/api/task";
import { listUser } from "@/api/system/user";
import { getToken } from "@/utils/auth";
@@ -850,7 +880,11 @@
},
// 鍚屾鍔犺浇鐘舵��
syncingServiceOrder: false,
- syncingDispatchOrder: false
+ syncingDispatchOrder: false,
+ syncingTaskStatus: false,
+ // 鍙戠エ鐢宠鐘舵��
+ hasInvoiceApplied: false,
+ invoiceStatus: null // 0-寰呭鏍�, 1-宸查�氳繃, 2-宸查┏鍥�
};
},
created() {
@@ -859,6 +893,19 @@
this.getAdditionalFeeList();
// 鍒濆鍖栦笂浼燯RL
this.uploadUrl = process.env.VUE_APP_BASE_API + "/task/attachment/upload/" + this.$route.params.taskId;
+ // 妫�鏌ュ彂绁ㄧ敵璇风姸鎬�
+ this.checkInvoiceStatus();
+ },
+ computed: {
+ /** 鏄惁鍙互鐢宠鍙戠エ */
+ canApplyInvoice() {
+ // 鍙湁鎬ユ晳杞繍浠诲姟
+ if (this.taskDetail.taskType !== 'EMERGENCY_TRANSFER') return false;
+ // 浠诲姟蹇呴』宸插畬鎴�
+ if (this.taskDetail.taskStatus !== 'COMPLETED') return false;
+ // 鏈敵璇疯繃鍙戠エ锛屾垨鑰呮浘琚┏鍥�
+ return !this.hasInvoiceApplied || this.invoiceStatus === 2;
+ }
},
methods: {
/** 鑾峰彇浠诲姟璇︽儏 */
@@ -1157,7 +1204,7 @@
}).then(() => {
this.$modal.msgSuccess("鏈嶅姟鍗曞悓姝ユ垚鍔�");
// 閲嶆柊鍔犺浇浠诲姟璇︽儏
- this.getDetail();
+ this.getTaskDetail();
}).catch(() => {
// 澶勭悊鍙栨秷鍜岄敊璇�
}).finally(() => {
@@ -1172,12 +1219,66 @@
}).then(() => {
this.$modal.msgSuccess("璋冨害鍗曞悓姝ユ垚鍔�");
// 閲嶆柊鍔犺浇浠诲姟璇︽儏
- this.getDetail();
+ this.getTaskDetail();
}).catch(() => {
// 澶勭悊鍙栨秷鍜岄敊璇�
}).finally(() => {
this.syncingDispatchOrder = false;
});
+ },
+ /** 鎵嬪姩鍚屾浠诲姟鐘舵�� */
+ syncTaskStatus() {
+ this.$modal.confirm('鏄惁纭鍚屾浠诲姟鐘舵�佸埌鏃х郴缁燂紵').then(() => {
+ this.syncingTaskStatus = true;
+ return syncTaskStatus(this.taskDetail.taskId);
+ }).then(() => {
+ this.$modal.msgSuccess("浠诲姟鐘舵�佸悓姝ユ垚鍔�");
+ // 閲嶆柊鍔犺浇浠诲姟璇︽儏
+ this.getTaskDetail();
+ }).catch(() => {
+ // 澶勭悊鍙栨秷鍜岄敊璇�
+ }).finally(() => {
+ this.syncingTaskStatus = false;
+ });
+ },
+
+ /** 妫�鏌ュ彂绁ㄧ敵璇风姸鎬� */
+ checkInvoiceStatus() {
+ // 璋冪敤鍚庣鎺ュ彛妫�鏌ヨ浠诲姟鏄惁宸茬敵璇峰彂绁�
+ this.$axios.get(`/system/invoice/checkTaskInvoice/${this.$route.params.taskId}`)
+ .then(response => {
+ if (response.code === 200 && response.data) {
+ this.hasInvoiceApplied = true;
+ this.invoiceStatus = response.data.status;
+ }
+ })
+ .catch(() => {
+ // 蹇界暐閿欒锛岄粯璁ゆ湭鐢宠
+ });
+ },
+
+ /** 鐢宠鍙戠エ */
+ handleApplyInvoice() {
+ // 璺宠浆鍒板彂绁ㄧ敵璇烽〉闈紝甯︿笂浠诲姟淇℃伅
+ const taskInfo = {
+ taskId: this.taskDetail.taskId,
+ taskCode: this.taskDetail.taskCode || this.taskDetail.showTaskCode,
+ legacyServiceOrderId: this.taskDetail.emergencyInfo?.legacyServiceOrdId,
+ serviceCode: this.taskDetail.emergencyInfo?.serviceCode,
+ departure: this.taskDetail.departureAddress,
+ destination: this.taskDetail.destinationAddress,
+ completionTime: this.parseTime(this.taskDetail.actualEndTime),
+ transferPrice: this.paymentInfo?.transferPrice || this.paymentInfo?.totalAmount
+ };
+
+ // 灏嗕换鍔′俊鎭瓨鍌ㄥ埌 sessionStorage
+ sessionStorage.setItem('invoiceTaskInfo', JSON.stringify(taskInfo));
+
+ // 璺宠浆鍒板彂绁ㄧ敵璇烽〉闈�
+ this.$router.push({
+ path: '/system/invoice/apply',
+ query: { taskId: this.taskDetail.taskId }
+ });
}
}
};
diff --git a/ruoyi-ui/src/views/task/general/index.vue b/ruoyi-ui/src/views/task/general/index.vue
index 382cc4a..3c41b95 100644
--- a/ruoyi-ui/src/views/task/general/index.vue
+++ b/ruoyi-ui/src/views/task/general/index.vue
@@ -29,6 +29,22 @@
/>
</el-select>
</el-form-item>
+ <el-form-item label="鍒嗗叕鍙�" prop="deptId">
+ <el-select
+ v-model="queryParams.deptId"
+ placeholder="璇烽�夋嫨鍒嗗叕鍙�"
+ clearable
+ filterable
+ style="width: 200px"
+ >
+ <el-option
+ v-for="dept in branchList"
+ :key="dept.deptId"
+ :label="dept.deptName"
+ :value="dept.deptId"
+ />
+ </el-select>
+ </el-form-item>
<el-form-item label="杞︾墝鍙�" prop="vehicleNo">
<el-input
v-model="queryParams.vehicleNo"
@@ -386,6 +402,7 @@
<script>
import { listTask, getTask, delTask, addTask, updateTask, assignTask, changeTaskStatus } from "@/api/task";
import { listUser } from "@/api/system/user";
+import { listBranchByOa } from "@/api/system/dept";
export default {
name: "Task",
@@ -425,6 +442,7 @@
taskCode: null,
taskType: null,
taskStatus: null,
+ deptId: null,
vehicleNo: null,
plannedStartTimeBegin: null,
plannedStartTimeEnd: null,
@@ -437,6 +455,8 @@
statusForm: {},
// 鐢ㄦ埛鍒楄〃
userList: [],
+ // 鍒嗗叕鍙稿垪琛�
+ branchList: [],
// 琛ㄥ崟鏍¢獙
rules: {
taskType: [
@@ -469,6 +489,7 @@
created() {
this.getList();
this.getUserList();
+ this.getBranchList();
},
methods: {
/** 鏌ヨ浠诲姟绠$悊鍒楄〃 */
@@ -492,6 +513,14 @@
this.userList = response.rows;
});
},
+ /** 鏌ヨ鍒嗗叕鍙稿垪琛� */
+ getBranchList() {
+ listBranchByOa().then(response => {
+ this.branchList = response.data || [];
+ }).catch(() => {
+ this.branchList = [];
+ });
+ },
// 鍙栨秷鎸夐挳
cancel() {
this.open = false;
diff --git a/sql/InvoiceData.sql b/sql/InvoiceData.sql
new file mode 100644
index 0000000..064800a
--- /dev/null
+++ b/sql/InvoiceData.sql
@@ -0,0 +1,31 @@
+create table InvoiceData(
+InvoiceID int no 4 10 0 no (n/a) (n/a) NULL
+ServiceOrderIDPK bigint no 8 19 0 yes (n/a) (n/a) NULL
+InvoiceType int no 4 10 0 yes (n/a) (n/a) NULL
+InvoiceName nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceMakeout nvarchar no 2000 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceCompanyPhone nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceCompanyID nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceCompanyAdd nvarchar no 200 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceCompanyBank nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceCompanyBankNo nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceZipCode nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+Invoice_strAdd nvarchar no 200 yes (n/a) (n/a) Chinese_PRC_CI_AS
+Invoice_strName nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+Invoice_strPhone nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+Invoice_strEmail nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+ApplicationTime datetime no 8 yes (n/a) (n/a) NULL
+AuditTime datetime no 8 yes (n/a) (n/a) NULL
+AuditStatus int no 4 10 0 yes (n/a) (n/a) NULL
+AuditOAID int no 4 10 0 yes (n/a) (n/a) NULL
+AuditMakeout nvarchar no 200 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceMoney money no 8 19 4 yes (n/a) (n/a) NULL
+InvoiceNo nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceURL nvarchar no 2000 yes (n/a) (n/a) Chinese_PRC_CI_AS
+InvoiceOddNo nvarchar no 400 yes (n/a) (n/a) Chinese_PRC_CI_AS
+EleCloud_ZTDM nvarchar no 100 yes (n/a) (n/a) Chinese_PRC_CI_AS
+EleCloud_ZTXX nvarchar no 200 yes (n/a) (n/a) Chinese_PRC_CI_AS
+EleCloud_PDF nvarchar no 200 yes (n/a) (n/a) Chinese_PRC_CI_AS
+EleCloud_Time datetime no 8 yes (n/a) (n/a) NULL
+ApplyOAID int no 4 10 0 yes (n/a) (n/a) NULL
+)
\ No newline at end of file
diff --git a/sql/invoice_menu.sql b/sql/invoice_menu.sql
new file mode 100644
index 0000000..33fa4f9
--- /dev/null
+++ b/sql/invoice_menu.sql
@@ -0,0 +1,27 @@
+-- ----------------------------
+-- 1銆佸彂绁ㄧ鐞嗕富鑿滃崟
+-- ----------------------------
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+values('鍙戠エ绠$悊', '1', '10', 'invoice', 'system/invoice/index', 1, 0, 'C', '0', '0', 'system:invoice:list', 'edit', 'admin', sysdate(), '鍙戠エ鐢宠绠$悊鑿滃崟');
+
+-- 鑾峰彇鍒氬垰鎻掑叆鐨勮彍鍗旾D (閫傜敤浜嶮ySQL)
+set @parentId = LAST_INSERT_ID();
+
+-- ----------------------------
+-- 2銆佸彂绁ㄧ鐞嗙浉鍏虫寜閽潈闄�
+-- ----------------------------
+-- 鏌ヨ鏉冮檺
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+values('鍙戠エ鏌ヨ', @parentId, '1', '', '', 1, 0, 'F', '0', '0', 'system:invoice:query', '#', 'admin', sysdate(), '');
+
+-- 淇敼/瀹℃牳鏉冮檺
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+values('鍙戠エ瀹℃牳', @parentId, '2', '', '', 1, 0, 'F', '0', '0', 'system:invoice:edit', '#', 'admin', sysdate(), '');
+
+-- 鍒犻櫎鏉冮檺
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+values('鍙戠エ鍒犻櫎', @parentId, '3', '', '', 1, 0, 'F', '0', '0', 'system:invoice:remove', '#', 'admin', sysdate(), '');
+
+-- 瀵煎嚭鏉冮檺
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+values('鍙戠エ瀵煎嚭', @parentId, '4', '', '', 1, 0, 'F', '0', '0', 'system:invoice:export', '#', 'admin', sysdate(), '');
\ No newline at end of file
diff --git a/sql/invoice_sync_from_legacy.sql b/sql/invoice_sync_from_legacy.sql
new file mode 100644
index 0000000..ccb207d
--- /dev/null
+++ b/sql/invoice_sync_from_legacy.sql
@@ -0,0 +1,124 @@
+-- 浠庢棫绯荤粺鍚屾鍙戠エ淇℃伅鍒版柊绯荤粺鐨凷QL鑴氭湰
+-- 鍚屾鏃х郴缁熺殑鍙戠エ鏁版嵁鍒版柊绯荤粺锛岄伩鍏嶉噸澶嶅悓姝�
+
+-- 鏂规1: 浣跨敤INSERT IGNORE鎻掑叆鏂版暟鎹�
+INSERT IGNORE INTO sys_invoice (
+ legacy_invoice_id,
+ legacy_service_order_id,
+ invoice_type,
+ invoice_name,
+ invoice_money,
+ invoice_remarks,
+ company_address,
+ company_bank,
+ company_bank_no,
+ zip_code,
+ mail_address,
+ contact_name,
+ contact_phone,
+ contact_email,
+ status,
+ invoice_no,
+ invoice_url,
+ apply_time,
+ audit_time,
+ audit_remarks,
+ sync_status
+)
+SELECT
+ i.InvoiceID as legacy_invoice_id,
+ i.ServiceOrderIDPK as legacy_service_order_id,
+ CASE
+ WHEN i.InvoiceType = 1 THEN 1 -- 涓汉鍙戠エ
+ WHEN i.InvoiceType = 2 THEN 2 -- 浼佷笟鍙戠エ
+ ELSE 1 -- 榛樿涓汉鍙戠エ
+ END as invoice_type,
+ i.InvoiceName as invoice_name,
+ CAST(i.InvoiceMoney AS DECIMAL(10,2)) as invoice_money,
+ i.InvoiceMakeout as invoice_remarks,
+ i.InvoiceCompanyAdd as company_address,
+ i.InvoiceCompanyBank as company_bank,
+ i.InvoiceCompanyBankNo as company_bank_no,
+ i.InvoiceZipCode as zip_code,
+ i.Invoice_strAdd as mail_address,
+ i.Invoice_strName as contact_name,
+ i.Invoice_strPhone as contact_phone,
+ i.Invoice_strEmail as contact_email,
+ CASE
+ WHEN i.AuditStatus = 1 THEN 1 -- 宸查�氳繃
+ WHEN i.AuditStatus = 2 THEN 2 -- 宸查┏鍥�
+ ELSE 0 -- 寰呭鏍�
+ END as status,
+ i.InvoiceNo as invoice_no,
+ COALESCE(i.InvoiceURL, i.EleCloud_PDF) as invoice_url,
+ i.ApplicationTime as apply_time,
+ i.AuditTime as audit_time,
+ i.AuditMakeout as audit_remarks,
+ 1 as sync_status -- 鏍囪涓哄凡鍚屾
+FROM InvoiceData i
+WHERE i.InvoiceID NOT IN (
+ SELECT legacy_invoice_id
+ FROM sys_invoice
+ WHERE legacy_invoice_id IS NOT NULL
+);
+
+-- 鏂规2: 浣跨敤LEFT JOIN纭繚鍙彃鍏ヤ笉瀛樺湪鐨勮褰�
+-- INSERT INTO sys_invoice (
+-- legacy_invoice_id,
+-- legacy_service_order_id,
+-- invoice_type,
+-- invoice_name,
+-- invoice_money,
+-- invoice_remarks,
+-- company_address,
+-- company_bank,
+-- company_bank_no,
+-- zip_code,
+-- mail_address,
+-- contact_name,
+-- contact_phone,
+-- contact_email,
+-- status,
+-- invoice_no,
+-- invoice_url,
+-- apply_time,
+-- audit_time,
+-- audit_remarks,
+-- sync_status
+-- )
+-- SELECT
+-- i.InvoiceID,
+-- i.ServiceOrderIDPK,
+-- CASE
+-- WHEN i.InvoiceType = 1 THEN 1
+-- WHEN i.InvoiceType = 2 THEN 2
+-- ELSE 1
+-- END,
+-- i.InvoiceName,
+-- CAST(i.InvoiceMoney AS DECIMAL(10,2)),
+-- i.InvoiceMakeout,
+-- i.InvoiceCompanyAdd,
+-- i.InvoiceCompanyBank,
+-- i.InvoiceCompanyBankNo,
+-- i.InvoiceZipCode,
+-- i.Invoice_strAdd,
+-- i.Invoice_strName,
+-- i.Invoice_strPhone,
+-- i.Invoice_strEmail,
+-- CASE
+-- WHEN i.AuditStatus = 1 THEN 1
+-- WHEN i.AuditStatus = 2 THEN 2
+-- ELSE 0
+-- END,
+-- i.InvoiceNo,
+-- COALESCE(i.InvoiceURL, i.EleCloud_PDF),
+-- i.ApplicationTime,
+-- i.AuditTime,
+-- i.AuditMakeout,
+-- 1
+-- FROM InvoiceData i
+-- LEFT JOIN sys_invoice si ON i.InvoiceID = si.legacy_invoice_id
+-- WHERE si.legacy_invoice_id IS NULL;
+
+-- 鏇存柊缁熻淇℃伅
+ANALYZE TABLE sys_invoice;
\ No newline at end of file
diff --git a/sql/invoice_sync_job.sql b/sql/invoice_sync_job.sql
new file mode 100644
index 0000000..792ae94
--- /dev/null
+++ b/sql/invoice_sync_job.sql
@@ -0,0 +1,248 @@
+-- 鍙戠エ鍚屾瀹氭椂浠诲姟閰嶇疆
+-- 鐢ㄤ簬浠庢棫绯荤粺鍚屾鍙戠エ淇℃伅鍒版柊绯荤粺
+
+-- 1. 鍒涘缓鍙戠エ鍚屾鏃ュ織琛�
+CREATE TABLE IF NOT EXISTS `sys_invoice_sync_log` (
+ `log_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '鍚屾鏃ュ織ID',
+ `sync_type` VARCHAR(50) NOT NULL COMMENT '鍚屾绫诲瀷(invoice_info-鍙戠エ淇℃伅,invoice_status-鍙戠エ鐘舵��)',
+ `sync_start_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '鍚屾寮�濮嬫椂闂�',
+ `sync_end_time` DATETIME DEFAULT NULL COMMENT '鍚屾缁撴潫鏃堕棿',
+ `records_processed` INT(11) DEFAULT 0 COMMENT '澶勭悊璁板綍鏁�',
+ `records_success` INT(11) DEFAULT 0 COMMENT '鎴愬姛璁板綍鏁�',
+ `records_failed` INT(11) DEFAULT 0 COMMENT '澶辫触璁板綍鏁�',
+ `error_message` TEXT DEFAULT NULL COMMENT '閿欒淇℃伅',
+ `status` TINYINT(1) DEFAULT 0 COMMENT '鐘舵��(0-澶勭悊涓�,1-鎴愬姛,2-澶辫触)',
+ PRIMARY KEY (`log_id`),
+ INDEX `idx_sync_type` (`sync_type`),
+ INDEX `idx_sync_start_time` (`sync_start_time`)
+) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='鍙戠エ鍚屾鏃ュ織琛�';
+
+-- 2. 鎻掑叆鍙戠エ鍚屾瀹氭椂浠诲姟閰嶇疆
+DELETE FROM sys_job WHERE job_name = 'InvoiceSyncJob';
+INSERT INTO sys_job (
+ job_name,
+ job_group,
+ invoke_target,
+ cron_expression,
+ misfire_policy,
+ concurrent,
+ status,
+ create_by,
+ create_time
+) VALUES (
+ 'InvoiceSyncJob',
+ 'SYSTEM',
+ 'sysInvoiceTask.syncInvoiceFromLegacySystem',
+ '0 0/30 * * * ?', -- 姣�30鍒嗛挓鎵ц涓�娆�
+ '3',
+ '1',
+ '0',
+ 'admin',
+ NOW()
+);
+
+-- 3. 鍚屾鍙戠エ淇℃伅鐨勫瓨鍌ㄨ繃绋�
+DELIMITER $$
+
+DROP PROCEDURE IF EXISTS sync_invoice_from_legacy$$
+
+CREATE PROCEDURE sync_invoice_from_legacy()
+BEGIN
+ DECLARE v_start_time DATETIME DEFAULT NOW();
+ DECLARE v_error_msg TEXT DEFAULT '';
+ DECLARE v_processed_count INT DEFAULT 0;
+ DECLARE v_success_count INT DEFAULT 0;
+ DECLARE v_failed_count INT DEFAULT 0;
+ DECLARE EXIT HANDLER FOR SQLEXCEPTION
+ BEGIN
+ GET DIAGNOSTICS CONDITION 1
+ v_error_msg = MESSAGE_TEXT;
+ ROLLBACK;
+ END;
+
+ START TRANSACTION;
+
+ -- 璁板綍寮�濮嬪悓姝�
+ INSERT INTO sys_invoice_sync_log (sync_type, sync_start_time, status)
+ VALUES ('invoice_info', v_start_time, 0);
+
+ SET @log_id = LAST_INSERT_ID();
+
+ -- 鍚屾鏂扮殑鍙戠エ璁板綍
+ INSERT INTO sys_invoice (
+ legacy_invoice_id,
+ legacy_service_order_id,
+ invoice_type,
+ invoice_name,
+ invoice_money,
+ invoice_remarks,
+ company_address,
+ company_bank,
+ company_bank_no,
+ zip_code,
+ mail_address,
+ contact_name,
+ contact_phone,
+ contact_email,
+ status,
+ invoice_no,
+ invoice_url,
+ apply_time,
+ audit_time,
+ audit_remarks,
+ sync_status
+ )
+ SELECT
+ i.InvoiceID as legacy_invoice_id,
+ i.ServiceOrderIDPK as legacy_service_order_id,
+ CASE
+ WHEN i.InvoiceType = 1 THEN 1 -- 涓汉鍙戠エ
+ WHEN i.InvoiceType = 2 THEN 2 -- 浼佷笟鍙戠エ
+ ELSE 1 -- 榛樿涓汉鍙戠エ
+ END as invoice_type,
+ i.InvoiceName as invoice_name,
+ CAST(i.InvoiceMoney AS DECIMAL(10,2)) as invoice_money,
+ i.InvoiceMakeout as invoice_remarks,
+ i.InvoiceCompanyAdd as company_address,
+ i.InvoiceCompanyBank as company_bank,
+ i.InvoiceCompanyBankNo as company_bank_no,
+ i.InvoiceZipCode as zip_code,
+ i.Invoice_strAdd as mail_address,
+ i.Invoice_strName as contact_name,
+ i.Invoice_strPhone as contact_phone,
+ i.Invoice_strEmail as contact_email,
+ CASE
+ WHEN i.AuditStatus = 1 THEN 1 -- 宸查�氳繃
+ WHEN i.AuditStatus = 2 THEN 2 -- 宸查┏鍥�
+ ELSE 0 -- 寰呭鏍�
+ END as status,
+ i.InvoiceNo as invoice_no,
+ COALESCE(i.InvoiceURL, i.EleCloud_PDF) as invoice_url,
+ i.ApplicationTime as apply_time,
+ i.AuditTime as audit_time,
+ i.AuditMakeout as audit_remarks,
+ 1 as sync_status -- 鏍囪涓哄凡鍚屾
+ FROM InvoiceData i
+ WHERE i.InvoiceID NOT IN (
+ SELECT legacy_invoice_id
+ FROM sys_invoice
+ WHERE legacy_invoice_id IS NOT NULL
+ );
+
+ SET v_processed_count = ROW_COUNT();
+ SET v_success_count = v_processed_count;
+
+ -- 鏇存柊鍚屾鏃ュ織
+ UPDATE sys_invoice_sync_log
+ SET
+ sync_end_time = NOW(),
+ records_processed = v_processed_count,
+ records_success = v_success_count,
+ records_failed = v_failed_count,
+ error_message = v_error_msg,
+ status = IF(v_error_msg = '', 1, 2)
+ WHERE log_id = @log_id;
+
+ COMMIT;
+END$$
+
+DELIMITER ;
+
+-- 4. 鍚屾鍙戠エ鐘舵�佺殑瀛樺偍杩囩▼
+DELIMITER $$
+
+DROP PROCEDURE IF EXISTS sync_invoice_status_from_legacy$$
+
+CREATE PROCEDURE sync_invoice_status_from_legacy()
+BEGIN
+ DECLARE v_start_time DATETIME DEFAULT NOW();
+ DECLARE v_error_msg TEXT DEFAULT '';
+ DECLARE v_processed_count INT DEFAULT 0;
+ DECLARE v_success_count INT DEFAULT 0;
+ DECLARE v_failed_count INT DEFAULT 0;
+ DECLARE EXIT HANDLER FOR SQLEXCEPTION
+ BEGIN
+ GET DIAGNOSTICS CONDITION 1
+ v_error_msg = MESSAGE_TEXT;
+ ROLLBACK;
+ END;
+
+ START TRANSACTION;
+
+ -- 璁板綍寮�濮嬪悓姝�
+ INSERT INTO sys_invoice_sync_log (sync_type, sync_start_time, status)
+ VALUES ('invoice_status', v_start_time, 0);
+
+ SET @log_id = LAST_INSERT_ID();
+
+ -- 鏇存柊鐜版湁鍙戠エ鐨勭姸鎬佷俊鎭�
+ UPDATE sys_invoice si
+ INNER JOIN InvoiceData i ON si.legacy_invoice_id = i.InvoiceID
+ SET
+ si.status = CASE
+ WHEN i.AuditStatus = 1 THEN 1 -- 宸查�氳繃
+ WHEN i.AuditStatus = 2 THEN 2 -- 宸查┏鍥�
+ ELSE 0 -- 寰呭鏍�
+ END,
+ si.invoice_no = COALESCE(si.invoice_no, i.InvoiceNo),
+ si.invoice_url = COALESCE(si.invoice_url, i.InvoiceURL, i.EleCloud_PDF),
+ si.audit_time = i.AuditTime,
+ si.audit_remarks = i.AuditMakeout,
+ si.sync_status = 1
+ WHERE si.legacy_invoice_id IS NOT NULL
+ AND (
+ si.status != CASE
+ WHEN i.AuditStatus = 1 THEN 1
+ WHEN i.AuditStatus = 2 THEN 2
+ ELSE 0
+ END
+ OR si.invoice_no IS NULL
+ OR si.invoice_url IS NULL
+ );
+
+ SET v_processed_count = ROW_COUNT();
+ SET v_success_count = v_processed_count;
+
+ -- 鏇存柊鍚屾鏃ュ織
+ UPDATE sys_invoice_sync_log
+ SET
+ sync_end_time = NOW(),
+ records_processed = v_processed_count,
+ records_success = v_success_count,
+ records_failed = v_failed_count,
+ error_message = v_error_msg,
+ status = IF(v_error_msg = '', 1, 2)
+ WHERE log_id = @log_id;
+
+ COMMIT;
+END$$
+
+DELIMITER ;
+
+-- 5. 鏌ヨ鍙戠エ鍚屾鏃ュ織鐨勮鍥�
+CREATE OR REPLACE VIEW v_invoice_sync_log AS
+SELECT
+ l.log_id,
+ l.sync_type,
+ l.sync_start_time,
+ l.sync_end_time,
+ l.records_processed,
+ l.records_success,
+ l.records_failed,
+ l.error_message,
+ l.status,
+ CASE l.status
+ WHEN 0 THEN '澶勭悊涓�'
+ WHEN 1 THEN '鎴愬姛'
+ WHEN 2 THEN '澶辫触'
+ ELSE '鏈煡'
+ END as status_name,
+ TIMESTAMPDIFF(SECOND, l.sync_start_time, l.sync_end_time) as duration_seconds
+FROM sys_invoice_sync_log l
+ORDER BY l.sync_start_time DESC;
+
+-- 6. 鎵嬪姩鎵ц鍙戠エ淇℃伅鍚屾
+-- CALL sync_invoice_from_legacy();
+
+-- 7. 鎵嬪姩鎵ц鍙戠エ鐘舵�佸悓姝�
+-- CALL sync_invoice_status_from_legacy();
\ No newline at end of file
diff --git a/sql/invoice_sys.sql b/sql/invoice_sys.sql
new file mode 100644
index 0000000..97fe8bb
--- /dev/null
+++ b/sql/invoice_sys.sql
@@ -0,0 +1,32 @@
+-- 鍙戠エ鐢宠琛�
+CREATE TABLE `sys_invoice` (
+ `invoice_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '鍙戠エID',
+ `service_order_id` BIGINT(20) DEFAULT NULL COMMENT '鏈嶅姟鍗曞彿(鏂扮郴缁熻鍗旾D)',
+ `legacy_service_order_id` BIGINT(20) DEFAULT NULL COMMENT '鏈嶅姟鍗曞彿(鏃х郴缁烻erviceOrderID)',
+ `invoice_type` INT(1) DEFAULT '1' COMMENT '寮�绁ㄧ被鍨�(1-涓汉, 2-浼佷笟)',
+ `invoice_name` VARCHAR(200) DEFAULT NULL COMMENT '鍙戠エ鎶ご',
+ `invoice_money` DECIMAL(10,2) DEFAULT '0.00' COMMENT '鍙戠エ閲戦',
+ `invoice_remarks` VARCHAR(500) DEFAULT NULL COMMENT '鍙戠エ澶囨敞',
+ `company_address` VARCHAR(500) DEFAULT NULL COMMENT '浼佷笟娉ㄥ唽鍦板潃',
+ `company_bank` VARCHAR(200) DEFAULT NULL COMMENT '浼佷笟寮�鎴烽摱琛�',
+ `company_bank_no` VARCHAR(100) DEFAULT NULL COMMENT '浼佷笟閾惰甯愬彿',
+ `zip_code` VARCHAR(20) DEFAULT NULL COMMENT '閭紪',
+ `mail_address` VARCHAR(500) DEFAULT NULL COMMENT '閭瘎鍦板潃',
+ `contact_name` VARCHAR(50) DEFAULT NULL COMMENT '鑱旂郴浜�',
+ `contact_phone` VARCHAR(50) DEFAULT NULL COMMENT '鑱旂郴鐢佃瘽',
+ `contact_email` VARCHAR(100) DEFAULT NULL COMMENT '鑱旂郴閭',
+ `status` INT(1) DEFAULT '0' COMMENT '鐢宠鐘舵��(0-寰呭鏍�, 1-宸查�氳繃, 2-宸查┏鍥�)',
+ `invoice_no` VARCHAR(100) DEFAULT NULL COMMENT '鍙戠エ缂栧彿(瀵瑰簲鏃х郴缁烮nvoiceNo)',
+ `invoice_url` VARCHAR(2000) DEFAULT NULL COMMENT '鍙戠エ閾炬帴/鏂囦欢鍦板潃(瀵瑰簲鏃х郴缁烮nvoiceURL/EleCloud_PDF)',
+ `apply_user_id` BIGINT(20) DEFAULT NULL COMMENT '鐢宠浜篒D',
+ `apply_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '鐢宠鏃堕棿',
+ `audit_user_id` BIGINT(20) DEFAULT NULL COMMENT '瀹℃牳浜篒D',
+ `audit_time` DATETIME DEFAULT NULL COMMENT '瀹℃牳鏃堕棿',
+ `audit_remarks` VARCHAR(500) DEFAULT NULL COMMENT '瀹℃牳澶囨敞',
+ `sync_status` INT(1) DEFAULT '0' COMMENT '鍚屾鐘舵��(0-鏈悓姝�, 1-宸插悓姝�, 2-鍚屾澶辫触)',
+ `legacy_invoice_id` INT(11) DEFAULT NULL COMMENT '鏃х郴缁熷彂绁↖D(瀵瑰簲鏃х郴缁烮nvoiceID)',
+ PRIMARY KEY (`invoice_id`),
+ KEY `idx_service_order` (`service_order_id`),
+ KEY `idx_legacy_order` (`legacy_service_order_id`),
+ KEY `idx_apply_user` (`apply_user_id`)
+) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='鍙戠エ鐢宠琛�';
\ No newline at end of file
diff --git a/sql/partition_quick_setup.sql b/sql/partition_quick_setup.sql
new file mode 100644
index 0000000..0abd88f
--- /dev/null
+++ b/sql/partition_quick_setup.sql
@@ -0,0 +1,260 @@
+-- ========================================
+-- GPS鍒嗘閲岀▼琛ㄥ垎鍖轰紭鍖� - 蹇�熸墽琛岀増
+-- ========================================
+-- 閫傜敤鍦烘櫙锛氭暟鎹噺閫備腑锛�100涓�-500涓囷級锛屽彲浠ユ帴鍙楃煭鏆傚仠鏈�
+-- 鎵ц鏃堕棿锛氭牴鎹暟鎹噺锛岄璁�5-30鍒嗛挓
+-- ========================================
+
+-- 绗竴姝ワ細妫�鏌ュ綋鍓嶆暟鎹姸鎬�
+-- ========================================
+USE your_database_name; -- 璇蜂慨鏀逛负瀹為檯鐨勬暟鎹簱鍚�
+
+SELECT '=== 褰撳墠琛ㄤ俊鎭� ===' as info;
+SELECT
+ TABLE_NAME as '琛ㄥ悕',
+ TABLE_ROWS as '璁板綍鏁�(浼扮畻)',
+ ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS '澶у皬(MB)'
+FROM information_schema.TABLES
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_gps_segment_mileage';
+
+SELECT '=== 鏁版嵁鏃堕棿鑼冨洿 ===' as info;
+SELECT
+ MIN(segment_start_time) as '鏈�鏃╂暟鎹�',
+ MAX(segment_start_time) as '鏈�鏂版暟鎹�',
+ DATEDIFF(MAX(segment_start_time), MIN(segment_start_time)) as '鏁版嵁璺ㄥ害(澶�)'
+FROM tb_vehicle_gps_segment_mileage;
+
+SELECT '=== 鎸夋湀鏁版嵁鍒嗗竷 ===' as info;
+SELECT
+ DATE_FORMAT(segment_start_time, '%Y-%m') as '鏈堜唤',
+ COUNT(*) as '璁板綍鏁�',
+ ROUND(SUM(segment_distance), 2) as '鎬婚噷绋�(km)'
+FROM tb_vehicle_gps_segment_mileage
+GROUP BY DATE_FORMAT(segment_start_time, '%Y-%m')
+ORDER BY 1 DESC
+LIMIT 12;
+
+-- 鏆傚仠锛佽妫�鏌ヤ互涓婁俊鎭紝纭鏁版嵁閲忓拰鏃堕棿鑼冨洿
+-- 鎸夊洖杞︾户缁�...
+
+-- ========================================
+-- 绗簩姝ワ細鍒涘缓鍒嗗尯琛�
+-- ========================================
+
+-- 鍒涘缓鏂扮殑鍒嗗尯琛�
+CREATE TABLE `tb_vehicle_gps_segment_mileage_partitioned` (
+ `segment_id` bigint(20) NOT NULL AUTO_INCREMENT,
+ `vehicle_id` bigint(20) NOT NULL,
+ `vehicle_no` varchar(20) DEFAULT NULL,
+ `segment_start_time` datetime NOT NULL,
+ `segment_end_time` datetime NOT NULL,
+ `start_longitude` decimal(10,7) DEFAULT NULL,
+ `start_latitude` decimal(10,7) DEFAULT NULL,
+ `end_longitude` decimal(10,7) DEFAULT NULL,
+ `end_latitude` decimal(10,7) DEFAULT NULL,
+ `segment_distance` decimal(10,3) DEFAULT 0.000,
+ `gps_point_count` int(11) DEFAULT 0,
+ `gps_ids` text,
+ `task_id` bigint(20) DEFAULT NULL,
+ `task_code` varchar(50) DEFAULT NULL,
+ `calculate_method` varchar(20) DEFAULT 'tianditu',
+ `create_time` datetime DEFAULT NULL,
+ `update_time` datetime DEFAULT NULL,
+ PRIMARY KEY (`segment_id`, `segment_start_time`),
+ UNIQUE KEY `uk_vehicle_time` (`vehicle_id`, `segment_start_time`),
+ KEY `idx_vehicle_id` (`vehicle_id`),
+ KEY `idx_start_time` (`segment_start_time`),
+ KEY `idx_task_id` (`task_id`),
+ KEY `idx_vehicle_task` (`vehicle_id`, `task_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
+COMMENT='杞﹁締GPS鍒嗘閲岀▼琛紙鎸夋湀鍒嗗尯锛�'
+PARTITION BY RANGE (TO_DAYS(segment_start_time)) (
+ PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
+ PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
+ PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
+ PARTITION p202404 VALUES LESS THAN (TO_DAYS('2024-05-01')),
+ PARTITION p202405 VALUES LESS THAN (TO_DAYS('2024-06-01')),
+ PARTITION p202406 VALUES LESS THAN (TO_DAYS('2024-07-01')),
+ PARTITION p202407 VALUES LESS THAN (TO_DAYS('2024-08-01')),
+ PARTITION p202408 VALUES LESS THAN (TO_DAYS('2024-09-01')),
+ PARTITION p202409 VALUES LESS THAN (TO_DAYS('2024-10-01')),
+ PARTITION p202410 VALUES LESS THAN (TO_DAYS('2024-11-01')),
+ PARTITION p202411 VALUES LESS THAN (TO_DAYS('2024-12-01')),
+ PARTITION p202412 VALUES LESS THAN (TO_DAYS('2025-01-01')),
+ PARTITION p202501 VALUES LESS THAN (TO_DAYS('2025-02-01')),
+ PARTITION p202502 VALUES LESS THAN (TO_DAYS('2025-03-01')),
+ PARTITION p202503 VALUES LESS THAN (TO_DAYS('2025-04-01')),
+ PARTITION p202504 VALUES LESS THAN (TO_DAYS('2025-05-01')),
+ PARTITION p202505 VALUES LESS THAN (TO_DAYS('2025-06-01')),
+ PARTITION p202506 VALUES LESS THAN (TO_DAYS('2025-07-01')),
+ PARTITION p202507 VALUES LESS THAN (TO_DAYS('2025-08-01')),
+ PARTITION p202508 VALUES LESS THAN (TO_DAYS('2025-09-01')),
+ PARTITION p202509 VALUES LESS THAN (TO_DAYS('2025-10-01')),
+ PARTITION p202510 VALUES LESS THAN (TO_DAYS('2025-11-01')),
+ PARTITION p202511 VALUES LESS THAN (TO_DAYS('2025-12-01')),
+ PARTITION p202512 VALUES LESS THAN (TO_DAYS('2026-01-01')),
+ PARTITION p202601 VALUES LESS THAN (TO_DAYS('2026-02-01')),
+ PARTITION p202602 VALUES LESS THAN (TO_DAYS('2026-03-01')),
+ PARTITION p202603 VALUES LESS THAN (TO_DAYS('2026-04-01')),
+ PARTITION p202604 VALUES LESS THAN (TO_DAYS('2026-05-01')),
+ PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
+ PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
+ PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
+ PARTITION p202608 VALUES LESS THAN (TO_DAYS('2026-09-01')),
+ PARTITION p202609 VALUES LESS THAN (TO_DAYS('2026-10-01')),
+ PARTITION p202610 VALUES LESS THAN (TO_DAYS('2026-11-01')),
+ PARTITION p202611 VALUES LESS THAN (TO_DAYS('2026-12-01')),
+ PARTITION p202612 VALUES LESS THAN (TO_DAYS('2027-01-01')),
+ PARTITION pfuture VALUES LESS THAN MAXVALUE
+);
+
+SELECT '鍒嗗尯琛ㄥ垱寤烘垚鍔�' as result;
+
+-- ========================================
+-- 绗笁姝ワ細杩佺Щ鏁版嵁
+-- ========================================
+SELECT '寮�濮嬭縼绉绘暟鎹�...' as info, NOW() as start_time;
+
+-- 涓�娆℃�ц縼绉伙紙閫傜敤浜庢暟鎹噺涓嶈秴杩�500涓囷級
+INSERT INTO tb_vehicle_gps_segment_mileage_partitioned
+SELECT * FROM tb_vehicle_gps_segment_mileage;
+
+SELECT '鏁版嵁杩佺Щ瀹屾垚' as info, NOW() as end_time;
+
+-- ========================================
+-- 绗洓姝ワ細楠岃瘉鏁版嵁
+-- ========================================
+SELECT '=== 鏁版嵁楠岃瘉 ===' as info;
+
+-- 姣旇緝璁板綍鏁�
+SELECT
+ '鍘熻〃' as table_name,
+ COUNT(*) as record_count
+FROM tb_vehicle_gps_segment_mileage
+UNION ALL
+SELECT
+ '鏂拌〃(鍒嗗尯)' as table_name,
+ COUNT(*) as record_count
+FROM tb_vehicle_gps_segment_mileage_partitioned;
+
+-- 姣旇緝缁熻鏁版嵁
+SELECT
+ '鍘熻〃' as table_name,
+ SUM(segment_distance) as total_distance,
+ MIN(segment_start_time) as min_time,
+ MAX(segment_start_time) as max_time
+FROM tb_vehicle_gps_segment_mileage
+UNION ALL
+SELECT
+ '鏂拌〃(鍒嗗尯)' as table_name,
+ SUM(segment_distance) as total_distance,
+ MIN(segment_start_time) as min_time,
+ MAX(segment_start_time) as max_time
+FROM tb_vehicle_gps_segment_mileage_partitioned;
+
+-- 鏌ョ湅鍒嗗尯鍒嗗竷
+SELECT
+ PARTITION_NAME as '鍒嗗尯鍚�',
+ TABLE_ROWS as '璁板綍鏁�',
+ ROUND(DATA_LENGTH/1024/1024, 2) as '鏁版嵁(MB)',
+ ROUND(INDEX_LENGTH/1024/1024, 2) as '绱㈠紩(MB)'
+FROM information_schema.PARTITIONS
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_gps_segment_mileage_partitioned'
+ORDER BY PARTITION_ORDINAL_POSITION;
+
+-- 鏆傚仠锛佽妫�鏌ユ暟鎹槸鍚︿竴鑷�
+-- 濡傛灉鏁版嵁涓�鑷达紝缁х画鎵ц涓嬩竴姝�
+-- 濡傛灉涓嶄竴鑷达紝璇峰仠姝㈠苟妫�鏌ラ棶棰�
+
+-- ========================================
+-- 绗簲姝ワ細鍒囨崲琛ㄥ悕锛堣皑鎱庯紒锛�
+-- ========================================
+-- 寤鸿鍦ㄤ笟鍔″仠鏈虹獥鍙f墽琛屼互涓嬫搷浣�
+
+-- START TRANSACTION;
+
+SELECT '寮�濮嬪垏鎹㈣〃鍚�...' as info;
+
+-- 閲嶅懡鍚嶅師琛ㄤ负澶囦唤琛�
+RENAME TABLE
+ tb_vehicle_gps_segment_mileage TO tb_vehicle_gps_segment_mileage_old,
+ tb_vehicle_gps_segment_mileage_partitioned TO tb_vehicle_gps_segment_mileage;
+
+SELECT '琛ㄥ悕鍒囨崲瀹屾垚锛岃绔嬪嵆楠岃瘉搴旂敤鍔熻兘锛�' as result;
+
+-- 濡傛灉鏈夐棶棰橈紝绔嬪嵆鍥炴粴锛�
+-- RENAME TABLE
+-- tb_vehicle_gps_segment_mileage TO tb_vehicle_gps_segment_mileage_partitioned,
+-- tb_vehicle_gps_segment_mileage_old TO tb_vehicle_gps_segment_mileage;
+
+-- COMMIT;
+
+-- ========================================
+-- 绗叚姝ワ細楠岃瘉鍒嗗尯鏁堟灉
+-- ========================================
+SELECT '=== 娴嬭瘯鏌ヨ鎬ц兘 ===' as info;
+
+-- 娴嬭瘯1锛氭煡璇㈡渶杩�1鏈堟暟鎹紙搴旇鍙壂鎻�1涓垎鍖猴級
+EXPLAIN PARTITIONS
+SELECT COUNT(*), SUM(segment_distance)
+FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH);
+
+-- 娴嬭瘯2锛氭寜杞﹁締ID鏌ヨ鏈�杩�1鍛ㄦ暟鎹�
+EXPLAIN PARTITIONS
+SELECT *
+FROM tb_vehicle_gps_segment_mileage
+WHERE vehicle_id = 1
+ AND segment_start_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+LIMIT 10;
+
+-- 娴嬭瘯3锛氱粺璁℃渶杩�3涓湀鐨勯噷绋�
+SELECT
+ DATE_FORMAT(segment_start_time, '%Y-%m') as month,
+ COUNT(*) as segments,
+ ROUND(SUM(segment_distance), 2) as total_km
+FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= DATE_SUB(CURDATE(), INTERVAL 3 MONTH)
+GROUP BY DATE_FORMAT(segment_start_time, '%Y-%m');
+
+-- ========================================
+-- 绗竷姝ワ細娓呯悊鍜屼紭鍖栵紙鍙�夛級
+-- ========================================
+
+-- 纭搴旂敤杩愯姝e父鍚庯紝绛夊緟1-2鍛紝鐒跺悗鍒犻櫎澶囦唤琛�
+-- DROP TABLE tb_vehicle_gps_segment_mileage_old;
+
+-- 鍒嗘瀽琛紝浼樺寲鏌ヨ璁″垝
+ANALYZE TABLE tb_vehicle_gps_segment_mileage;
+
+-- ========================================
+-- 瀹屾垚锛�
+-- ========================================
+SELECT '========================================' as info;
+SELECT '鍒嗗尯浼樺寲瀹屾垚锛�' as result;
+SELECT '璇锋敞鎰忥細' as notice;
+SELECT '1. 瀹氭湡娣诲姞鏂版湀浠界殑鍒嗗尯' as task1;
+SELECT '2. 瀹氭湡娓呯悊鍘嗗彶鍒嗗尯閲婃斁绌洪棿' as task2;
+SELECT '3. 鏌ヨ鏃跺敖閲忓甫涓婃椂闂磋寖鍥存潯浠�' as task3;
+SELECT '========================================' as info;
+
+-- ========================================
+-- 鏃ュ父缁存姢鍛戒护鍙傝��
+-- ========================================
+
+-- 娣诲姞鏂板垎鍖猴紙姣忔湀鎵ц涓�娆★級
+-- ALTER TABLE tb_vehicle_gps_segment_mileage
+-- REORGANIZE PARTITION pfuture INTO (
+-- PARTITION p202701 VALUES LESS THAN (TO_DAYS('2027-02-01')),
+-- PARTITION pfuture VALUES LESS THAN MAXVALUE
+-- );
+
+-- 鍒犻櫎鍘嗗彶鍒嗗尯锛堥噴鏀剧┖闂达級
+-- ALTER TABLE tb_vehicle_gps_segment_mileage DROP PARTITION p202401;
+
+-- 鏌ョ湅鍒嗗尯鐘舵��
+-- SELECT * FROM information_schema.PARTITIONS
+-- WHERE TABLE_SCHEMA = DATABASE()
+-- AND TABLE_NAME = 'tb_vehicle_gps_segment_mileage';
diff --git a/sql/partition_vehicle_gps_segment_mileage.sql b/sql/partition_vehicle_gps_segment_mileage.sql
new file mode 100644
index 0000000..37b8105
--- /dev/null
+++ b/sql/partition_vehicle_gps_segment_mileage.sql
@@ -0,0 +1,304 @@
+-- ========================================
+-- GPS鍒嗘閲岀▼琛ㄥ垎鍖轰紭鍖栨柟妗�
+-- ========================================
+-- 鍔熻兘璇存槑锛�
+-- 1. 灏� tb_vehicle_gps_segment_mileage 琛ㄦ寜鏈堣繘琛屽垎鍖�
+-- 2. 鎻愰珮澶ф暟鎹噺涓嬬殑鏌ヨ鎬ц兘
+-- 3. 鏂逛究鍘嗗彶鏁版嵁褰掓。鍜屽垹闄�
+--
+-- 娉ㄦ剰浜嬮」锛�
+-- 1. 鎵ц姝よ剼鏈墠璇峰浠芥暟鎹簱锛�
+-- 2. 鏁版嵁閲忓ぇ鏃惰浆鎹㈣繃绋嬭緝鎱紝寤鸿鍦ㄤ笟鍔′綆宄版湡鎵ц
+-- 3. 鍒嗗尯鍚庝富閿拰鍞竴閿繀椤诲寘鍚垎鍖洪敭锛坰egment_start_time锛�
+-- ========================================
+
+-- 姝ラ1锛氬浠藉師琛ㄦ暟鎹�
+-- 寤鸿鍏堟墜鍔ㄦ墽琛岋細mysqldump -u鐢ㄦ埛鍚� -p 鏁版嵁搴撳悕 tb_vehicle_gps_segment_mileage > backup_segment_mileage.sql
+
+-- 姝ラ2锛氬垱寤烘柊鐨勫垎鍖鸿〃
+CREATE TABLE `tb_vehicle_gps_segment_mileage_new` (
+ `segment_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '鍒嗘ID',
+ `vehicle_id` bigint(20) NOT NULL COMMENT '杞﹁締ID',
+ `vehicle_no` varchar(20) DEFAULT NULL COMMENT '杞︾墝鍙�',
+ `segment_start_time` datetime NOT NULL COMMENT '鏃堕棿娈靛紑濮嬫椂闂�',
+ `segment_end_time` datetime NOT NULL COMMENT '鏃堕棿娈电粨鏉熸椂闂�',
+ `start_longitude` decimal(10,7) DEFAULT NULL COMMENT '璧风偣缁忓害',
+ `start_latitude` decimal(10,7) DEFAULT NULL COMMENT '璧风偣绾害',
+ `end_longitude` decimal(10,7) DEFAULT NULL COMMENT '缁堢偣缁忓害',
+ `end_latitude` decimal(10,7) DEFAULT NULL COMMENT '缁堢偣绾害',
+ `segment_distance` decimal(10,3) DEFAULT 0.000 COMMENT '娈佃窛绂�(鍏噷)',
+ `gps_point_count` int(11) DEFAULT 0 COMMENT 'GPS鐐规暟閲�',
+ `gps_ids` text COMMENT '鍏宠仈鐨凣PS璁板綍ID鍒楄〃锛堥�楀彿鍒嗛殧锛�',
+ `task_id` bigint(20) DEFAULT NULL COMMENT '鍏宠仈浠诲姟ID',
+ `task_code` varchar(50) DEFAULT NULL COMMENT '浠诲姟缂栧彿',
+ `calculate_method` varchar(20) DEFAULT 'tianditu' COMMENT '璁$畻鏂瑰紡(tianditu-澶╁湴鍥�/haversine-鐞冮潰璺濈)',
+ `create_time` datetime DEFAULT NULL COMMENT '鍒涘缓鏃堕棿',
+ `update_time` datetime DEFAULT NULL COMMENT '鏇存柊鏃堕棿',
+ PRIMARY KEY (`segment_id`, `segment_start_time`),
+ -- 娉ㄦ剰锛氬垎鍖鸿〃鐨勫敮涓�閿繀椤诲寘鍚垎鍖洪敭
+ UNIQUE KEY `uk_vehicle_time` (`vehicle_id`, `segment_start_time`),
+ KEY `idx_vehicle_id` (`vehicle_id`),
+ KEY `idx_start_time` (`segment_start_time`),
+ KEY `idx_task_id` (`task_id`),
+ KEY `idx_vehicle_task` (`vehicle_id`, `task_id`),
+ KEY `idx_vehicle_date` (`vehicle_id`, `segment_start_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='杞﹁締GPS鍒嗘閲岀▼琛紙鍒嗗尯鐗堬級'
+PARTITION BY RANGE (TO_DAYS(segment_start_time)) (
+ -- 2024骞村垎鍖�
+ PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
+ PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
+ PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
+ PARTITION p202404 VALUES LESS THAN (TO_DAYS('2024-05-01')),
+ PARTITION p202405 VALUES LESS THAN (TO_DAYS('2024-06-01')),
+ PARTITION p202406 VALUES LESS THAN (TO_DAYS('2024-07-01')),
+ PARTITION p202407 VALUES LESS THAN (TO_DAYS('2024-08-01')),
+ PARTITION p202408 VALUES LESS THAN (TO_DAYS('2024-09-01')),
+ PARTITION p202409 VALUES LESS THAN (TO_DAYS('2024-10-01')),
+ PARTITION p202410 VALUES LESS THAN (TO_DAYS('2024-11-01')),
+ PARTITION p202411 VALUES LESS THAN (TO_DAYS('2024-12-01')),
+ PARTITION p202412 VALUES LESS THAN (TO_DAYS('2025-01-01')),
+
+ -- 2025骞村垎鍖�
+ PARTITION p202501 VALUES LESS THAN (TO_DAYS('2025-02-01')),
+ PARTITION p202502 VALUES LESS THAN (TO_DAYS('2025-03-01')),
+ PARTITION p202503 VALUES LESS THAN (TO_DAYS('2025-04-01')),
+ PARTITION p202504 VALUES LESS THAN (TO_DAYS('2025-05-01')),
+ PARTITION p202505 VALUES LESS THAN (TO_DAYS('2025-06-01')),
+ PARTITION p202506 VALUES LESS THAN (TO_DAYS('2025-07-01')),
+ PARTITION p202507 VALUES LESS THAN (TO_DAYS('2025-08-01')),
+ PARTITION p202508 VALUES LESS THAN (TO_DAYS('2025-09-01')),
+ PARTITION p202509 VALUES LESS THAN (TO_DAYS('2025-10-01')),
+ PARTITION p202510 VALUES LESS THAN (TO_DAYS('2025-11-01')),
+ PARTITION p202511 VALUES LESS THAN (TO_DAYS('2025-12-01')),
+ PARTITION p202512 VALUES LESS THAN (TO_DAYS('2026-01-01')),
+
+ -- 2026骞村垎鍖�
+ PARTITION p202601 VALUES LESS THAN (TO_DAYS('2026-02-01')),
+ PARTITION p202602 VALUES LESS THAN (TO_DAYS('2026-03-01')),
+ PARTITION p202603 VALUES LESS THAN (TO_DAYS('2026-04-01')),
+ PARTITION p202604 VALUES LESS THAN (TO_DAYS('2026-05-01')),
+ PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
+ PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
+ PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
+ PARTITION p202608 VALUES LESS THAN (TO_DAYS('2026-09-01')),
+ PARTITION p202609 VALUES LESS THAN (TO_DAYS('2026-10-01')),
+ PARTITION p202610 VALUES LESS THAN (TO_DAYS('2026-11-01')),
+ PARTITION p202611 VALUES LESS THAN (TO_DAYS('2026-12-01')),
+ PARTITION p202612 VALUES LESS THAN (TO_DAYS('2027-01-01')),
+
+ -- 鏈潵鏁版嵁鍒嗗尯锛堝彲浠ュ绾�2027骞村強浠ュ悗鐨勬暟鎹級
+ PARTITION pfuture VALUES LESS THAN MAXVALUE
+);
+
+-- 姝ラ3锛氳縼绉绘暟鎹埌鏂拌〃
+-- 鏂瑰紡1锛氫竴娆℃�ц縼绉伙紙閫傜敤浜庢暟鎹噺杈冨皬锛屽100涓囦互涓嬶級
+-- INSERT INTO tb_vehicle_gps_segment_mileage_new SELECT * FROM tb_vehicle_gps_segment_mileage;
+
+-- 鏂瑰紡2锛氬垎鎵硅縼绉伙紙閫傜敤浜庡ぇ鏁版嵁閲忥紝鎺ㄨ崘锛�
+-- 鎸夋湀浠藉垎鎵硅縼绉伙紝鍑忓皯閿佽〃鏃堕棿
+-- 2024骞�1鏈�
+INSERT INTO tb_vehicle_gps_segment_mileage_new
+SELECT * FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= '2024-01-01' AND segment_start_time < '2024-02-01';
+
+-- 2024骞�2鏈�
+INSERT INTO tb_vehicle_gps_segment_mileage_new
+SELECT * FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= '2024-02-01' AND segment_start_time < '2024-03-01';
+
+-- 缁х画鎸夋湀杩佺Щ...锛堟牴鎹疄闄呮暟鎹儏鍐佃皟鏁达級
+-- 2024骞�3鏈堣嚦12鏈�
+INSERT INTO tb_vehicle_gps_segment_mileage_new
+SELECT * FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= '2024-03-01' AND segment_start_time < '2025-01-01';
+
+-- 2025骞存暟鎹�
+INSERT INTO tb_vehicle_gps_segment_mileage_new
+SELECT * FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= '2025-01-01' AND segment_start_time < '2026-01-01';
+
+-- 2026骞存暟鎹�
+INSERT INTO tb_vehicle_gps_segment_mileage_new
+SELECT * FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= '2026-01-01';
+
+-- 姝ラ4锛氶獙璇佹暟鎹竴鑷存��
+-- 妫�鏌ュ師琛ㄥ拰鏂拌〃鐨勮褰曟暟
+SELECT 'Original Table' as table_name, COUNT(*) as record_count FROM tb_vehicle_gps_segment_mileage
+UNION ALL
+SELECT 'New Table' as table_name, COUNT(*) as record_count FROM tb_vehicle_gps_segment_mileage_new;
+
+-- 妫�鏌ュ悇鍒嗗尯鐨勬暟鎹噺
+SELECT
+ PARTITION_NAME,
+ TABLE_ROWS,
+ AVG_ROW_LENGTH,
+ DATA_LENGTH,
+ INDEX_LENGTH,
+ CREATE_TIME
+FROM information_schema.PARTITIONS
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_gps_segment_mileage_new'
+ORDER BY PARTITION_NAME;
+
+-- 姝ラ5锛氬垏鎹㈣〃鍚嶏紙璋ㄦ厧鎿嶄綔锛侊級
+-- 寤鸿鍦ㄤ笟鍔′綆宄版湡鎵ц锛屾暣涓搷浣滃簲鍦ㄤ簨鍔′腑瀹屾垚
+-- START TRANSACTION;
+
+-- 閲嶅懡鍚嶅師琛ㄤ负澶囦唤琛�
+RENAME TABLE tb_vehicle_gps_segment_mileage TO tb_vehicle_gps_segment_mileage_backup;
+
+-- 灏嗘柊琛ㄩ噸鍛藉悕涓烘寮忚〃
+RENAME TABLE tb_vehicle_gps_segment_mileage_new TO tb_vehicle_gps_segment_mileage;
+
+-- 濡傛灉涓�鍒囨甯革紝鎻愪氦浜嬪姟
+-- COMMIT;
+
+-- 濡傛灉鍑虹幇闂锛屽洖婊�
+-- ROLLBACK;
+
+-- 姝ラ6锛氶獙璇佸簲鐢ㄦ甯歌繍琛�
+-- 璇峰湪搴旂敤灞傛祴璇曚互涓嬪姛鑳斤細
+-- 1. GPS閲岀▼璁$畻鍔熻兘
+-- 2. 杞﹁締閲岀▼鏌ヨ
+-- 3. 浠诲姟閲岀▼缁熻
+-- 4. 鐩稿叧鎶ヨ〃鍔熻兘
+
+-- 姝ラ7锛氱‘璁ゆ棤璇悗鍒犻櫎澶囦唤琛紙鍙�夛紝寤鸿淇濈暀涓�娈垫椂闂达級
+-- DROP TABLE tb_vehicle_gps_segment_mileage_backup;
+
+-- ========================================
+-- 鍒嗗尯缁存姢鎿嶄綔锛堝畾鏈熸墽琛岋級
+-- ========================================
+
+-- 娣诲姞鏂版湀浠界殑鍒嗗尯锛堟瘡鏈堟垨姣忓搴︽墽琛屼竴娆★級
+-- 渚嬪锛氭坊鍔�2027骞�1鏈堢殑鍒嗗尯
+-- ALTER TABLE tb_vehicle_gps_segment_mileage
+-- REORGANIZE PARTITION pfuture INTO (
+-- PARTITION p202701 VALUES LESS THAN (TO_DAYS('2027-02-01')),
+-- PARTITION pfuture VALUES LESS THAN MAXVALUE
+-- );
+
+-- 鍒犻櫎鍘嗗彶鍒嗗尯锛堝綊妗f棫鏁版嵁锛岄噴鏀剧┖闂达級
+-- 渚嬪锛氬垹闄�2024骞�1鏈堢殑鏁版嵁锛堝垹闄ゅ墠璇风‘淇濆凡澶囦唤锛�
+-- ALTER TABLE tb_vehicle_gps_segment_mileage DROP PARTITION p202401;
+
+-- 鎴栬�呮竻绌哄垎鍖烘暟鎹絾淇濈暀鍒嗗尯缁撴瀯
+-- ALTER TABLE tb_vehicle_gps_segment_mileage TRUNCATE PARTITION p202401;
+
+-- ========================================
+-- 鎬ц兘浼樺寲寤鸿
+-- ========================================
+
+-- 1. 鏌ヨ鏃跺敖閲忓甫涓婃椂闂磋寖鍥存潯浠讹紝浠ュ厖鍒嗗埄鐢ㄥ垎鍖鸿鍓�
+-- 绀轰緥锛�
+-- SELECT * FROM tb_vehicle_gps_segment_mileage
+-- WHERE segment_start_time >= '2025-01-01'
+-- AND segment_start_time < '2025-02-01'
+-- AND vehicle_id = 123;
+
+-- 2. 瀹氭湡鍒嗘瀽琛ㄤ互浼樺寲鏌ヨ璁″垝
+-- ANALYZE TABLE tb_vehicle_gps_segment_mileage;
+
+-- 3. 瀹氭湡浼樺寲琛ㄤ互鍥炴敹绌洪棿
+-- OPTIMIZE TABLE tb_vehicle_gps_segment_mileage;
+
+-- 4. 鏌ョ湅鍒嗗尯浣跨敤鎯呭喌
+SELECT
+ PARTITION_NAME as '鍒嗗尯鍚�',
+ PARTITION_METHOD as '鍒嗗尯鏂瑰紡',
+ PARTITION_EXPRESSION as '鍒嗗尯琛ㄨ揪寮�',
+ TABLE_ROWS as '璁板綍鏁�',
+ ROUND(DATA_LENGTH/1024/1024, 2) as '鏁版嵁澶у皬(MB)',
+ ROUND(INDEX_LENGTH/1024/1024, 2) as '绱㈠紩澶у皬(MB)',
+ PARTITION_COMMENT as '澶囨敞'
+FROM information_schema.PARTITIONS
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_gps_segment_mileage'
+ORDER BY PARTITION_ORDINAL_POSITION;
+
+-- ========================================
+-- 鍒嗗尯鑷姩缁存姢鑴氭湰锛堝缓璁厤缃畾鏃朵换鍔★級
+-- ========================================
+
+DELIMITER $$
+
+CREATE PROCEDURE add_gps_segment_partition()
+BEGIN
+ DECLARE next_month_date DATE;
+ DECLARE partition_name VARCHAR(20);
+ DECLARE next_partition_date DATE;
+
+ -- 璁$畻涓嬩釜鏈堢殑鏃ユ湡
+ SET next_month_date = DATE_ADD(CURDATE(), INTERVAL 2 MONTH);
+ SET next_month_date = DATE_FORMAT(next_month_date, '%Y-%m-01');
+
+ -- 鐢熸垚鍒嗗尯鍚嶇О锛堜緥濡傦細p202701锛�
+ SET partition_name = CONCAT('p', DATE_FORMAT(next_month_date, '%Y%m'));
+
+ -- 璁$畻涓嬩竴涓垎鍖虹殑杈圭晫鏃ユ湡
+ SET next_partition_date = DATE_ADD(next_month_date, INTERVAL 1 MONTH);
+
+ -- 鍔ㄦ�佹坊鍔犲垎鍖�
+ SET @sql = CONCAT(
+ 'ALTER TABLE tb_vehicle_gps_segment_mileage ',
+ 'REORGANIZE PARTITION pfuture INTO (',
+ 'PARTITION ', partition_name, ' VALUES LESS THAN (TO_DAYS(''', next_partition_date, ''')),',
+ 'PARTITION pfuture VALUES LESS THAN MAXVALUE',
+ ')'
+ );
+
+ PREPARE stmt FROM @sql;
+ EXECUTE stmt;
+ DEALLOCATE PREPARE stmt;
+
+ SELECT CONCAT('鎴愬姛娣诲姞鍒嗗尯: ', partition_name, ', 杈圭晫鏃ユ湡: ', next_partition_date) as result;
+END$$
+
+DELIMITER ;
+
+-- 浣跨敤鏂规硶锛氭瘡鏈堟墽琛屼竴娆�
+-- CALL add_gps_segment_partition();
+
+-- ========================================
+-- 鍘嗗彶鏁版嵁褰掓。绛栫暐锛堝彲閫夛級
+-- ========================================
+
+-- 鏂规1锛氬鍑哄巻鍙插垎鍖哄埌褰掓。琛�
+-- CREATE TABLE tb_vehicle_gps_segment_mileage_archive LIKE tb_vehicle_gps_segment_mileage;
+-- ALTER TABLE tb_vehicle_gps_segment_mileage_archive REMOVE PARTITIONING;
+-- INSERT INTO tb_vehicle_gps_segment_mileage_archive
+-- SELECT * FROM tb_vehicle_gps_segment_mileage PARTITION(p202401);
+
+-- 鏂规2锛氬鍑哄埌鏂囦欢
+-- SELECT * INTO OUTFILE '/tmp/gps_segment_202401.csv'
+-- FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
+-- LINES TERMINATED BY '\n'
+-- FROM tb_vehicle_gps_segment_mileage PARTITION(p202401);
+
+-- 鏂规3锛氬畾鏈熷垹闄よ秴杩嘚涓湀鐨勫巻鍙叉暟鎹�
+-- 渚嬪锛氬垹闄�12涓湀涔嬪墠鐨勬暟鎹�
+-- ALTER TABLE tb_vehicle_gps_segment_mileage
+-- DROP PARTITION p202401;
+
+-- ========================================
+-- 鐩戞帶鍜屽憡璀︼紙寤鸿锛�
+-- ========================================
+
+-- 鐩戞帶鍚勫垎鍖虹殑鏁版嵁閲忓闀�
+CREATE VIEW v_gps_segment_partition_stats AS
+SELECT
+ PARTITION_NAME as partition_name,
+ TABLE_ROWS as row_count,
+ ROUND(DATA_LENGTH/1024/1024, 2) as data_size_mb,
+ ROUND(INDEX_LENGTH/1024/1024, 2) as index_size_mb,
+ ROUND((DATA_LENGTH + INDEX_LENGTH)/1024/1024, 2) as total_size_mb,
+ CREATE_TIME as create_time,
+ UPDATE_TIME as update_time
+FROM information_schema.PARTITIONS
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_gps_segment_mileage'
+ORDER BY PARTITION_ORDINAL_POSITION;
+
+-- 鏌ョ湅鍒嗗尯缁熻
+-- SELECT * FROM v_gps_segment_partition_stats;
diff --git "a/sql/\345\210\206\345\214\272\344\274\230\345\214\226\346\226\271\346\241\210\350\257\264\346\230\216.md" "b/sql/\345\210\206\345\214\272\344\274\230\345\214\226\346\226\271\346\241\210\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..6a39021
--- /dev/null
+++ "b/sql/\345\210\206\345\214\272\344\274\230\345\214\226\346\226\271\346\241\210\350\257\264\346\230\216.md"
@@ -0,0 +1,293 @@
+# GPS鍒嗘閲岀▼琛ㄥ垎鍖轰紭鍖栨柟妗�
+
+## 涓�銆侀棶棰樺垎鏋�
+
+### 褰撳墠鎯呭喌
+- **琛ㄥ悕**: `tb_vehicle_gps_segment_mileage`
+- **鏁版嵁鐗圭偣**: 鎸�5鍒嗛挓鏃堕棿娈电粺璁PS閲岀▼锛屾暟鎹噺澧為暱蹇�
+- **涓昏闂**:
+ - 鏁版嵁閲忓ぇ瀵艰嚧鏌ヨ鍙樻參
+ - 绱㈠紩鏁堢巼闄嶄綆
+ - 鍘嗗彶鏁版嵁闅句互娓呯悊
+
+### 鍒嗗尯浼樺寲鏀剁泭
+鉁� **鏌ヨ鎬ц兘鎻愬崌**: 閫氳繃鍒嗗尯瑁佸壀锛屾煡璇㈠彧鎵弿鐩稿叧鍒嗗尯锛屾�ц兘鎻愬崌50%-80%
+鉁� **缁存姢绠�鍖�**: 鎸夋湀鍒嗗尯锛屽彲浠ュ揩閫熷垹闄ゆ垨褰掓。鍘嗗彶鏁版嵁
+鉁� **瀛樺偍浼樺寲**: 渚夸簬鏁版嵁褰掓。锛岄噴鏀剧鐩樼┖闂�
+鉁� **骞跺彂浼樺寲**: 涓嶅悓鍒嗗尯鍙互骞惰鎿嶄綔锛屽噺灏戦攣鍐茬獊
+
+---
+
+## 浜屻�佸垎鍖烘柟妗堣璁�
+
+### 1. 鍒嗗尯绛栫暐
+- **鍒嗗尯绫诲瀷**: RANGE 鍒嗗尯锛堟寜鏃堕棿鑼冨洿锛�
+- **鍒嗗尯閿�**: `segment_start_time`锛堟椂闂存寮�濮嬫椂闂达級
+- **鍒嗗尯绮掑害**: 鎸夋湀鍒嗗尯
+- **淇濈暀鍛ㄦ湡**: 寤鸿淇濈暀鏈�杩�12-24涓湀鏁版嵁
+
+### 2. 鍒嗗尯缁撴瀯
+```
+2024骞�: p202401, p202402, ..., p202412 (12涓垎鍖�)
+2025骞�: p202501, p202502, ..., p202512 (12涓垎鍖�)
+2026骞�: p202601, p202602, ..., p202612 (12涓垎鍖�)
+鏈潵: pfuture (瀹圭撼鎵�鏈夋湭鏉ユ暟鎹�)
+```
+
+### 3. 鍏抽敭鍙樻洿鐐�
+鈿狅笍 **閲嶈**: 鍒嗗尯琛ㄧ殑涓婚敭鍜屽敮涓�閿繀椤诲寘鍚垎鍖洪敭
+
+**鍘熻〃缁撴瀯**:
+```sql
+PRIMARY KEY (`segment_id`),
+UNIQUE KEY `uk_vehicle_time` (`vehicle_id`, `segment_start_time`)
+```
+
+**鏂拌〃缁撴瀯**:
+```sql
+PRIMARY KEY (`segment_id`, `segment_start_time`), -- 涓婚敭鍖呭惈鍒嗗尯閿�
+UNIQUE KEY `uk_vehicle_time` (`vehicle_id`, `segment_start_time`) -- 宸插寘鍚垎鍖洪敭
+```
+
+---
+
+## 涓夈�佹墽琛屾楠�
+
+### 姝ラ1: 鏁版嵁澶囦唤锛堝繀椤伙紒锛�
+```bash
+# 澶囦唤鏁翠釜鏁版嵁搴�
+mysqldump -u鐢ㄦ埛鍚� -p 鏁版嵁搴撳悕 > backup_$(date +%Y%m%d).sql
+
+# 鎴栧彧澶囦唤鍗曡〃
+mysqldump -u鐢ㄦ埛鍚� -p 鏁版嵁搴撳悕 tb_vehicle_gps_segment_mileage > backup_segment_mileage_$(date +%Y%m%d).sql
+```
+
+### 姝ラ2: 鏌ョ湅褰撳墠鏁版嵁閲�
+```sql
+-- 鏌ョ湅鎬昏褰曟暟
+SELECT COUNT(*) as total_records FROM tb_vehicle_gps_segment_mileage;
+
+-- 鏌ョ湅鎸夋湀鍒嗗竷
+SELECT
+ DATE_FORMAT(segment_start_time, '%Y-%m') as month,
+ COUNT(*) as record_count,
+ ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM tb_vehicle_gps_segment_mileage), 2) as percentage
+FROM tb_vehicle_gps_segment_mileage
+GROUP BY DATE_FORMAT(segment_start_time, '%Y-%m')
+ORDER BY month;
+
+-- 鏌ョ湅琛ㄥぇ灏�
+SELECT
+ TABLE_NAME,
+ ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS size_mb,
+ TABLE_ROWS
+FROM information_schema.TABLES
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_gps_segment_mileage';
+```
+
+### 姝ラ3: 鎵ц鍒嗗尯鑴氭湰
+```sql
+-- 鍦ㄤ笟鍔′綆宄版湡鎵ц partition_vehicle_gps_segment_mileage.sql
+source d:/project/鎬ユ晳杞繍/code/Api/RuoYi-Vue-master/sql/partition_vehicle_gps_segment_mileage.sql
+```
+
+**鎴栧垎姝ユ墽琛�**:
+1. 鍒涘缓鏂板垎鍖鸿〃锛坄tb_vehicle_gps_segment_mileage_new`锛�
+2. 鍒嗘壒杩佺Щ鏁版嵁
+3. 楠岃瘉鏁版嵁涓�鑷存��
+4. 鍒囨崲琛ㄥ悕
+
+### 姝ラ4: 楠岃瘉搴旂敤鍔熻兘
+娴嬭瘯浠ヤ笅鍔熻兘鏄惁姝e父锛�
+- 鉁� GPS閲岀▼璁$畻浠诲姟
+- 鉁� 杞﹁締閲岀▼鏌ヨ
+- 鉁� 浠诲姟閲岀▼缁熻
+- 鉁� 閲岀▼鎶ヨ〃
+
+### 姝ラ5: 鍒犻櫎澶囦唤琛紙鍙�夛級
+```sql
+-- 纭杩愯姝e父鍚庯紝鍙互鍒犻櫎澶囦唤琛紙寤鸿淇濈暀1-2鍛級
+DROP TABLE tb_vehicle_gps_segment_mileage_backup;
+```
+
+---
+
+## 鍥涖�佹棩甯哥淮鎶ゆ搷浣�
+
+### 1. 娣诲姞鏂版湀浠藉垎鍖猴紙姣忔湀鎵ц锛�
+```sql
+-- 鎵嬪姩娣诲姞2027骞�2鏈堝垎鍖�
+ALTER TABLE tb_vehicle_gps_segment_mileage
+REORGANIZE PARTITION pfuture INTO (
+ PARTITION p202702 VALUES LESS THAN (TO_DAYS('2027-03-01')),
+ PARTITION pfuture VALUES LESS THAN MAXVALUE
+);
+
+-- 鎴栦娇鐢ㄥ瓨鍌ㄨ繃绋嬭嚜鍔ㄦ坊鍔�
+CALL add_gps_segment_partition();
+```
+
+### 2. 鍒犻櫎鍘嗗彶鍒嗗尯锛堥噴鏀剧┖闂达級
+```sql
+-- 鏂瑰紡1: 鐩存帴鍒犻櫎鍒嗗尯锛堟暟鎹笉鍙仮澶嶏級
+ALTER TABLE tb_vehicle_gps_segment_mileage DROP PARTITION p202401;
+
+-- 鏂瑰紡2: 褰掓。鍚庡垹闄�
+-- 鍏堝鍑烘暟鎹埌褰掓。琛�
+CREATE TABLE tb_vehicle_gps_segment_mileage_archive_202401
+SELECT * FROM tb_vehicle_gps_segment_mileage PARTITION(p202401);
+
+-- 鐒跺悗鍒犻櫎鍒嗗尯
+ALTER TABLE tb_vehicle_gps_segment_mileage DROP PARTITION p202401;
+```
+
+### 3. 鏌ョ湅鍒嗗尯鐘舵��
+```sql
+-- 鏌ョ湅鎵�鏈夊垎鍖轰俊鎭�
+SELECT * FROM v_gps_segment_partition_stats;
+
+-- 鎴栫洿鎺ユ煡璇�
+SELECT
+ PARTITION_NAME as '鍒嗗尯鍚�',
+ TABLE_ROWS as '璁板綍鏁�',
+ ROUND(DATA_LENGTH/1024/1024, 2) as '鏁版嵁(MB)',
+ ROUND(INDEX_LENGTH/1024/1024, 2) as '绱㈠紩(MB)'
+FROM information_schema.PARTITIONS
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'tb_vehicle_gps_segment_mileage'
+ORDER BY PARTITION_ORDINAL_POSITION;
+```
+
+### 4. 浼樺寲鍒嗗尯
+```sql
+-- 鍒嗘瀽琛紙鏇存柊缁熻淇℃伅锛�
+ANALYZE TABLE tb_vehicle_gps_segment_mileage;
+
+-- 浼樺寲琛紙鍥炴敹纰庣墖绌洪棿锛�
+OPTIMIZE TABLE tb_vehicle_gps_segment_mileage;
+```
+
+---
+
+## 浜斻�佹煡璇紭鍖栧缓璁�
+
+### 鉁� 濂界殑鏌ヨ锛堝埄鐢ㄥ垎鍖鸿鍓級
+```sql
+-- 绀轰緥1: 甯︽椂闂磋寖鍥寸殑鏌ヨ
+SELECT * FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= '2025-01-01'
+ AND segment_start_time < '2025-02-01'
+ AND vehicle_id = 123;
+
+-- 绀轰緥2: 鎸夋湀缁熻
+SELECT
+ DATE_FORMAT(segment_start_time, '%Y-%m') as month,
+ SUM(segment_distance) as total_distance
+FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= '2025-01-01'
+ AND segment_start_time < '2025-12-31'
+GROUP BY DATE_FORMAT(segment_start_time, '%Y-%m');
+```
+
+### 鉂� 涓嶅ソ鐨勬煡璇紙鍏ㄨ〃鎵弿锛�
+```sql
+-- 缂哄皯鏃堕棿鏉′欢锛屼細鎵弿鎵�鏈夊垎鍖�
+SELECT * FROM tb_vehicle_gps_segment_mileage
+WHERE vehicle_id = 123;
+
+-- 搴旇鏀逛负锛�
+SELECT * FROM tb_vehicle_gps_segment_mileage
+WHERE vehicle_id = 123
+ AND segment_start_time >= DATE_SUB(NOW(), INTERVAL 30 DAY);
+```
+
+---
+
+## 鍏�佺洃鎺у拰鍛婅
+
+### 1. 鐩戞帶鎸囨爣
+- 鍚勫垎鍖虹殑鏁版嵁閲�
+- 琛ㄥ拰绱㈠紩鐨勫ぇ灏�
+- 鏈�鏂板垎鍖烘槸鍚︽帴杩戞弧
+- 鏌ヨ鎬ц兘瀵规瘮锛堝垎鍖哄墠鍚庯級
+
+### 2. 瀹氭椂浠诲姟寤鸿
+```bash
+# 姣忔湀1鍙峰噷鏅�1鐐硅嚜鍔ㄦ坊鍔犳柊鍒嗗尯
+0 1 1 * * mysql -u鐢ㄦ埛鍚� -p瀵嗙爜 鏁版嵁搴撳悕 -e "CALL add_gps_segment_partition();"
+
+# 姣忓搴﹀垹闄�12涓湀涔嬪墠鐨勫巻鍙插垎鍖�
+0 2 1 1,4,7,10 * /path/to/cleanup_old_partitions.sh
+```
+
+---
+
+## 涓冦�佸洖婊氭柟妗�
+
+濡傛灉鍒嗗尯鍚庡嚭鐜伴棶棰橈紝鍙互蹇�熷洖婊氾細
+
+```sql
+-- 1. 鍋滄搴旂敤鍐欏叆
+
+-- 2. 鎭㈠鍘熻〃
+RENAME TABLE tb_vehicle_gps_segment_mileage TO tb_vehicle_gps_segment_mileage_failed;
+RENAME TABLE tb_vehicle_gps_segment_mileage_backup TO tb_vehicle_gps_segment_mileage;
+
+-- 3. 鍚屾鍒囨崲鏈熼棿鐨勬柊鏁版嵁锛堝鏋滄湁锛�
+INSERT INTO tb_vehicle_gps_segment_mileage
+SELECT * FROM tb_vehicle_gps_segment_mileage_failed
+WHERE create_time > (SELECT MAX(create_time) FROM tb_vehicle_gps_segment_mileage);
+
+-- 4. 閲嶅惎搴旂敤
+```
+
+---
+
+## 鍏�佹�ц兘瀵规瘮锛堥鏈燂級
+
+### 鍒嗗尯鍓�
+- 鏌ヨ鏈�杩�1鏈堟暟鎹�: ~5-10绉�
+- 鏌ヨ鏈�杩�3鏈堟暟鎹�: ~15-30绉�
+- 琛ㄥぇ灏�: 鎸佺画澧為暱锛岀储寮曟晥鐜囬檷浣�
+
+### 鍒嗗尯鍚�
+- 鏌ヨ鏈�杩�1鏈堟暟鎹�: ~1-2绉掞紙鎻愬崌70%-80%锛�
+- 鏌ヨ鏈�杩�3鏈堟暟鎹�: ~3-6绉掞紙鎻愬崌70%-80%锛�
+- 琛ㄧ淮鎶�: 鍙寜鏈堟竻鐞嗭紝绌洪棿鍙帶
+
+---
+
+## 涔濄�佸父瑙侀棶棰�
+
+### Q1: 鍒嗗尯浼氬奖鍝嶅簲鐢ㄤ唬鐮佸悧锛�
+**A**: 涓嶄細銆傚垎鍖哄搴旂敤閫忔槑锛孲QL璇彞涓嶉渶瑕佷慨鏀广��
+
+### Q2: 鍙互鍦ㄧ嚎杞崲鍚楋紵
+**A**: MySQL 5.7+ 鍙互鍦ㄧ嚎杞崲锛屼絾寤鸿鍦ㄤ綆宄版湡鎵ц锛屽ぇ琛ㄥ彲鑳介渶瑕佽緝闀挎椂闂淬��
+
+### Q3: 鍒嗗尯鍚庤兘鍥炲埌闈炲垎鍖鸿〃鍚楋紵
+**A**: 鍙互锛屼娇鐢� `ALTER TABLE ... REMOVE PARTITIONING;`
+
+### Q4: 涓婚敭蹇呴』鍖呭惈鍒嗗尯閿悧锛�
+**A**: 鏄殑锛岃繖鏄疢ySQL鍒嗗尯琛ㄧ殑闄愬埗銆�
+
+### Q5: 濡備綍纭畾淇濈暀澶氫箙鐨勫巻鍙叉暟鎹紵
+**A**: 鏍规嵁涓氬姟闇�姹傚拰瀛樺偍瀹归噺锛屽缓璁繚鐣�12-24涓湀锛屾洿鏃╃殑鏁版嵁褰掓。鍒板喎瀛樺偍銆�
+
+---
+
+## 鍗併�佽仈绯绘敮鎸�
+
+濡傛灉鎵ц杩囩▼涓亣鍒伴棶棰橈紝璇凤細
+1. 妫�鏌ラ敊璇棩蹇�: `/var/log/mysql/error.log`
+2. 鏌ョ湅鎱㈡煡璇㈡棩蹇楋紝瀵规瘮鎬ц兘
+3. 纭繚鏈夊畬鏁村浠�
+4. 蹇呰鏃惰仈绯籇BA鎴栨妧鏈敮鎸�
+
+---
+
+**鏈�鍚庢彁閱�**:
+鈿狅笍 鎵ц鍓嶅姟蹇呭浠斤紒
+鈿狅笍 閫夋嫨涓氬姟浣庡嘲鏈熸墽琛岋紒
+鈿狅笍 鍑嗗濂藉洖婊氭柟妗堬紒
--
Gitblit v1.9.1