增加报销模块功能

This commit is contained in:
2026-07-20 17:50:47 +08:00
parent f00ba9f9cc
commit a78666177a
15 changed files with 2706 additions and 248 deletions
+2
View File
@@ -65,6 +65,8 @@ declare module 'vue' {
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
+90
View File
@@ -0,0 +1,90 @@
import request from '@/utils/request'
export function getReimbursementList(params) {
return request({
url: '/backend/reimbursements',
method: 'get',
params
})
}
export function getReimbursementDetail(id) {
return request({
url: `/backend/reimbursements/${id}`,
method: 'get'
})
}
export function createReimbursement(data) {
return request({
url: '/backend/reimbursements',
method: 'post',
data
})
}
export function updateReimbursement(id, data) {
return request({
url: `/backend/reimbursements/${id}`,
method: 'post',
data
})
}
export function deleteReimbursement(id) {
return request({
url: `/backend/reimbursements/${id}`,
method: 'delete'
})
}
export function submitReimbursement(id) {
return request({
url: `/backend/reimbursements/${id}/submit`,
method: 'post'
})
}
export function getReimbursementRecords(id) {
return request({
url: `/backend/reimbursements/${id}/records`,
method: 'get'
})
}
export function addReimbursementItem(id, data) {
return request({
url: `/backend/reimbursements/${id}/items`,
method: 'post',
data
})
}
export function updateReimbursementItem(id, itemId, data) {
return request({
url: `/backend/reimbursements/${id}/items/${itemId}`,
method: 'post',
data
})
}
export function deleteReimbursementItem(id, itemId) {
return request({
url: `/backend/reimbursements/${id}/items/${itemId}`,
method: 'delete'
})
}
export function getApprovalFlows() {
return request({
url: '/backend/reimbursements/flows',
method: 'get'
})
}
export function getReimbursementExpenseTypes() {
return request({
url: '/backend/reimbursements/types',
method: 'get'
})
}
@@ -0,0 +1,329 @@
<template>
<div class="erp-dashboard">
<!-- 第一行统计卡片 -->
<el-row :gutter="16" class="stat-row">
<el-col :span="4" v-for="(item, index) in firstRowStats" :key="'first-' + index">
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">{{ item.label }}</span>
<el-icon class="stat-icon"><Info-Filled /></el-icon>
</div>
<div class="stat-value">
<span class="currency">¥</span>
<span class="number">{{ formatMoney(item.value) }}</span>
</div>
</div>
</el-col>
</el-row>
<!-- 第二行统计卡片 -->
<el-row :gutter="16" class="stat-row" style="margin-top: 16px;">
<el-col :span="4" v-for="(item, index) in secondRowStats" :key="'second-' + index">
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">{{ item.label }}</span>
<el-icon class="stat-icon"><Info-Filled /></el-icon>
</div>
<div class="stat-value">
<span class="currency">¥</span>
<span class="number">{{ formatMoney(item.value) }}</span>
</div>
</div>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="16" class="chart-row" style="margin-top: 16px;">
<el-col :span="8">
<div class="chart-card">
<div class="chart-title">销售统计</div>
<div ref="salesChartRef" class="chart-container"></div>
</div>
</el-col>
<el-col :span="8">
<div class="chart-card">
<div class="chart-title">零售统计</div>
<div ref="retailChartRef" class="chart-container"></div>
</div>
</el-col>
<el-col :span="8">
<div class="chart-card">
<div class="chart-title">采购统计</div>
<div ref="purchaseChartRef" class="chart-container"></div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
import * as echarts from 'echarts';
import { InfoFilled } from '@element-plus/icons-vue';
// 统计卡片数据 - 第一行
const firstRowStats = ref([
{ label: '今日销售', value: 0 },
{ label: '今日零售', value: 0 },
{ label: '今日采购', value: 0 },
{ label: '本月累计销售', value: 0 },
{ label: '本月累计零售', value: 0 },
{ label: '本月累计采购', value: 0 },
]);
// 统计卡片数据 - 第二行
const secondRowStats = ref([
{ label: '昨日销售', value: 0 },
{ label: '昨日零售', value: 0 },
{ label: '昨日采购', value: 0 },
{ label: '今年累计销售', value: 0 },
{ label: '今年累计零售', value: 0 },
{ label: '今年累计采购', value: 0 },
]);
// 图表实例
const salesChartRef = ref<HTMLElement | null>(null);
const retailChartRef = ref<HTMLElement | null>(null);
const purchaseChartRef = ref<HTMLElement | null>(null);
const salesChartInstance = shallowRef<echarts.ECharts | null>(null);
const retailChartInstance = shallowRef<echarts.ECharts | null>(null);
const purchaseChartInstance = shallowRef<echarts.ECharts | null>(null);
// 格式化金额
const formatMoney = (value: number): string => {
return value.toLocaleString();
};
// 生成最近6个月的月份标签
const getMonthLabels = (): string[] => {
const labels: string[] = [];
const now = new Date();
for (let i = 5; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
labels.push(`${year}-${month}`);
}
return labels;
};
// 初始化图表通用配置
const initChart = (refEl: HTMLElement, title: string, color: string) => {
const chart = echarts.init(refEl);
const months = getMonthLabels();
chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: months,
axisLine: { lineStyle: { color: '#E0E6ED' } },
axisLabel: { color: '#606266', fontSize: 11 }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#F2F6FC' } },
axisLabel: { color: '#606266', fontSize: 11 }
},
series: [
{
name: title,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: color, width: 2 },
itemStyle: { color: color },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: color + '40' },
{ offset: 1, color: color + '10' }
])
},
data: [0, 0, 0, 0, 0, 0]
}
]
});
return chart;
};
// 初始化所有图表
const initCharts = () => {
if (salesChartRef.value) {
salesChartInstance.value = initChart(salesChartRef.value, '销售', '#409EFF');
}
if (retailChartRef.value) {
retailChartInstance.value = initChart(retailChartRef.value, '零售', '#67C23A');
}
if (purchaseChartRef.value) {
purchaseChartInstance.value = initChart(purchaseChartRef.value, '采购', '#E6A23C');
}
};
// 加载数据(模拟接口调用)
const loadData = async () => {
// TODO: 调用接口获取真实数据
// const res = await getErpDashboard();
// 模拟数据更新
firstRowStats.value = [
{ label: '今日销售', value: 12580 },
{ label: '今日零售', value: 8560 },
{ label: '今日采购', value: 23400 },
{ label: '本月累计销售', value: 358600 },
{ label: '本月累计零售', value: 245800 },
{ label: '本月累计采购', value: 568900 },
];
secondRowStats.value = [
{ label: '昨日销售', value: 15230 },
{ label: '昨日零售', value: 9870 },
{ label: '昨日采购', value: 18900 },
{ label: '今年累计销售', value: 2856000 },
{ label: '今年累计零售', value: 1985000 },
{ label: '今年累计采购', value: 4568000 },
];
// 更新图表数据
updateCharts();
};
// 更新图表数据
const updateCharts = () => {
const salesData = [28000, 32000, 35000, 38000, 42000, 358600];
const retailData = [18000, 21000, 22000, 25000, 28000, 245800];
const purchaseData = [45000, 48000, 52000, 55000, 58000, 568900];
if (salesChartInstance.value) {
salesChartInstance.value.setOption({ series: [{ data: salesData }] });
}
if (retailChartInstance.value) {
retailChartInstance.value.setOption({ series: [{ data: retailData }] });
}
if (purchaseChartInstance.value) {
purchaseChartInstance.value.setOption({ series: [{ data: purchaseData }] });
}
};
// 自适应窗口大小
const handleResize = () => {
salesChartInstance.value?.resize();
retailChartInstance.value?.resize();
purchaseChartInstance.value?.resize();
};
onMounted(async () => {
initCharts();
await nextTick();
await loadData();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
salesChartInstance.value?.dispose();
retailChartInstance.value?.dispose();
purchaseChartInstance.value?.dispose();
});
</script>
<style lang="less" scoped>
.erp-dashboard {
background-color: #f5f7fa;
min-height: calc(100vh - 84px);
}
.stat-row {
.el-col {
margin-bottom: 0;
}
}
.stat-card {
background: #fff;
border-radius: 4px;
padding: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
.stat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.stat-label {
font-size: 14px;
color: #606266;
}
.stat-icon {
font-size: 14px;
color: #c0c4cc;
cursor: help;
}
}
.stat-value {
display: flex;
align-items: baseline;
.currency {
font-size: 14px;
color: #303133;
font-weight: 500;
margin-right: 4px;
}
.number {
font-size: 24px;
font-weight: 600;
color: #303133;
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
}
}
}
.chart-row {
.el-col {
margin-bottom: 0;
}
}
.chart-card {
background: #fff;
border-radius: 4px;
padding: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
height: 320px;
display: flex;
flex-direction: column;
.chart-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
.chart-container {
flex: 1;
min-height: 0;
}
}
</style>
+8
View File
@@ -0,0 +1,8 @@
<template>
<router-view />
</template>
<script setup></script>
<style lang="less" scoped></style>
@@ -0,0 +1,398 @@
<template>
<el-dialog v-model="visible" title="报销详情" width="980px" destroy-on-close>
<el-descriptions v-if="detail" :column="3" border>
<el-descriptions-item label="报销事由" :span="2">{{ detail.title }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="费用类型">{{ detail.expense_type || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请日期">{{ detail.apply_date || '-' }}</el-descriptions-item>
<el-descriptions-item label="合计金额">¥ {{ money(detail.total_amount) }}</el-descriptions-item>
<el-descriptions-item label="备注说明" :span="3">{{ detail.description || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="section-header">
<h4 class="detail-title">费用明细</h4>
<el-button v-if="isDraft" type="primary" size="small" @click="openAddItem">添加费用</el-button>
</div>
<el-table :data="detail?.items || []" border size="small">
<el-table-column prop="expense_type" label="费用类型" min-width="120" />
<el-table-column prop="expense_date" label="发生日期" width="120" />
<el-table-column prop="amount" label="金额" width="110">
<template #default="{ row }">¥ {{ money(row.amount) }}</template>
</el-table-column>
<el-table-column prop="receipt_flag" label="发票" width="70">
<template #default="{ row }">{{ row.receipt_flag ? '有' : '无' }}</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
<el-table-column label="操作" :width="isDraft ? 176 : 70" fixed="right">
<template #default="{ row }">
<div class="item-actions">
<el-button
v-if="row.receipt_url"
link
type="primary"
title="查看票据"
aria-label="查看票据"
@click="openReceiptPreview(row)"
>
<i class="fa-solid fa-eye"></i>
</el-button>
<template v-if="isDraft">
<el-upload
:show-file-list="false"
:auto-upload="false"
accept="image/*,.pdf"
:on-change="file => uploadInvoice(row, file)"
:disabled="uploadingItemId === row.id"
>
<el-button
link
type="success"
title="上传发票"
aria-label="上传发票"
>
<i
:class="uploadingItemId === row.id ? 'fa-solid fa-spinner fa-spin' : 'fa-solid fa-file-arrow-up'"
></i>
</el-button>
</el-upload>
<el-button link type="primary" title="编辑" aria-label="编辑" @click="openEditItem(row)">
<i class="fa-solid fa-pen-to-square"></i>
</el-button>
<el-button link type="danger" title="删除" aria-label="删除" @click="removeItem(row)">
<i class="fa-regular fa-trash-can"></i>
</el-button>
</template>
</div>
</template>
</el-table-column>
<template #empty><el-empty description="暂无费用明细" :image-size="60" /></template>
</el-table>
<el-dialog v-model="receiptPreviewVisible" title="查看票据" width="980px" append-to-body destroy-on-close>
<div v-if="receiptList.length" class="receipt-preview">
<aside class="receipt-list">
<button
v-for="receipt in receiptList"
:key="receipt.id"
type="button"
class="receipt-thumb"
:class="{ active: receipt.id === receiptPreviewItemId }"
@click="selectReceipt(receipt)"
>
<el-image
v-if="receipt.isImage"
:src="receipt.url"
fit="cover"
class="receipt-thumb-image"
lazy
/>
<i v-else class="fa-regular fa-file-pdf receipt-thumb-pdf"></i>
<span>{{ receipt.name }}</span>
</button>
</aside>
<main v-if="receiptPreviewUrl" class="receipt-viewer">
<el-image
v-if="isReceiptImage"
:src="receiptPreviewUrl"
:preview-src-list="[receiptPreviewUrl]"
fit="contain"
class="receipt-image"
preview-teleported
/>
<iframe v-else :src="receiptPreviewUrl" title="票据 PDF 预览" class="receipt-pdf"></iframe>
</main>
</div>
</el-dialog>
<h4 class="detail-title">审批记录</h4>
<el-timeline v-if="records.length">
<el-timeline-item
v-for="record in records"
:key="record.id"
:timestamp="formatDateTime(record.create_time)"
placement="top"
>
<strong>{{ actionLabel(record.action) }} · {{ record.approver_name || '-' }}</strong>
<div v-if="record.comment" class="record-comment">{{ record.comment }}</div>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无审批记录" :image-size="60" />
<ItemForm
v-model="itemVisible"
:reimbursement-id="detail?.id"
:editing-item-id="editingItemId"
:editing-item="editingItem"
:expense-type-options="expenseTypeOptions"
@saved="handleItemsSaved"
/>
</el-dialog>
</template>
<script setup>
import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { uploadFile } from '@/api/file'
import { deleteReimbursementItem, updateReimbursementItem } from '@/api/reimburse'
import ItemForm from './itemForm.vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
detail: { type: Object, default: null },
records: { type: Array, default: () => [] },
expenseTypeOptions: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:modelValue', 'changed'])
const visible = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value)
})
const isDraft = computed(() => props.detail?.status === 0)
const itemVisible = ref(false)
const uploadingItemId = ref(0)
const editingItemId = ref(0)
const editingItem = ref(null)
const receiptPreviewVisible = ref(false)
const receiptPreviewUrl = ref('')
const receiptPreviewType = ref('')
const receiptPreviewItemId = ref(0)
const receiptList = computed(() =>
(props.detail?.items || [])
.filter(item => item.receipt_url)
.map(item => {
const url = resolveReceiptUrl(item.receipt_url)
const isImage = /\.(jpe?g|png|gif|bmp|webp)(\?|#|$)/i.test(item.receipt_url || '')
return {
id: item.id,
url,
isImage,
name: item.expense_type || `票据 ${item.id}`
}
})
)
const isReceiptImage = computed(() => receiptPreviewType.value.startsWith('image/'))
const resolveReceiptUrl = url => {
if (!url) return ''
if (/^https?:\/\//i.test(url) || url.startsWith('blob:') || url.startsWith('data:')) return url
return `${window.location.origin}/${url.replace(/^\/+/, '')}`
}
const selectReceipt = receipt => {
receiptPreviewItemId.value = receipt.id
receiptPreviewUrl.value = receipt.url
receiptPreviewType.value = receipt.isImage ? 'image/*' : 'application/pdf'
}
const openReceiptPreview = row => {
const receipt = receiptList.value.find(item => item.id === row.id)
if (!receipt) return
selectReceipt(receipt)
receiptPreviewVisible.value = true
}
const openAddItem = () => {
editingItemId.value = 0
editingItem.value = null
itemVisible.value = true
}
const openEditItem = row => {
editingItemId.value = row.id
editingItem.value = row
itemVisible.value = true
}
const handleItemsSaved = reimbursementId => {
itemVisible.value = false
editingItemId.value = 0
editingItem.value = null
emit('changed', reimbursementId)
}
const uploadInvoice = async (row, file) => {
if (!file?.raw) return
const rawFile = file.raw
const isAllowed = rawFile.type.startsWith('image/') || rawFile.type === 'application/pdf'
if (!isAllowed) {
ElMessage.error('发票仅支持图片或 PDF 文件')
return
}
uploadingItemId.value = row.id
try {
const formData = new FormData()
formData.append('file', rawFile)
formData.append('cate', 'reimbursement-invoice')
const uploadResponse = await uploadFile(formData)
if (uploadResponse?.code !== 200 && uploadResponse?.code !== 201) {
throw new Error(uploadResponse?.msg || '发票上传失败')
}
await updateReimbursementItem(props.detail.id, row.id, {
expense_type: row.expense_type,
expense_date: row.expense_date,
amount: row.amount,
quantity: row.quantity || 1,
unit_price: row.unit_price || 0,
description: row.description || '',
receipt_flag: 1,
receipt_url: uploadResponse?.data?.url || uploadResponse?.url || ''
})
ElMessage.success('发票上传成功')
emit('changed', props.detail.id)
} catch (error) {
ElMessage.error(error?.message || '发票上传失败')
} finally {
uploadingItemId.value = 0
}
}
const removeItem = async row => {
try {
await ElMessageBox.confirm(`确认删除“${row.expense_type}”这条费用明细吗?`, '删除确认', { type: 'warning' })
await deleteReimbursementItem(props.detail.id, row.id)
ElMessage.success('费用明细已删除')
emit('changed', props.detail.id)
} catch (error) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(error?.message || '删除费用明细失败')
}
}
const statusOptions = [
{ value: 0, label: '草稿' },
{ value: 1, label: '审批中' },
{ value: 2, label: '已通过' },
{ value: 3, label: '已驳回' },
{ value: 4, label: '已撤回' },
{ value: 5, label: '已打款' }
]
const statusLabel = value => statusOptions.find(item => item.value === value)?.label || '未知'
const statusType = value => ({ 0: 'info', 1: 'warning', 2: 'success', 3: 'danger', 4: 'info', 5: 'success' })[value] || 'info'
const actionLabel = value => ({ 1: '提交申请', 2: '审批通过', 3: '审批驳回', 4: '转审', 5: '撤回' })[value] || '审批操作'
const money = value => Number(value || 0).toFixed(2)
const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-'
</script>
<style lang="less" scoped>
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 22px;
}
.detail-title {
margin: 0 0 12px;
color: #303133;
}
.record-comment {
margin-top: 6px;
color: #606266;
}
.receipt-preview {
display: flex;
gap: 16px;
min-height: 620px;
}
.receipt-list {
flex: 0 0 180px;
display: flex;
flex-direction: column;
gap: 10px;
padding-right: 12px;
border-right: 1px solid var(--el-border-color-light);
overflow-y: auto;
}
.receipt-thumb {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-height: 72px;
padding: 8px;
overflow: visible;
border: 1px solid var(--el-border-color);
border-radius: 4px;
background: var(--el-bg-color);
color: var(--el-text-color-regular);
cursor: pointer;
text-align: left;
&:hover,
&.active {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
span {
flex: 1;
overflow: visible;
word-break: break-all;
}
}
.receipt-thumb-image {
flex: 0 0 52px;
width: 52px;
height: 56px;
}
.receipt-thumb-pdf {
flex: 0 0 52px;
width: 52px;
color: var(--el-color-danger);
font-size: 34px;
text-align: center;
}
.receipt-viewer {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
}
.receipt-image {
display: block;
width: 100%;
max-width: 100%;
max-height: 620px;
cursor: zoom-in;
}
.receipt-pdf {
width: 100%;
height: 620px;
border: 0;
}
.item-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
:deep(.el-upload) {
display: inline-flex;
}
.el-button {
margin: 0;
padding: 4px 6px;
font-size: 15px;
}
}
</style>
@@ -0,0 +1,73 @@
<template>
<el-dialog v-model="visible" :title="title" width="720px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
<el-form-item label="申请日期" prop="apply_date">
<el-date-picker
v-model="form.apply_date"
type="date"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注说明">
<el-input v-model="form.description" type="textarea" :rows="4" maxlength="500" show-word-limit />
</el-form-item>
<el-alert
title="保存报销单后,可在详情中逐条添加报销事项。"
type="info"
:closable="false"
show-icon
/>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="save">创建</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
title: { type: String, default: '新建报销单' },
formData: { type: Object, required: true },
expenseTypeOptions: { type: Array, default: () => [] },
saving: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue', 'save'])
const visible = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value)
})
const today = () => new Date().toISOString().slice(0, 10)
const cloneForm = data => ({
apply_date: data?.apply_date || today(),
description: data?.description || ''
})
const form = reactive(cloneForm(props.formData))
const formRef = ref()
const formRules = {
apply_date: [{ required: true, message: '请选择申请日期', trigger: 'change' }]
}
watch(() => props.formData, value => {
Object.assign(form, cloneForm(value))
}, { deep: true })
const save = async () => {
if (!(await formRef.value.validate().catch(() => false))) return
emit('save', {
apply_date: form.apply_date,
description: form.description
})
}
</script>
@@ -0,0 +1,252 @@
<template>
<el-dialog
v-model="visible"
:title="editingItemId ? '编辑费用明细' : '批量添加费用明细'"
width="860px"
append-to-body
destroy-on-close
>
<div v-if="!editingItemId" class="batch-toolbar">
<span class="batch-tip">可同时填写多条费用明细保存后将一次性提交</span>
<el-button type="primary" plain size="small" @click="addRow">
<i class="fa-solid fa-plus"></i>
增加一行
</el-button>
</div>
<div class="item-list">
<el-card v-for="(item, index) in items" :key="item.key" shadow="never" class="item-card">
<template #header>
<div class="item-header">
<span>费用明细 {{ index + 1 }}</span>
<el-button
v-if="!editingItemId && items.length > 1"
link
type="danger"
title="删除此行"
aria-label="删除此行"
@click="removeRow(index)"
>
<i class="fa-regular fa-trash-can"></i>
</el-button>
</div>
</template>
<el-form :model="item" :rules="rules" label-width="80px" class="item-form">
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="费用类型" prop="expense_type">
<el-select v-model="item.expense_type" placeholder="请选择" style="width: 100%">
<el-option
v-for="option in expenseTypeOptions"
:key="option.id"
:label="option.name"
:value="option.name"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="发生日期" prop="expense_date">
<el-date-picker
v-model="item.expense_date"
type="date"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="金额" prop="amount">
<el-input-number
v-model="item.amount"
:min="0.01"
:precision="2"
:step="10"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="费用描述" style="margin-top:20px">
<el-input
v-model="item.description"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
</el-card>
</div>
<el-empty v-if="!items.length" description="请添加费用明细" :image-size="70" />
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" :disabled="!items.length" @click="submit">
{{ editingItemId ? '保存' : `保存 ${items.length} ` }}
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { addReimbursementItem, updateReimbursementItem } from '@/api/reimburse'
const props = defineProps({
modelValue: { type: Boolean, default: false },
reimbursementId: { type: [Number, String], default: 0 },
editingItemId: { type: [Number, String], default: 0 },
editingItem: { type: Object, default: null },
expenseTypeOptions: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:modelValue', 'saved'])
const visible = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value)
})
const saving = ref(false)
const items = ref([])
let rowKey = 0
const today = () => new Date().toISOString().slice(0, 10)
const createRow = (source = {}) => ({
key: ++rowKey,
expense_type: source.expense_type || '',
expense_date: source.expense_date || today(),
amount: Number(source.amount || 0),
description: source.description || '',
unit_price: Number(source.unit_price || 0),
receipt_url: source.receipt_url || ''
})
const rules = {
expense_type: [{ required: true, message: '请选择费用类型', trigger: 'change' }],
expense_date: [{ required: true, message: '请选择发生日期', trigger: 'change' }],
amount: [
{ required: true, message: '请输入费用金额', trigger: 'change' },
{ type: 'number', min: 0.01, message: '金额必须大于0', trigger: 'change' }
]
}
const resetItems = () => {
if (props.editingItemId && props.editingItem) {
items.value = [createRow(props.editingItem)]
} else {
items.value = [createRow()]
}
}
const addRow = () => items.value.push(createRow())
const removeRow = index => items.value.splice(index, 1)
const submit = async () => {
if (!items.value.length) return
const invalidIndex = items.value.findIndex(
item => !item.expense_type || !item.expense_date || Number(item.amount) <= 0
)
if (invalidIndex !== -1) {
ElMessage.warning(`请完善第 ${invalidIndex + 1} 条费用明细`)
return
}
saving.value = true
try {
if (props.editingItemId) {
const item = items.value[0]
await updateReimbursementItem(props.reimbursementId, props.editingItemId, {
expense_type: item.expense_type,
expense_date: item.expense_date,
amount: item.amount,
quantity: 1,
unit_price: item.unit_price || 0,
description: item.description || '',
receipt_flag: props.editingItem?.receipt_flag || 0,
receipt_url: item.receipt_url || ''
})
} else {
for (const item of items.value) {
await addReimbursementItem(props.reimbursementId, {
expense_type: item.expense_type,
expense_date: item.expense_date,
amount: item.amount,
quantity: 1,
unit_price: item.unit_price || 0,
description: item.description || '',
receipt_flag: 0,
receipt_url: ''
})
}
}
ElMessage.success(props.editingItemId ? '费用明细保存成功' : `成功添加 ${items.value.length} 条费用明细`)
visible.value = false
emit('saved', props.reimbursementId)
} catch (error) {
ElMessage.error(error?.message || '保存费用明细失败')
} finally {
saving.value = false
}
}
watch(
() => [props.modelValue, props.editingItemId, props.editingItem],
([isVisible]) => {
if (isVisible) resetItems()
},
{ immediate: true }
)
</script>
<style lang="less" scoped>
.batch-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.batch-tip {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.item-list {
max-height: 540px;
overflow-y: auto;
padding-right: 4px;
}
.item-card {
margin-bottom: 12px;
border: 1px solid var(--el-border-color-light);
&:last-child {
margin-bottom: 0;
}
}
.item-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
}
.item-form {
:deep(.el-form-item) {
margin-bottom: 14px;
}
:deep(.el-form-item:last-child) {
margin-bottom: 0;
}
}
</style>
@@ -0,0 +1,330 @@
<template>
<div class="reimburse-page">
<div class="page-header">
<div>
<h2>报销管理</h2>
<p>管理个人报销申请及审批进度</p>
</div>
<el-button type="primary" :icon="Plus" @click="openCreate">新建报销</el-button>
</div>
<div class="filter-card">
<el-form :inline="true" :model="filters" @submit.prevent>
<el-form-item label="关键词">
<el-input v-model="filters.keyword" clearable placeholder="请输入备注说明" @keyup.enter="loadList" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filters.status" clearable placeholder="全部状态" style="width: 150px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="loadList">查询</el-button>
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="table-card">
<el-table v-loading="loading" :data="list" stripe>
<el-table-column prop="apply_date" label="申请日期" width="120">
<template #default="{ row }">{{ formatDate(row.apply_date) }}</template>
</el-table-column>
<el-table-column prop="description" label="备注说明" min-width="220" show-overflow-tooltip>
<template #default="{ row }">{{ row.description || '-' }}</template>
</el-table-column>
<el-table-column prop="total_amount" label="报销金额" width="130">
<template #default="{ row }">¥ {{ money(row.total_amount) }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="110">
<template #default="{ row }">
<el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="175">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="245">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row.id)">详情</el-button>
<el-button v-if="row.status === 0" link type="primary" @click="openEdit(row.id)">编辑</el-button>
<el-button v-if="row.status === 0" link type="success" @click="handleSubmit(row)">提交</el-button>
<el-button v-if="row.status === 0" link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="暂无报销记录" /></template>
</el-table>
<div class="pagination">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@update:current-page="pagination.page = $event"
@update:page-size="pagination.pageSize = $event"
@current-change="loadList"
@size-change="loadList"
/>
</div>
</div>
<ReimburseEdit
v-model="formVisible"
:title="formTitle"
:form-data="form"
:saving="saving"
@save="saveForm"
/>
<ReimburseDetail
v-model="detailVisible"
:detail="detail"
:records="records"
:expense-type-options="expenseTypeOptions"
@changed="reloadDetail"
/>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
import ReimburseEdit from './components/edit.vue'
import ReimburseDetail from './components/detail.vue'
import {
createReimbursement,
deleteReimbursement,
getReimbursementDetail,
getReimbursementExpenseTypes,
getReimbursementList,
getReimbursementRecords,
submitReimbursement,
updateReimbursement
} from '@/api/reimburse'
const statusOptions = [
{ value: 0, label: '草稿' },
{ value: 1, label: '审批中' },
{ value: 2, label: '已通过' },
{ value: 3, label: '已驳回' },
{ value: 4, label: '已撤回' },
{ value: 5, label: '已打款' }
]
const filters = reactive({ keyword: '', status: undefined })
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const list = ref([])
const loading = ref(false)
const saving = ref(false)
const formVisible = ref(false)
const detailVisible = ref(false)
const editingId = ref(0)
const detail = ref(null)
const records = ref([])
const expenseTypeOptions = ref([])
const emptyForm = () => ({
apply_date: new Date().toISOString().slice(0, 10),
description: ''
})
const form = reactive(emptyForm())
const formTitle = computed(() => editingId.value ? '编辑报销单' : '新建报销单')
const responseData = res => res?.data?.data ?? res?.data ?? res ?? {}
const loadExpenseTypes = async () => {
try {
const data = responseData(await getReimbursementExpenseTypes())
expenseTypeOptions.value = Array.isArray(data) ? data : data.list || []
} catch (error) {
ElMessage.error(error?.message || '加载费用类型失败')
}
}
const loadList = async () => {
loading.value = true
try {
const data = responseData(await getReimbursementList({ ...filters, ...pagination }))
list.value = data.list || []
pagination.total = Number(data.total || 0)
} catch (error) {
ElMessage.error(error?.message || '加载报销列表失败')
} finally {
loading.value = false
}
}
const resetFilters = () => {
filters.keyword = ''
filters.status = undefined
pagination.page = 1
loadList()
}
const resetForm = () => Object.assign(form, emptyForm())
const openCreate = () => {
editingId.value = 0
resetForm()
formVisible.value = true
}
const openEdit = async id => {
try {
const data = responseData(await getReimbursementDetail(id))
editingId.value = id
Object.assign(form, {
apply_date: data.apply_date || new Date().toISOString().slice(0, 10),
description: data.description || ''
})
formVisible.value = true
} catch (error) {
ElMessage.error(error?.message || '加载报销详情失败')
}
}
const saveForm = async payload => {
saving.value = true
try {
if (editingId.value) await updateReimbursement(editingId.value, payload)
else await createReimbursement(payload)
ElMessage.success('保存成功')
formVisible.value = false
loadList()
} catch (error) {
ElMessage.error(error?.message || '保存失败')
} finally {
saving.value = false
}
}
const handleSubmit = async row => {
try {
await ElMessageBox.confirm('提交后将进入审批流程,确认提交吗?', '提示', { type: 'warning' })
await submitReimbursement(row.id)
ElMessage.success('提交成功')
loadList()
} catch (error) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(error?.message || '提交失败')
}
}
const handleDelete = async row => {
try {
await ElMessageBox.confirm('确认删除这条报销记录吗?', '删除确认', { type: 'warning' })
await deleteReimbursement(row.id)
ElMessage.success('删除成功')
loadList()
} catch (error) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(error?.message || '删除失败')
}
}
const reloadDetail = async id => {
try {
const [detailRes, recordsRes] = await Promise.all([
getReimbursementDetail(id),
getReimbursementRecords(id)
])
detail.value = responseData(detailRes)
records.value = responseData(recordsRes) || []
await loadList()
} catch (error) {
ElMessage.error(error?.message || '刷新详情失败')
}
}
const openDetail = async id => {
await reloadDetail(id)
detailVisible.value = true
}
const money = value => Number(value || 0).toFixed(2)
const formatDate = value => {
if (!value) return '-'
const text = String(value)
const match = text.match(/^(\d{4}-\d{2}-\d{2})/)
if (match) return match[1]
return text
}
const formatDateTime = value => value
? new Date(value).toLocaleString('zh-CN', { hour12: false })
: '-'
const statusLabel = value => statusOptions.find(item => item.value === value)?.label || '未知'
const statusType = value => ({
0: 'info',
1: 'warning',
2: 'success',
3: 'danger',
4: 'info',
5: 'success'
})[value] || 'info'
onMounted(() => {
loadExpenseTypes()
loadList()
})
</script>
<style lang="less" scoped>
.reimburse-page {
min-height: calc(100vh - 84px);
background: #f5f7fa;
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.page-header h2 {
margin: 0 0 8px;
color: #303133;
font-size: 22px;
}
.page-header p {
margin: 0;
color: #909399;
font-size: 13px;
}
.filter-card,
.table-card {
background: #fff;
border-radius: 6px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .05);
}
.filter-card {
margin-bottom: 16px;
}
.filter-card :deep(.el-form-item) {
margin-bottom: 0;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 18px;
}
@media (max-width: 768px) {
.reimburse-page {
padding: 12px;
}
.page-header {
align-items: flex-start;
gap: 12px;
}
.filter-card {
overflow-x: auto;
}
}
</style>
+701
View File
@@ -0,0 +1,701 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// BackendReimburseController 报销及审批接口。
type BackendReimburseController struct {
beego.Controller
}
type reimbursementItemPayload struct {
ID uint64 `json:"id"`
ExpenseType string `json:"expense_type"`
ExpenseDate string `json:"expense_date"`
Amount float64 `json:"amount"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
Description string `json:"description"`
ReceiptFlag int8 `json:"receipt_flag"`
ReceiptURL string `json:"receipt_url"`
}
type reimbursementPayload struct {
Title string `json:"title"`
DepartmentID *uint64 `json:"department_id"`
ExpenseType string `json:"expense_type"`
ApplyDate string `json:"apply_date"`
Description string `json:"description"`
Items []reimbursementItemPayload `json:"items"`
}
func (c *BackendReimburseController) claims() (*jwtutil.Claims, bool) {
auth := strings.TrimSpace(c.Ctx.Request.Header.Get("Authorization"))
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, false
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil || claims.UserType != "backend" || claims.TenantId <= 0 || claims.UserID <= 0 {
return nil, false
}
return claims, true
}
func (c *BackendReimburseController) reply(code int, msg string, data interface{}) {
if code != 200 {
c.Ctx.Output.SetStatus(code)
}
result := map[string]interface{}{"code": code, "msg": msg}
if data != nil {
result["data"] = data
}
c.Data["json"] = result
_ = c.ServeJSON()
}
func (c *BackendReimburseController) parsePayload() (reimbursementPayload, error) {
var payload reimbursementPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
return payload, err
}
err = json.Unmarshal(raw, &payload)
return payload, err
}
func parseReimburseDate(value string) (time.Time, error) {
return time.Parse("2006-01-02", strings.TrimSpace(value))
}
func (c *BackendReimburseController) getID() (uint64, bool) {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
return id, err == nil && id > 0
}
func (c *BackendReimburseController) findOwned(id uint64, claims *jwtutil.Claims) (*models.BackendReimbursement, error) {
row := &models.BackendReimbursement{}
err := models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("id", id).
Filter("tid", claims.TenantId).
Filter("user_id", claims.UserID).
Filter("is_deleted", 0).
One(row)
return row, err
}
func reimbursementItemDTO(item models.BackendReimbursementItem) map[string]interface{} {
return map[string]interface{}{
"id": item.ID, "reimbursement_id": item.ReimbursementID,
"expense_type": item.ExpenseType, "expense_date": item.ExpenseDate.Format("2006-01-02"),
"amount": item.Amount, "quantity": item.Quantity, "unit_price": item.UnitPrice,
"description": item.Description, "receipt_flag": item.ReceiptFlag, "receipt_url": item.ReceiptURL,
}
}
func reimbursementDTO(row models.BackendReimbursement, items []models.BackendReimbursementItem) map[string]interface{} {
result := map[string]interface{}{
"id": row.ID, "tid": row.Tid, "uid": row.UID, "user_id": row.UserID,
"department_id": row.DepartmentID, "title": row.Title, "total_amount": row.TotalAmount,
"status": row.Status, "expense_type": row.ExpenseType, "apply_date": row.ApplyDate.Format("2006-01-02"),
"description": row.Description, "current_approver_id": row.CurrentApproverID,
"current_step": row.CurrentStep, "create_time": row.CreateTime, "update_time": row.UpdateTime,
}
list := make([]map[string]interface{}, 0, len(items))
for _, item := range items {
list = append(list, reimbursementItemDTO(item))
}
result["items"] = list
return result
}
// List GET /backend/reimbursements
func (c *BackendReimburseController) List() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
keyword := strings.TrimSpace(c.GetString("keyword"))
status, statusErr := c.GetInt("status")
hasStatus := statusErr == nil
qs := models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("tid", claims.TenantId).Filter("user_id", claims.UserID).Filter("is_deleted", 0)
if keyword != "" {
qs = qs.Filter("description__icontains", keyword)
}
if hasStatus {
qs = qs.Filter("status", status)
}
total, err := qs.Count()
if err != nil {
c.reply(500, "查询失败", nil)
return
}
var rows []models.BackendReimbursement
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil && err != orm.ErrNoRows {
c.reply(500, "查询失败", nil)
return
}
if rows == nil {
rows = []models.BackendReimbursement{}
}
list := make([]map[string]interface{}, 0, len(rows))
for _, row := range rows {
list = append(list, map[string]interface{}{
"id": row.ID,
"tid": row.Tid,
"uid": row.UID,
"user_id": row.UserID,
"department_id": row.DepartmentID,
"title": row.Title,
"total_amount": row.TotalAmount,
"status": row.Status,
"expense_type": row.ExpenseType,
"apply_date": row.ApplyDate.Format("2006-01-02"),
"description": row.Description,
"current_approver_id": row.CurrentApproverID,
"current_step": row.CurrentStep,
"create_time": row.CreateTime,
"update_time": row.UpdateTime,
})
}
c.reply(200, "success", map[string]interface{}{"list": list, "total": total})
}
// Detail GET /backend/reimbursements/:id
func (c *BackendReimburseController) Detail() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
id, ok := c.getID()
if !ok {
c.reply(400, "无效ID", nil)
return
}
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
return
}
var items []models.BackendReimbursementItem
_, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("reimbursement_id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).
OrderBy("id").All(&items)
if err != nil && err != orm.ErrNoRows {
c.reply(500, "查询明细失败", nil)
return
}
c.reply(200, "success", reimbursementDTO(*row, items))
}
// Create POST /backend/reimbursements
func (c *BackendReimburseController) Create() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
payload, err := c.parsePayload()
if err != nil {
c.reply(400, "参数错误", nil)
return
}
applyDate, err := parseReimburseDate(payload.ApplyDate)
if err != nil {
c.reply(400, "申请日期格式错误", nil)
return
}
uid := uint64(claims.UserID)
row := &models.BackendReimbursement{
Tid: uint64(claims.TenantId), UID: &uid, UserID: uid, DepartmentID: payload.DepartmentID,
Title: "", TotalAmount: 0, Status: 0,
ExpenseType: strings.TrimSpace(payload.ExpenseType), ApplyDate: applyDate,
Description: nullableReimburseString(payload.Description),
}
if err := saveReimbursement(row); err != nil {
c.reply(500, "创建报销单失败", nil)
return
}
c.reply(200, "success", map[string]interface{}{"id": row.ID})
}
// Update POST /backend/reimbursements/:id
func (c *BackendReimburseController) Update() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
id, ok := c.getID()
if !ok {
c.reply(400, "无效ID", nil)
return
}
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
return
}
if row.Status != 0 {
c.reply(400, "只有草稿可以编辑", nil)
return
}
payload, err := c.parsePayload()
if err != nil {
c.reply(400, "参数错误", nil)
return
}
applyDate, err := parseReimburseDate(payload.ApplyDate)
if err != nil {
c.reply(400, "申请日期格式错误", nil)
return
}
row.ApplyDate, row.Description = applyDate, nullableReimburseString(payload.Description)
if err := saveReimbursement(row); err != nil {
c.reply(500, "更新报销单失败", nil)
return
}
c.reply(200, "success", map[string]interface{}{"id": row.ID})
}
// Delete DELETE /backend/reimbursements/:id
func (c *BackendReimburseController) Delete() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
id, ok := c.getID()
if !ok {
c.reply(400, "无效ID", nil)
return
}
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
return
}
if row.Status != 0 {
c.reply(400, "只有草稿可以删除", nil)
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
if err != nil {
c.reply(500, "删除失败", nil)
return
}
_, _ = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("reimbursement_id", id).Update(orm.Params{"is_deleted": 1, "delete_time": now})
c.reply(200, "success", nil)
}
func (c *BackendReimburseController) parseItemPayload() (reimbursementItemPayload, error) {
var payload reimbursementItemPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
return payload, err
}
err = json.Unmarshal(raw, &payload)
return payload, err
}
func (c *BackendReimburseController) getItemID() (uint64, bool) {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":item_id"), 10, 64)
return id, err == nil && id > 0
}
func (c *BackendReimburseController) findOwnedItem(id, reimbursementID uint64, claims *jwtutil.Claims) (*models.BackendReimbursementItem, error) {
item := &models.BackendReimbursementItem{}
err := models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("id", id).
Filter("reimbursement_id", reimbursementID).
Filter("tid", claims.TenantId).
Filter("uid", claims.UserID).
Filter("is_deleted", 0).
One(item)
return item, err
}
func validateReimbursementItem(item reimbursementItemPayload) (time.Time, error) {
if strings.TrimSpace(item.ExpenseType) == "" {
return time.Time{}, fmt.Errorf("费用类型不能为空")
}
date, err := parseReimburseDate(item.ExpenseDate)
if err != nil {
return time.Time{}, fmt.Errorf("费用日期格式错误")
}
if item.Amount <= 0 {
return time.Time{}, fmt.Errorf("费用金额必须大于0")
}
return date, nil
}
func (c *BackendReimburseController) ensureDraftReimbursement(id uint64, claims *jwtutil.Claims) (*models.BackendReimbursement, bool) {
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
return nil, false
}
if row.Status != 0 {
c.reply(400, "只有草稿可以维护费用明细", nil)
return nil, false
}
return row, true
}
func recalculateReimbursementTotal(id uint64) error {
var items []models.BackendReimbursementItem
_, err := models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("reimbursement_id", id).Filter("is_deleted", 0).All(&items)
if err != nil && err != orm.ErrNoRows {
return err
}
var total float64
for _, item := range items {
total += item.Amount
}
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("id", id).Update(orm.Params{
"total_amount": total,
"update_time": time.Now(),
})
return err
}
// AddItem POST /backend/reimbursements/:id/items
func (c *BackendReimburseController) AddItem() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
reimbursementID, ok := c.getID()
if !ok {
c.reply(400, "无效报销单ID", nil)
return
}
if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok {
return
}
payload, err := c.parseItemPayload()
if err != nil {
c.reply(400, "参数错误", nil)
return
}
date, err := validateReimbursementItem(payload)
if err != nil {
c.reply(400, err.Error(), nil)
return
}
uid := uint64(claims.UserID)
quantity := payload.Quantity
if quantity <= 0 {
quantity = 1
}
item := &models.BackendReimbursementItem{
Tid: uint64(claims.TenantId), UID: &uid, ReimbursementID: reimbursementID,
ExpenseType: strings.TrimSpace(payload.ExpenseType), ExpenseDate: date,
Amount: payload.Amount, Quantity: quantity, UnitPrice: payload.UnitPrice,
Description: strings.TrimSpace(payload.Description), ReceiptFlag: payload.ReceiptFlag,
ReceiptURL: strings.TrimSpace(payload.ReceiptURL),
}
if _, err := models.Orm.Insert(item); err != nil {
c.reply(500, "新增费用明细失败", nil)
return
}
if err := recalculateReimbursementTotal(reimbursementID); err != nil {
c.reply(500, "更新报销总额失败", nil)
return
}
c.reply(200, "success", reimbursementItemDTO(*item))
}
// UpdateItem POST /backend/reimbursements/:id/items/:item_id
func (c *BackendReimburseController) UpdateItem() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
reimbursementID, ok := c.getID()
if !ok {
c.reply(400, "无效报销单ID", nil)
return
}
itemID, ok := c.getItemID()
if !ok {
c.reply(400, "无效费用明细ID", nil)
return
}
if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok {
return
}
item, err := c.findOwnedItem(itemID, reimbursementID, claims)
if err != nil {
c.reply(404, "费用明细不存在", nil)
return
}
payload, err := c.parseItemPayload()
if err != nil {
c.reply(400, "参数错误", nil)
return
}
date, err := validateReimbursementItem(payload)
if err != nil {
c.reply(400, err.Error(), nil)
return
}
quantity := payload.Quantity
if quantity <= 0 {
quantity = 1
}
item.ExpenseType = strings.TrimSpace(payload.ExpenseType)
item.ExpenseDate = date
item.Amount = payload.Amount
item.Quantity = quantity
item.UnitPrice = payload.UnitPrice
item.Description = strings.TrimSpace(payload.Description)
item.ReceiptFlag = payload.ReceiptFlag
item.ReceiptURL = strings.TrimSpace(payload.ReceiptURL)
if _, err := models.Orm.Update(item); err != nil {
c.reply(500, "更新费用明细失败", nil)
return
}
if err := recalculateReimbursementTotal(reimbursementID); err != nil {
c.reply(500, "更新报销总额失败", nil)
return
}
c.reply(200, "success", reimbursementItemDTO(*item))
}
// DeleteItem DELETE /backend/reimbursements/:id/items/:item_id
func (c *BackendReimburseController) DeleteItem() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
reimbursementID, ok := c.getID()
if !ok {
c.reply(400, "无效报销单ID", nil)
return
}
itemID, ok := c.getItemID()
if !ok {
c.reply(400, "无效费用明细ID", nil)
return
}
if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok {
return
}
item, err := c.findOwnedItem(itemID, reimbursementID, claims)
if err != nil {
c.reply(404, "费用明细不存在", nil)
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)).
Filter("id", item.ID).Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
if err != nil {
c.reply(500, "删除费用明细失败", nil)
return
}
if err := recalculateReimbursementTotal(reimbursementID); err != nil {
c.reply(500, "更新报销总额失败", nil)
return
}
c.reply(200, "success", nil)
}
// Submit POST /backend/reimbursements/:id/submit
func (c *BackendReimburseController) Submit() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
id, ok := c.getID()
if !ok {
c.reply(400, "无效ID", nil)
return
}
row, err := c.findOwned(id, claims)
if err != nil {
c.reply(404, "报销单不存在", nil)
return
}
if row.Status != 0 {
c.reply(400, "当前状态不可提交", nil)
return
}
var flow models.BackendApprovalFlow
err = models.Orm.QueryTable(new(models.BackendApprovalFlow)).
Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0).
OrderBy("step_order").One(&flow)
if err != nil {
c.reply(400, "尚未配置审批流程", nil)
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{
"status": 1, "current_step": flow.StepOrder, "current_approver_id": 0, "update_time": now,
})
if err != nil {
c.reply(500, "提交失败", nil)
return
}
_, err = models.Orm.Insert(&models.BackendApprovalRecord{
Tid: uint64(claims.TenantId), UID: uint64Ptr(uint64(claims.UserID)), ReimbursementID: id,
ApproverID: uint64(claims.UserID), ApproverName: claims.Username, Action: 1,
Step: flow.StepOrder, PrevStep: 0,
})
if err != nil {
c.reply(500, "提交审批记录失败", nil)
return
}
c.reply(200, "success", nil)
}
// Records GET /backend/reimbursements/:id/records
func (c *BackendReimburseController) Records() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
id, ok := c.getID()
if !ok {
c.reply(400, "无效ID", nil)
return
}
if _, err := c.findOwned(id, claims); err != nil {
c.reply(404, "报销单不存在", nil)
return
}
var rows []models.BackendApprovalRecord
_, err := models.Orm.QueryTable(new(models.BackendApprovalRecord)).
Filter("tid", claims.TenantId).Filter("reimbursement_id", id).Filter("is_deleted", 0).
OrderBy("step", "id").All(&rows)
if err != nil && err != orm.ErrNoRows {
c.reply(500, "查询审批记录失败", nil)
return
}
if rows == nil {
rows = []models.BackendApprovalRecord{}
}
c.reply(200, "success", rows)
}
// Types GET /backend/reimbursements/types
func (c *BackendReimburseController) Types() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
var rows []models.BackendReimbursementExpenseType
_, err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)).
Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0).
OrderBy("sort_order", "id").All(&rows)
if err != nil && err != orm.ErrNoRows {
c.reply(500, "查询费用类型失败", nil)
return
}
if rows == nil {
rows = []models.BackendReimbursementExpenseType{}
}
c.reply(200, "success", rows)
}
// Flows GET /backend/reimbursements/flows
func (c *BackendReimburseController) Flows() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
var rows []models.BackendApprovalFlow
_, err := models.Orm.QueryTable(new(models.BackendApprovalFlow)).
Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0).
OrderBy("step_order").All(&rows)
if err != nil && err != orm.ErrNoRows {
c.reply(500, "查询审批流程失败", nil)
return
}
if rows == nil {
rows = []models.BackendApprovalFlow{}
}
c.reply(200, "success", rows)
}
func validateItems(items []reimbursementItemPayload) (float64, error) {
var total float64
for i, item := range items {
if strings.TrimSpace(item.ExpenseType) == "" {
return 0, fmt.Errorf("第%d条费用类型不能为空", i+1)
}
if _, err := parseReimburseDate(item.ExpenseDate); err != nil {
return 0, fmt.Errorf("第%d条费用日期格式错误", i+1)
}
if item.Amount <= 0 {
return 0, fmt.Errorf("第%d条费用金额必须大于0", i+1)
}
if item.Quantity <= 0 {
item.Quantity = 1
}
total += item.Amount
}
return total, nil
}
func saveReimbursement(row *models.BackendReimbursement) error {
if row.ID == 0 {
id, err := models.Orm.Insert(row)
if err != nil {
return err
}
row.ID = uint64(id)
return nil
}
_, err := models.Orm.Update(row)
return err
}
func nullableReimburseString(value string) *string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
return &value
}
func uint64Ptr(value uint64) *uint64 {
return &value
}
+91 -86
View File
@@ -1,86 +1,91 @@
package models
import (
"fmt"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
_ "github.com/go-sql-driver/mysql"
)
// Orm 全局 ORM 对象,供业务层和控制器使用
var Orm orm.Ormer
// Init 初始化模型层资源(数据库连接、模型注册等)。
func Init(_ string) {
// 从配置读取数据库连接信息
user, _ := beego.AppConfig.String("mysqluser")
pass, _ := beego.AppConfig.String("mysqlpass")
urls, _ := beego.AppConfig.String("mysqlurls")
dbname, _ := beego.AppConfig.String("mysqldb")
if user == "" || urls == "" || dbname == "" {
panic("数据库配置(mysqluser/mysqlurls/mysqldb) 未正确设置")
}
// 组装 DSNuser:pass@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, urls, dbname)
// 注册默认数据库
if err := orm.RegisterDataBase("default", "mysql", dsn); err != nil {
panic("注册数据库失败: " + err.Error())
}
// 注册模型
orm.RegisterModel(
new(SystemTenant),
new(SystemTenantUser),
new(BackendErpOrganization),
new(BackendErpEmployee),
new(BackendErpPosition),
new(SystemMenu),
new(AdminUser),
new(AdminRole),
new(SystemFile),
new(SystemFilesCategory),
new(SystemSMSTask),
new(SystemOperationLog),
new(SystemDomainPool),
new(SystemTenantDomain),
new(SystemModules),
new(StorageConfig),
new(TenantSiteSetting),
new(ComplaintCategory),
new(PlatformComplaint),
new(SystemSoftwareUpgrade),
new(PlatformCursorEquipment),
new(PlatformCursorActivationCode),
new(PlatformCursorEquipmentIpLog),
new(PlatformAccountPoolKiro),
new(PlatformAccountPoolWindsurf),
new(PlatformAccountPoolCursor),
new(PlatformAccountPoolCodex),
new(PlatformNotebook),
new(CmsArticleCategory),
new(CmsArticle),
new(SystemReminderList),
new(SystemNormalSetting),
new(PlatformNormalSetting),
new(BackendNormalSetting),
new(PlatformSchedule),
new(PlatformScheduleReminder),
new(PlatformScheduleReminderSendLog),
new(BackendNotebook),
new(BackendSchedule),
new(BackendScheduleReminder),
new(BackendScheduleReminderSendLog),
)
// 创建全局 Ormer
Orm = orm.NewOrm()
}
package models
import (
"fmt"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
_ "github.com/go-sql-driver/mysql"
)
// Orm 全局 ORM 对象,供业务层和控制器使用
var Orm orm.Ormer
// Init 初始化模型层资源(数据库连接、模型注册等)。
func Init(_ string) {
// 从配置读取数据库连接信息
user, _ := beego.AppConfig.String("mysqluser")
pass, _ := beego.AppConfig.String("mysqlpass")
urls, _ := beego.AppConfig.String("mysqlurls")
dbname, _ := beego.AppConfig.String("mysqldb")
if user == "" || urls == "" || dbname == "" {
panic("数据库配置(mysqluser/mysqlurls/mysqldb) 未正确设置")
}
// 组装 DSNuser:pass@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, urls, dbname)
// 注册默认数据库
if err := orm.RegisterDataBase("default", "mysql", dsn); err != nil {
panic("注册数据库失败: " + err.Error())
}
// 注册模型
orm.RegisterModel(
new(SystemTenant),
new(SystemTenantUser),
new(BackendErpOrganization),
new(BackendErpEmployee),
new(BackendErpPosition),
new(BackendApprovalFlow),
new(BackendApprovalRecord),
new(BackendReimbursement),
new(BackendReimbursementItem),
new(BackendReimbursementExpenseType),
new(SystemMenu),
new(AdminUser),
new(AdminRole),
new(SystemFile),
new(SystemFilesCategory),
new(SystemSMSTask),
new(SystemOperationLog),
new(SystemDomainPool),
new(SystemTenantDomain),
new(SystemModules),
new(StorageConfig),
new(TenantSiteSetting),
new(ComplaintCategory),
new(PlatformComplaint),
new(SystemSoftwareUpgrade),
new(PlatformCursorEquipment),
new(PlatformCursorActivationCode),
new(PlatformCursorEquipmentIpLog),
new(PlatformAccountPoolKiro),
new(PlatformAccountPoolWindsurf),
new(PlatformAccountPoolCursor),
new(PlatformAccountPoolCodex),
new(PlatformNotebook),
new(CmsArticleCategory),
new(CmsArticle),
new(SystemReminderList),
new(SystemNormalSetting),
new(PlatformNormalSetting),
new(BackendNormalSetting),
new(PlatformSchedule),
new(PlatformScheduleReminder),
new(PlatformScheduleReminderSendLog),
new(BackendNotebook),
new(BackendSchedule),
new(BackendScheduleReminder),
new(BackendScheduleReminderSendLog),
)
// 创建全局 Ormer
Orm = orm.NewOrm()
}
+113
View File
@@ -0,0 +1,113 @@
package models
import "time"
// BackendApprovalFlow 审批流程配置表 yz_backend_approval_flows
type BackendApprovalFlow struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid)" json:"tid"`
UID *uint64 `orm:"column(uid);null" json:"uid"`
FlowName string `orm:"column(flow_name);size(100)" json:"flow_name"`
StepOrder int8 `orm:"column(step_order);default(0)" json:"step_order"`
ApproverRole string `orm:"column(approver_role);size(50)" json:"approver_role"`
ApproverRule string `orm:"column(approver_rule);size(200)" json:"approver_rule"`
TimeoutHours int `orm:"column(timeout_hours);default(48)" json:"timeout_hours"`
IsActive int8 `orm:"column(is_active);default(1)" json:"is_active"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
}
func (m *BackendApprovalFlow) TableName() string {
return "yz_backend_approval_flows"
}
// BackendApprovalRecord 审批记录表 yz_backend_approval_records
type BackendApprovalRecord struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid)" json:"tid"`
UID *uint64 `orm:"column(uid);null" json:"uid"`
ReimbursementID uint64 `orm:"column(reimbursement_id)" json:"reimbursement_id"`
ApproverID uint64 `orm:"column(approver_id)" json:"approver_id"`
ApproverName string `orm:"column(approver_name);size(100)" json:"approver_name"`
Action int8 `orm:"column(action);default(0)" json:"action"`
Comment *string `orm:"column(comment);null;type(text)" json:"comment"`
Step int8 `orm:"column(step);default(0)" json:"step"`
PrevStep int8 `orm:"column(prev_step);default(0)" json:"prev_step"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
}
func (m *BackendApprovalRecord) TableName() string {
return "yz_backend_approval_records"
}
// BackendReimbursement 报销单主表 yz_backend_reimbursements
type BackendReimbursement struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid)" json:"tid"`
UID *uint64 `orm:"column(uid);null" json:"uid"`
UserID uint64 `orm:"column(user_id)" json:"user_id"`
DepartmentID *uint64 `orm:"column(department_id);null" json:"department_id"`
Title string `orm:"column(title);size(200)" json:"title"`
TotalAmount float64 `orm:"column(total_amount);type(decimal);digits(10);decimals(2)" json:"total_amount"`
Status int8 `orm:"column(status);default(0)" json:"status"`
ExpenseType string `orm:"column(expense_type);size(50)" json:"expense_type"`
ApplyDate time.Time `orm:"column(apply_date);type(date)" json:"apply_date"`
Description *string `orm:"column(description);null;type(text)" json:"description"`
CurrentApproverID uint64 `orm:"column(current_approver_id);default(0)" json:"current_approver_id"`
CurrentStep int8 `orm:"column(current_step);default(0)" json:"current_step"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
}
func (m *BackendReimbursement) TableName() string {
return "yz_backend_reimbursements"
}
// BackendReimbursementItem 报销费用明细表 yz_backend_reimbursement_items
type BackendReimbursementItem struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid)" json:"tid"`
UID *uint64 `orm:"column(uid);null" json:"uid"`
ReimbursementID uint64 `orm:"column(reimbursement_id)" json:"reimbursement_id"`
ExpenseType string `orm:"column(expense_type);size(50)" json:"expense_type"`
ExpenseDate time.Time `orm:"column(expense_date);type(date)" json:"expense_date"`
Amount float64 `orm:"column(amount);type(decimal);digits(10);decimals(2)" json:"amount"`
Quantity int `orm:"column(quantity);default(1)" json:"quantity"`
UnitPrice float64 `orm:"column(unit_price);type(decimal);digits(10);decimals(2)" json:"unit_price"`
Description string `orm:"column(description);size(200)" json:"description"`
ReceiptFlag int8 `orm:"column(receipt_flag);default(0)" json:"receipt_flag"`
ReceiptURL string `orm:"column(receipt_url);size(500)" json:"receipt_url"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
}
func (m *BackendReimbursementItem) TableName() string {
return "yz_backend_reimbursement_items"
}
// BackendReimbursementExpenseType 报销费用类型表 yz_backend_reimbursement_expense_types
type BackendReimbursementExpenseType struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid)" json:"tid"`
UID *uint64 `orm:"column(uid);null" json:"uid"`
Name string `orm:"column(name);size(50)" json:"name"`
SortOrder int `orm:"column(sort_order);default(0)" json:"sort_order"`
IsActive int8 `orm:"column(is_active);default(1)" json:"is_active"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
}
func (m *BackendReimbursementExpenseType) TableName() string {
return "yz_backend_reimbursement_expense_types"
}
+172 -162
View File
@@ -1,162 +1,172 @@
package backend
import (
"server/controllers"
beego "github.com/beego/beego/v2/server/web"
)
// Register 注册租户端(backend)路由。
// 该端不包含平台菜单配置接口。
func Register() {
RegisterAuthRoutes()
}
// RegisterAuthRoutes 注册 backend 认证相关路由。
func RegisterAuthRoutes() {
// 登录、注册与找回密码相关
beego.Router("/backend/login", &controllers.BackendAuthController{}, "post:LoginBackend")
beego.Router("/backend/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode")
beego.Router("/backend/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms")
beego.Router("/backend/logout", &controllers.BackendAuthController{}, "post:Logout")
beego.Router("/backend/register", &controllers.BackendAuthController{}, "post:Register")
beego.Router("/backend/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode")
beego.Router("/backend/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword")
beego.Router("/backend/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode")
// 极验与登录验证配置
beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos")
beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos")
beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify")
// 菜单接口
beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu")
beego.Router("/backend/allmenu", &controllers.BackendMenuController{}, "get:GetAllBackendMenus")
// 操作日志(yz_system_operation_log
beego.Router("/backend/operationLogs", &controllers.BackendOperationLogController{}, "get:List")
beego.Router("/backend/operationLogs/statistics", &controllers.BackendOperationLogController{}, "get:Statistics")
beego.Router("/backend/operationLogs/:id", &controllers.BackendOperationLogController{}, "get:Detail;delete:Delete")
beego.Router("/backend/operationLogs/batchDelete", &controllers.BackendOperationLogController{}, "post:BatchDelete")
// 租户站点设置
beego.Router("/backend/normalInfos", &controllers.BackendSiteSettingsController{}, "get:GetNormalInfos")
beego.Router("/backend/saveNormalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveNormalInfos")
beego.Router("/backend/legalInfos", &controllers.BackendSiteSettingsController{}, "get:GetLegalInfos")
beego.Router("/backend/saveLegalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveLegalInfos")
beego.Router("/backend/companyInfos", &controllers.BackendSiteSettingsController{}, "get:GetCompanyInfos")
beego.Router("/backend/saveCompanyInfos", &controllers.BackendSiteSettingsController{}, "post:SaveCompanyInfos")
beego.Router("/backend/companySeo", &controllers.BackendSiteSettingsController{}, "get:GetCompanySeo")
beego.Router("/backend/saveCompanySeo", &controllers.BackendSiteSettingsController{}, "post:SaveCompanySeo")
beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos")
beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos")
// 站内信(yz_system_reminderlist
beego.Router("/backend/sitereminder/myList", &controllers.BackendSiteReminderController{}, "get:GetMyList")
beego.Router("/backend/sitereminder/read", &controllers.BackendSiteReminderController{}, "post:MarkRead")
beego.Router("/backend/sitereminder/readall", &controllers.BackendSiteReminderController{}, "post:MarkAllRead")
beego.Router("/backend/sitereminder/delete", &controllers.BackendSiteReminderController{}, "post:Delete")
// 文件管理(yz_system_files / yz_system_files_category
beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate")
beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles")
beego.Router("/backend/catefiles/:id", &controllers.BackendFileController{}, "get:GetCateFiles")
beego.Router("/backend/file/:id", &controllers.BackendFileController{}, "get:GetFileByID")
beego.Router("/backend/deletefilepermanently/:id", &controllers.BackendFileController{}, "delete:DeleteFilePermanently")
beego.Router("/backend/uploadfile", &controllers.BackendFileController{}, "post:UploadFile")
beego.Router("/backend/uploadfiles", &controllers.BackendFileController{}, "post:UploadFile")
beego.Router("/backend/updatefile/:id", &controllers.BackendFileController{}, "post:UpdateFile")
beego.Router("/backend/deletefile/:id", &controllers.BackendFileController{}, "delete:DeleteFile")
beego.Router("/backend/movefile/:id", &controllers.BackendFileController{}, "get:MoveFile")
beego.Router("/backend/createfilecate", &controllers.BackendFileController{}, "post:CreateFileCate")
beego.Router("/backend/renamefilecate/:id", &controllers.BackendFileController{}, "post:RenameFileCate")
beego.Router("/backend/deletefilecate/:id", &controllers.BackendFileController{}, "delete:DeleteFileCate")
beego.Router("/backend/uploadavatar", &controllers.BackendFileController{}, "post:UploadAvatar")
beego.Router("/backend/uploadavatar/:id", &controllers.BackendFileController{}, "post:UpdateAvatar")
beego.Router("/backend/batchdeletefiles", &controllers.BackendFileController{}, "post:BatchDeleteFiles")
beego.Router("/backend/batchDeleteFilesPermanently", &controllers.BackendFileController{}, "post:BatchDeleteFilesPermanently")
beego.Router("/backend/batchMoveFiles", &controllers.BackendFileController{}, "post:BatchMoveFiles")
// 模块接口
beego.Router("/backend/modules/getTenantList", &controllers.BackendModulesController{}, "get:GetTenantList")
// 用户接口
beego.Router("/backend/getTenantUsers/:tid", &controllers.BackendAdminUserController{}, "get:GetTenantUsers")
beego.Router("/backend/getAllUsers", &controllers.BackendAdminUserController{}, "get:GetAllUsers")
beego.Router("/backend/getUserInfo/:id", &controllers.BackendAdminUserController{}, "get:GetUserInfo")
beego.Router("/backend/addUser", &controllers.BackendAdminUserController{}, "post:AddUser")
beego.Router("/backend/editUser/:id", &controllers.BackendAdminUserController{}, "post:EditUser")
beego.Router("/backend/deleteUser/:id", &controllers.BackendAdminUserController{}, "delete:DeleteUser")
beego.Router("/backend/changePassword", &controllers.BackendAdminUserController{}, "post:ChangePassword")
// ERP 接口
beego.Router("/backend/erp/getOrganization", &controllers.BackendErpController{}, "get:GetOrganization")
beego.Router("/backend/erp/getOrganizationDetail/:id", &controllers.BackendErpController{}, "get:GetOrganizationDetail")
beego.Router("/backend/erp/createOrganization", &controllers.BackendErpController{}, "post:CreateOrganization")
beego.Router("/backend/erp/editOrganization/:id", &controllers.BackendErpController{}, "post:EditOrganization")
beego.Router("/backend/erp/deleteOrganization/:id", &controllers.BackendErpController{}, "delete:DeleteOrganization")
beego.Router("/backend/erp/getCompanys", &controllers.BackendErpController{}, "get:GetCompanys")
beego.Router("/backend/erp/getDepartments", &controllers.BackendErpController{}, "get:GetDepartments")
beego.Router("/backend/erp/getEmployee", &controllers.BackendErpController{}, "get:GetEmployee")
beego.Router("/backend/erp/getEmployeeDetail/:id", &controllers.BackendErpController{}, "get:GetEmployeeDetail")
beego.Router("/backend/erp/createEmployee", &controllers.BackendErpController{}, "post:CreateEmployee")
beego.Router("/backend/erp/editEmployee/:id", &controllers.BackendErpController{}, "post:EditEmployee")
beego.Router("/backend/erp/deleteEmployee/:id", &controllers.BackendErpController{}, "delete:DeleteEmployee")
beego.Router("/backend/erp/getPosition", &controllers.BackendErpController{}, "get:GetPosition")
beego.Router("/backend/erp/getPositionDetail/:id", &controllers.BackendErpController{}, "get:GetPositionDetail")
beego.Router("/backend/erp/createPosition", &controllers.BackendErpController{}, "post:CreatePosition")
beego.Router("/backend/erp/editPosition/:id", &controllers.BackendErpController{}, "post:EditPosition")
beego.Router("/backend/erp/deletePosition/:id", &controllers.BackendErpController{}, "delete:DeletePosition")
// 文章管理
beego.Router("/backend/articlesList", &controllers.BackendArticleController{}, "get:List")
beego.Router("/backend/contentstats", &controllers.BackendArticleController{}, "get:ContentStats")
beego.Router("/backend/allarticles", &controllers.BackendArticleController{}, "get:ListAll")
beego.Router("/backend/articles/:id", &controllers.BackendArticleController{}, "get:Detail")
beego.Router("/backend/createarticle", &controllers.BackendArticleController{}, "post:Create")
beego.Router("/backend/editarticle/:id", &controllers.BackendArticleController{}, "post:Update")
beego.Router("/backend/deletearticle/:id", &controllers.BackendArticleController{}, "delete:Delete")
beego.Router("/backend/publisharticle/:id", &controllers.BackendArticleController{}, "post:Publish")
beego.Router("/backend/unPublisharticle/:id", &controllers.BackendArticleController{}, "post:Unpublish")
beego.Router("/backend/articleRecommend/:id", &controllers.BackendArticleController{}, "post:Recommend")
beego.Router("/backend/unArticleRecommend/:id", &controllers.BackendArticleController{}, "post:Unrecommend")
beego.Router("/backend/articleTop/:id", &controllers.BackendArticleController{}, "post:Top")
beego.Router("/backend/unArticleTop/:id", &controllers.BackendArticleController{}, "post:Untop")
beego.Router("/backend/categories", &controllers.BackendArticleCategoryController{}, "get:List")
beego.Router("/backend/allcategories", &controllers.BackendArticleCategoryController{}, "get:ListAll")
beego.Router("/backend/categories/:id", &controllers.BackendArticleCategoryController{}, "get:Detail;delete:Delete")
beego.Router("/backend/createCategory", &controllers.BackendArticleCategoryController{}, "post:Create")
beego.Router("/backend/editCategory/:id", &controllers.BackendArticleCategoryController{}, "post:Update")
beego.Router("/backend/categories/:id/status", &controllers.BackendArticleCategoryController{}, "patch:UpdateStatus")
// 域名管理(主域名池 / 租户域名)
beego.Router("/backend/domain/pool/index", &controllers.BackendDomainPoolController{}, "get:Index")
beego.Router("/backend/domain/pool/getEnabledDomains", &controllers.BackendDomainPoolController{}, "get:GetEnabledDomains")
beego.Router("/backend/domain/pool/create", &controllers.BackendDomainPoolController{}, "post:Create")
beego.Router("/backend/domain/pool/update", &controllers.BackendDomainPoolController{}, "post:Update")
beego.Router("/backend/domain/pool/delete/:id", &controllers.BackendDomainPoolController{}, "delete:Delete")
beego.Router("/backend/domain/pool/toggleStatus", &controllers.BackendDomainPoolController{}, "post:ToggleStatus")
beego.Router("/backend/domain/tenant/index", &controllers.BackendTenantDomainController{}, "get:Index")
beego.Router("/backend/domain/tenant/myDomains", &controllers.BackendTenantDomainController{}, "get:MyDomains")
beego.Router("/backend/domain/tenant/apply", &controllers.BackendTenantDomainController{}, "post:Apply")
beego.Router("/backend/domain/tenant/audit", &controllers.BackendTenantDomainController{}, "post:Audit")
beego.Router("/backend/domain/tenant/toggleStatus", &controllers.BackendTenantDomainController{}, "post:ToggleStatus")
beego.Router("/backend/domain/tenant/delete/:id", &controllers.BackendTenantDomainController{}, "delete:Delete")
// 记事本管理
beego.Router("/backend/notebook/list", &controllers.BackendNotebookController{}, "get:List")
beego.Router("/backend/notebook/detail/:id", &controllers.BackendNotebookController{}, "get:Detail")
beego.Router("/backend/notebook/create", &controllers.BackendNotebookController{}, "post:Create")
beego.Router("/backend/notebook/update/:id", &controllers.BackendNotebookController{}, "post:Update")
beego.Router("/backend/notebook/delete/:id", &controllers.BackendNotebookController{}, "delete:Delete")
// 日程提醒管理
beego.Router("/backend/reminder/list", &controllers.BackendReminderController{}, "get:GetReminderList")
beego.Router("/backend/reminder/test", &controllers.BackendReminderController{}, "post:TestReminder")
beego.Router("/backend/reminder/:id", &controllers.BackendReminderController{}, "get:GetReminderDetail;put:UpdateReminder;delete:DeleteReminder")
beego.Router("/backend/reminder", &controllers.BackendReminderController{}, "post:CreateReminder")
beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder")
beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder")
}
package backend
import (
"server/controllers"
beego "github.com/beego/beego/v2/server/web"
)
// Register 注册租户端(backend)路由。
// 该端不包含平台菜单配置接口。
func Register() {
RegisterAuthRoutes()
}
// RegisterAuthRoutes 注册 backend 认证相关路由。
func RegisterAuthRoutes() {
// 登录、注册与找回密码相关
beego.Router("/backend/login", &controllers.BackendAuthController{}, "post:LoginBackend")
beego.Router("/backend/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode")
beego.Router("/backend/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms")
beego.Router("/backend/logout", &controllers.BackendAuthController{}, "post:Logout")
beego.Router("/backend/register", &controllers.BackendAuthController{}, "post:Register")
beego.Router("/backend/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode")
beego.Router("/backend/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword")
beego.Router("/backend/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode")
// 极验与登录验证配置
beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos")
beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos")
beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify")
// 菜单接口
beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu")
beego.Router("/backend/allmenu", &controllers.BackendMenuController{}, "get:GetAllBackendMenus")
// 操作日志(yz_system_operation_log
beego.Router("/backend/operationLogs", &controllers.BackendOperationLogController{}, "get:List")
beego.Router("/backend/operationLogs/statistics", &controllers.BackendOperationLogController{}, "get:Statistics")
beego.Router("/backend/operationLogs/:id", &controllers.BackendOperationLogController{}, "get:Detail;delete:Delete")
beego.Router("/backend/operationLogs/batchDelete", &controllers.BackendOperationLogController{}, "post:BatchDelete")
// 租户站点设置
beego.Router("/backend/normalInfos", &controllers.BackendSiteSettingsController{}, "get:GetNormalInfos")
beego.Router("/backend/saveNormalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveNormalInfos")
beego.Router("/backend/legalInfos", &controllers.BackendSiteSettingsController{}, "get:GetLegalInfos")
beego.Router("/backend/saveLegalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveLegalInfos")
beego.Router("/backend/companyInfos", &controllers.BackendSiteSettingsController{}, "get:GetCompanyInfos")
beego.Router("/backend/saveCompanyInfos", &controllers.BackendSiteSettingsController{}, "post:SaveCompanyInfos")
beego.Router("/backend/companySeo", &controllers.BackendSiteSettingsController{}, "get:GetCompanySeo")
beego.Router("/backend/saveCompanySeo", &controllers.BackendSiteSettingsController{}, "post:SaveCompanySeo")
beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos")
beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos")
// 站内信(yz_system_reminderlist
beego.Router("/backend/sitereminder/myList", &controllers.BackendSiteReminderController{}, "get:GetMyList")
beego.Router("/backend/sitereminder/read", &controllers.BackendSiteReminderController{}, "post:MarkRead")
beego.Router("/backend/sitereminder/readall", &controllers.BackendSiteReminderController{}, "post:MarkAllRead")
beego.Router("/backend/sitereminder/delete", &controllers.BackendSiteReminderController{}, "post:Delete")
// 文件管理(yz_system_files / yz_system_files_category
beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate")
beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles")
beego.Router("/backend/catefiles/:id", &controllers.BackendFileController{}, "get:GetCateFiles")
beego.Router("/backend/file/:id", &controllers.BackendFileController{}, "get:GetFileByID")
beego.Router("/backend/deletefilepermanently/:id", &controllers.BackendFileController{}, "delete:DeleteFilePermanently")
beego.Router("/backend/uploadfile", &controllers.BackendFileController{}, "post:UploadFile")
beego.Router("/backend/uploadfiles", &controllers.BackendFileController{}, "post:UploadFile")
beego.Router("/backend/updatefile/:id", &controllers.BackendFileController{}, "post:UpdateFile")
beego.Router("/backend/deletefile/:id", &controllers.BackendFileController{}, "delete:DeleteFile")
beego.Router("/backend/movefile/:id", &controllers.BackendFileController{}, "get:MoveFile")
beego.Router("/backend/createfilecate", &controllers.BackendFileController{}, "post:CreateFileCate")
beego.Router("/backend/renamefilecate/:id", &controllers.BackendFileController{}, "post:RenameFileCate")
beego.Router("/backend/deletefilecate/:id", &controllers.BackendFileController{}, "delete:DeleteFileCate")
beego.Router("/backend/uploadavatar", &controllers.BackendFileController{}, "post:UploadAvatar")
beego.Router("/backend/uploadavatar/:id", &controllers.BackendFileController{}, "post:UpdateAvatar")
beego.Router("/backend/batchdeletefiles", &controllers.BackendFileController{}, "post:BatchDeleteFiles")
beego.Router("/backend/batchDeleteFilesPermanently", &controllers.BackendFileController{}, "post:BatchDeleteFilesPermanently")
beego.Router("/backend/batchMoveFiles", &controllers.BackendFileController{}, "post:BatchMoveFiles")
// 模块接口
beego.Router("/backend/modules/getTenantList", &controllers.BackendModulesController{}, "get:GetTenantList")
// 用户接口
beego.Router("/backend/getTenantUsers/:tid", &controllers.BackendAdminUserController{}, "get:GetTenantUsers")
beego.Router("/backend/getAllUsers", &controllers.BackendAdminUserController{}, "get:GetAllUsers")
beego.Router("/backend/getUserInfo/:id", &controllers.BackendAdminUserController{}, "get:GetUserInfo")
beego.Router("/backend/addUser", &controllers.BackendAdminUserController{}, "post:AddUser")
beego.Router("/backend/editUser/:id", &controllers.BackendAdminUserController{}, "post:EditUser")
beego.Router("/backend/deleteUser/:id", &controllers.BackendAdminUserController{}, "delete:DeleteUser")
beego.Router("/backend/changePassword", &controllers.BackendAdminUserController{}, "post:ChangePassword")
// ERP 接口
beego.Router("/backend/erp/getOrganization", &controllers.BackendErpController{}, "get:GetOrganization")
beego.Router("/backend/erp/getOrganizationDetail/:id", &controllers.BackendErpController{}, "get:GetOrganizationDetail")
beego.Router("/backend/erp/createOrganization", &controllers.BackendErpController{}, "post:CreateOrganization")
beego.Router("/backend/erp/editOrganization/:id", &controllers.BackendErpController{}, "post:EditOrganization")
beego.Router("/backend/erp/deleteOrganization/:id", &controllers.BackendErpController{}, "delete:DeleteOrganization")
beego.Router("/backend/erp/getCompanys", &controllers.BackendErpController{}, "get:GetCompanys")
beego.Router("/backend/erp/getDepartments", &controllers.BackendErpController{}, "get:GetDepartments")
beego.Router("/backend/erp/getEmployee", &controllers.BackendErpController{}, "get:GetEmployee")
beego.Router("/backend/erp/getEmployeeDetail/:id", &controllers.BackendErpController{}, "get:GetEmployeeDetail")
beego.Router("/backend/erp/createEmployee", &controllers.BackendErpController{}, "post:CreateEmployee")
beego.Router("/backend/erp/editEmployee/:id", &controllers.BackendErpController{}, "post:EditEmployee")
beego.Router("/backend/erp/deleteEmployee/:id", &controllers.BackendErpController{}, "delete:DeleteEmployee")
beego.Router("/backend/erp/getPosition", &controllers.BackendErpController{}, "get:GetPosition")
beego.Router("/backend/erp/getPositionDetail/:id", &controllers.BackendErpController{}, "get:GetPositionDetail")
beego.Router("/backend/erp/createPosition", &controllers.BackendErpController{}, "post:CreatePosition")
beego.Router("/backend/erp/editPosition/:id", &controllers.BackendErpController{}, "post:EditPosition")
beego.Router("/backend/erp/deletePosition/:id", &controllers.BackendErpController{}, "delete:DeletePosition")
// 文章管理
beego.Router("/backend/articlesList", &controllers.BackendArticleController{}, "get:List")
beego.Router("/backend/contentstats", &controllers.BackendArticleController{}, "get:ContentStats")
beego.Router("/backend/allarticles", &controllers.BackendArticleController{}, "get:ListAll")
beego.Router("/backend/articles/:id", &controllers.BackendArticleController{}, "get:Detail")
beego.Router("/backend/createarticle", &controllers.BackendArticleController{}, "post:Create")
beego.Router("/backend/editarticle/:id", &controllers.BackendArticleController{}, "post:Update")
beego.Router("/backend/deletearticle/:id", &controllers.BackendArticleController{}, "delete:Delete")
beego.Router("/backend/publisharticle/:id", &controllers.BackendArticleController{}, "post:Publish")
beego.Router("/backend/unPublisharticle/:id", &controllers.BackendArticleController{}, "post:Unpublish")
beego.Router("/backend/articleRecommend/:id", &controllers.BackendArticleController{}, "post:Recommend")
beego.Router("/backend/unArticleRecommend/:id", &controllers.BackendArticleController{}, "post:Unrecommend")
beego.Router("/backend/articleTop/:id", &controllers.BackendArticleController{}, "post:Top")
beego.Router("/backend/unArticleTop/:id", &controllers.BackendArticleController{}, "post:Untop")
beego.Router("/backend/categories", &controllers.BackendArticleCategoryController{}, "get:List")
beego.Router("/backend/allcategories", &controllers.BackendArticleCategoryController{}, "get:ListAll")
beego.Router("/backend/categories/:id", &controllers.BackendArticleCategoryController{}, "get:Detail;delete:Delete")
beego.Router("/backend/createCategory", &controllers.BackendArticleCategoryController{}, "post:Create")
beego.Router("/backend/editCategory/:id", &controllers.BackendArticleCategoryController{}, "post:Update")
beego.Router("/backend/categories/:id/status", &controllers.BackendArticleCategoryController{}, "patch:UpdateStatus")
// 域名管理(主域名池 / 租户域名)
beego.Router("/backend/domain/pool/index", &controllers.BackendDomainPoolController{}, "get:Index")
beego.Router("/backend/domain/pool/getEnabledDomains", &controllers.BackendDomainPoolController{}, "get:GetEnabledDomains")
beego.Router("/backend/domain/pool/create", &controllers.BackendDomainPoolController{}, "post:Create")
beego.Router("/backend/domain/pool/update", &controllers.BackendDomainPoolController{}, "post:Update")
beego.Router("/backend/domain/pool/delete/:id", &controllers.BackendDomainPoolController{}, "delete:Delete")
beego.Router("/backend/domain/pool/toggleStatus", &controllers.BackendDomainPoolController{}, "post:ToggleStatus")
beego.Router("/backend/domain/tenant/index", &controllers.BackendTenantDomainController{}, "get:Index")
beego.Router("/backend/domain/tenant/myDomains", &controllers.BackendTenantDomainController{}, "get:MyDomains")
beego.Router("/backend/domain/tenant/apply", &controllers.BackendTenantDomainController{}, "post:Apply")
beego.Router("/backend/domain/tenant/audit", &controllers.BackendTenantDomainController{}, "post:Audit")
beego.Router("/backend/domain/tenant/toggleStatus", &controllers.BackendTenantDomainController{}, "post:ToggleStatus")
beego.Router("/backend/domain/tenant/delete/:id", &controllers.BackendTenantDomainController{}, "delete:Delete")
// 记事本管理
beego.Router("/backend/notebook/list", &controllers.BackendNotebookController{}, "get:List")
beego.Router("/backend/notebook/detail/:id", &controllers.BackendNotebookController{}, "get:Detail")
beego.Router("/backend/notebook/create", &controllers.BackendNotebookController{}, "post:Create")
beego.Router("/backend/notebook/update/:id", &controllers.BackendNotebookController{}, "post:Update")
beego.Router("/backend/notebook/delete/:id", &controllers.BackendNotebookController{}, "delete:Delete")
// 报销与审批管理
beego.Router("/backend/reimbursements", &controllers.BackendReimburseController{}, "get:List;post:Create")
beego.Router("/backend/reimbursements/types", &controllers.BackendReimburseController{}, "get:Types")
beego.Router("/backend/reimbursements/flows", &controllers.BackendReimburseController{}, "get:Flows")
beego.Router("/backend/reimbursements/:id/items", &controllers.BackendReimburseController{}, "post:AddItem")
beego.Router("/backend/reimbursements/:id/items/:item_id", &controllers.BackendReimburseController{}, "post:UpdateItem;delete:DeleteItem")
beego.Router("/backend/reimbursements/:id", &controllers.BackendReimburseController{}, "get:Detail;post:Update;delete:Delete")
beego.Router("/backend/reimbursements/:id/submit", &controllers.BackendReimburseController{}, "post:Submit")
beego.Router("/backend/reimbursements/:id/records", &controllers.BackendReimburseController{}, "get:Records")
// 日程提醒管理
beego.Router("/backend/reminder/list", &controllers.BackendReminderController{}, "get:GetReminderList")
beego.Router("/backend/reminder/test", &controllers.BackendReminderController{}, "post:TestReminder")
beego.Router("/backend/reminder/:id", &controllers.BackendReminderController{}, "get:GetReminderDetail;put:UpdateReminder;delete:DeleteReminder")
beego.Router("/backend/reminder", &controllers.BackendReminderController{}, "post:CreateReminder")
beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder")
beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder")
}
@@ -0,0 +1,3 @@
ALTER TABLE `yz_backend_reimbursement_items`
ADD COLUMN `receipt_url` varchar(500) NOT NULL DEFAULT '' COMMENT '票据文件地址' AFTER `receipt_flag`,
ADD KEY `idx_receipt_url` (`receipt_url`);
@@ -0,0 +1,34 @@
-- 报销费用类型表
-- 每个租户维护自己的费用类型,uid 保留用于数据隔离扩展。
CREATE TABLE IF NOT EXISTS `yz_backend_reimbursement_expense_types` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID',
`uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)',
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型名称',
`sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序值,越小越靠前',
`is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用:0-禁用 1-启用',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_tid_name` (`tid`, `name`),
KEY `idx_uid` (`uid`) USING BTREE,
KEY `idx_sort_order` (`sort_order`) USING BTREE,
KEY `idx_is_active` (`is_active`) USING BTREE,
KEY `idx_is_deleted` (`is_deleted`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销费用类型表';
-- 可按需为已有租户初始化默认类型:
-- INSERT INTO `yz_backend_reimbursement_expense_types`
-- (`tid`, `name`, `sort_order`)
-- VALUES
-- (1, '差旅费', 10),
-- (1, '交通费', 20),
-- (1, '餐饮费', 30),
-- (1, '办公费', 40),
-- (1, '通讯费', 50),
-- (1, '业务招待费', 60),
-- (1, '培训费', 70),
-- (1, '其他', 999);
+110
View File
@@ -0,0 +1,110 @@
-- 报销模块表结构
-- 执行前请确认当前数据库字符集为 utf8mb4
CREATE TABLE IF NOT EXISTS `yz_backend_approval_flows` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID',
`uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)',
`flow_name` varchar(100) NOT NULL DEFAULT '' COMMENT '流程名称',
`step_order` tinyint(1) NOT NULL DEFAULT '0' COMMENT '步骤顺序(从1开始)',
`approver_role` varchar(50) NOT NULL DEFAULT '' COMMENT '审批角色(如: dept_manager, finance, gm)',
`approver_rule` varchar(200) NOT NULL DEFAULT '' COMMENT '审批人规则(如: 部门主管, 财务总监)',
`timeout_hours` int(11) NOT NULL DEFAULT '48' COMMENT '超时提醒时间(小时)',
`is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用:0-禁用 1-启用',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_flow_name` (`flow_name`) USING BTREE,
KEY `idx_step_order` (`step_order`) USING BTREE,
KEY `idx_is_active` (`is_active`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE,
KEY `idx_delete_time` (`delete_time`) USING BTREE,
KEY `idx_tid` (`tid`) USING BTREE,
KEY `idx_uid` (`uid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='审批流程配置表';
CREATE TABLE IF NOT EXISTS `yz_backend_reimbursements` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID',
`uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)',
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '申请人ID',
`department_id` int(11) unsigned DEFAULT '0' COMMENT '部门ID',
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '报销事由',
`total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总金额',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0-草稿 1-审批中 2-已通过 3-已驳回 4-已撤回 5-已打款',
`expense_type` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型主分类',
`apply_date` date NOT NULL COMMENT '申请日期',
`description` text COMMENT '备注说明',
`current_approver_id` int(11) unsigned DEFAULT '0' COMMENT '当前审批人ID',
`current_step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '当前审批步骤(0:未提交)',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE,
KEY `idx_department_id` (`department_id`) USING BTREE,
KEY `idx_status` (`status`) USING BTREE,
KEY `idx_expense_type` (`expense_type`) USING BTREE,
KEY `idx_apply_date` (`apply_date`) USING BTREE,
KEY `idx_current_approver_id` (`current_approver_id`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE,
KEY `idx_update_time` (`update_time`) USING BTREE,
KEY `idx_delete_time` (`delete_time`) USING BTREE,
KEY `idx_tid` (`tid`) USING BTREE,
KEY `idx_uid` (`uid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销单主表';
CREATE TABLE IF NOT EXISTS `yz_backend_reimbursement_items` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID',
`uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)',
`reimbursement_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联报销单ID',
`expense_type` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型(具体子类)',
`expense_date` date NOT NULL COMMENT '费用发生日期',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '金额',
`quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量',
`unit_price` decimal(10,2) DEFAULT '0.00' COMMENT '单价',
`description` varchar(200) NOT NULL DEFAULT '' COMMENT '费用描述',
`receipt_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否有发票:0-否 1-是',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_reimbursement_id` (`reimbursement_id`) USING BTREE,
KEY `idx_expense_type` (`expense_type`) USING BTREE,
KEY `idx_expense_date` (`expense_date`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE,
KEY `idx_delete_time` (`delete_time`) USING BTREE,
KEY `idx_tid` (`tid`) USING BTREE,
KEY `idx_uid` (`uid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销费用明细表';
CREATE TABLE IF NOT EXISTS `yz_backend_approval_records` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID',
`uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)',
`reimbursement_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联报销单ID',
`approver_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '审批人ID',
`approver_name` varchar(100) NOT NULL DEFAULT '' COMMENT '审批人姓名(冗余)',
`action` tinyint(1) NOT NULL DEFAULT '0' COMMENT '操作:1-提交 2-通过 3-驳回 4-转审 5-撤回',
`comment` text COMMENT '审批意见',
`step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '审批步骤序号',
`prev_step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '上一步骤序号',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_reimbursement_id` (`reimbursement_id`) USING BTREE,
KEY `idx_approver_id` (`approver_id`) USING BTREE,
KEY `idx_action` (`action`) USING BTREE,
KEY `idx_step` (`step`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE,
KEY `idx_delete_time` (`delete_time`) USING BTREE,
KEY `idx_tid` (`tid`) USING BTREE,
KEY `idx_uid` (`uid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='审批记录表';