docs: 更新项目文档,完善需求和技术细节
This commit is contained in:
382
admin_website/src/views/Orders.vue
Normal file
382
admin_website/src/views/Orders.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div class="orders-container">
|
||||
<el-card class="orders-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>订单管理</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索条件 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="订单状态">
|
||||
<el-select v-model="searchForm.status" clearable placeholder="请选择">
|
||||
<el-option label="待支付" value="pending" />
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
<el-option label="已发货" value="shipped" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<el-table :data="orderList" border style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="orderNo" label="订单号" width="180" />
|
||||
<el-table-column prop="username" label="用户" />
|
||||
<el-table-column prop="phone" label="手机号" width="120" />
|
||||
<el-table-column prop="amount" label="订单金额" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.amount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.paymentStatus === 'pending'">待支付</el-tag>
|
||||
<el-tag v-else-if="scope.row.paymentStatus === 'paid'" type="success">已支付</el-tag>
|
||||
<el-tag v-else-if="scope.row.paymentStatus === 'cancelled'" type="danger">已取消</el-tag>
|
||||
<el-tag v-else>{{ scope.row.paymentStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发货状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.shippingStatus === 'pending'">待发货</el-tag>
|
||||
<el-tag v-else-if="scope.row.shippingStatus === 'shipped'" type="success">已发货</el-tag>
|
||||
<el-tag v-else-if="scope.row.shippingStatus === 'completed'" type="primary">已完成</el-tag>
|
||||
<el-tag v-else>{{ scope.row.shippingStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="下单时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleView(scope.row)">查看</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleUpdateStatus(scope.row)"
|
||||
:disabled="scope.row.paymentStatus === 'cancelled'"
|
||||
>
|
||||
状态
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
class="pagination"
|
||||
/>
|
||||
|
||||
<!-- 订单详情对话框 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="订单详情" width="700px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="订单号">{{ currentOrder.orderNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户">{{ currentOrder.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ currentOrder.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单金额">¥{{ currentOrder.amount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付状态">
|
||||
<el-tag v-if="currentOrder.paymentStatus === 'pending'">待支付</el-tag>
|
||||
<el-tag v-else-if="currentOrder.paymentStatus === 'paid'" type="success">已支付</el-tag>
|
||||
<el-tag v-else-if="currentOrder.paymentStatus === 'cancelled'" type="danger">已取消</el-tag>
|
||||
<el-tag v-else>{{ currentOrder.paymentStatus }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发货状态">
|
||||
<el-tag v-if="currentOrder.shippingStatus === 'pending'">待发货</el-tag>
|
||||
<el-tag v-else-if="currentOrder.shippingStatus === 'shipped'" type="success">已发货</el-tag>
|
||||
<el-tag v-else-if="currentOrder.shippingStatus === 'completed'" type="primary">已完成</el-tag>
|
||||
<el-tag v-else>{{ currentOrder.shippingStatus }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收货地址">{{ currentOrder.shippingAddress }}</el-descriptions-item>
|
||||
<el-descriptions-item label="下单时间">{{ formatDate(currentOrder.createTime) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-table :data="currentOrder.items" style="margin-top: 20px;" border>
|
||||
<el-table-column prop="productName" label="商品名称" />
|
||||
<el-table-column prop="quantity" label="数量" width="80" />
|
||||
<el-table-column prop="unitPrice" label="单价" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.unitPrice }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="小计" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ (scope.row.unitPrice * scope.row.quantity).toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 更新订单状态对话框 -->
|
||||
<el-dialog v-model="statusDialogVisible" title="更新订单状态" width="500px">
|
||||
<el-form :model="statusForm" label-width="100px">
|
||||
<el-form-item label="支付状态">
|
||||
<el-select v-model="statusForm.paymentStatus" placeholder="请选择">
|
||||
<el-option label="待支付" value="pending" />
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发货状态">
|
||||
<el-select v-model="statusForm.shippingStatus" placeholder="请选择">
|
||||
<el-option label="待发货" value="pending" />
|
||||
<el-option label="已发货" value="shipped" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="statusDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitStatusUpdate">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
export default {
|
||||
name: 'Orders',
|
||||
setup() {
|
||||
const loading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const statusDialogVisible = ref(false)
|
||||
|
||||
const searchForm = reactive({
|
||||
status: ''
|
||||
})
|
||||
|
||||
const dateRange = ref([])
|
||||
|
||||
const statusForm = reactive({
|
||||
id: null,
|
||||
paymentStatus: '',
|
||||
shippingStatus: ''
|
||||
})
|
||||
|
||||
const orderList = ref([])
|
||||
const currentOrder = ref({})
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
const fetchOrders = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 模拟数据
|
||||
orderList.value = [
|
||||
{
|
||||
id: 1,
|
||||
orderNo: 'ORD202401150001',
|
||||
username: 'user001',
|
||||
phone: '13800001111',
|
||||
amount: 199,
|
||||
paymentStatus: 'paid',
|
||||
shippingStatus: 'shipped',
|
||||
createTime: '2024-01-15 10:30:00',
|
||||
shippingAddress: '浙江省杭州市西湖区文三路159号',
|
||||
items: [
|
||||
{ productName: 'AI鉴花小程序', quantity: 1, unitPrice: 199 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderNo: 'ORD202401160002',
|
||||
username: 'admin',
|
||||
phone: '13900002222',
|
||||
amount: 598,
|
||||
paymentStatus: 'paid',
|
||||
shippingStatus: 'pending',
|
||||
createTime: '2024-01-16 14:20:00',
|
||||
shippingAddress: '浙江省杭州市滨江区网商路699号',
|
||||
items: [
|
||||
{ productName: '花卉识别API', quantity: 2, unitPrice: 299 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
orderNo: 'ORD202401170003',
|
||||
username: 'editor001',
|
||||
phone: '13700003333',
|
||||
amount: 999,
|
||||
paymentStatus: 'pending',
|
||||
shippingStatus: 'pending',
|
||||
createTime: '2024-01-17 09:15:00',
|
||||
shippingAddress: '浙江省杭州市余杭区五常大道1001号',
|
||||
items: [
|
||||
{ productName: '企业版解决方案', quantity: 1, unitPrice: 999 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
pagination.total = orderList.value.length
|
||||
} catch (error) {
|
||||
ElMessage.error('获取订单列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchForm.status = ''
|
||||
dateRange.value = []
|
||||
pagination.page = 1
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
const handleView = (row) => {
|
||||
currentOrder.value = { ...row }
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
const handleUpdateStatus = (row) => {
|
||||
statusForm.id = row.id
|
||||
statusForm.paymentStatus = row.paymentStatus
|
||||
statusForm.shippingStatus = row.shippingStatus
|
||||
statusDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交状态更新
|
||||
const submitStatusUpdate = async () => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
// 更新订单状态
|
||||
const index = orderList.value.findIndex(item => item.id === statusForm.id)
|
||||
if (index !== -1) {
|
||||
orderList.value[index].paymentStatus = statusForm.paymentStatus
|
||||
orderList.value[index].shippingStatus = statusForm.shippingStatus
|
||||
}
|
||||
|
||||
ElMessage.success('状态更新成功')
|
||||
statusDialogVisible.value = false
|
||||
} catch (error) {
|
||||
ElMessage.error('状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 分页相关
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
pagination.page = 1
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
detailDialogVisible,
|
||||
statusDialogVisible,
|
||||
searchForm,
|
||||
dateRange,
|
||||
statusForm,
|
||||
orderList,
|
||||
currentOrder,
|
||||
pagination,
|
||||
formatDate,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleView,
|
||||
handleUpdateStatus,
|
||||
submitStatusUpdate,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.orders-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.orders-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
454
admin_website/src/views/Statistics.vue
Normal file
454
admin_website/src/views/Statistics.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="statistics-container">
|
||||
<el-card class="statistics-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据统计</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<el-row :gutter="20" class="stats-overview">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon users">
|
||||
<el-icon><user /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.userCount || 0 }}</div>
|
||||
<div class="stat-label">总用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon products">
|
||||
<el-icon><goods /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.productCount || 0 }}</div>
|
||||
<div class="stat-label">商品总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon orders">
|
||||
<el-icon><document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ statsData.orderCount || 0 }}</div>
|
||||
<div class="stat-label">订单总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon revenue">
|
||||
<el-icon><money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">¥{{ statsData.totalRevenue || 0 }}</div>
|
||||
<div class="stat-label">总销售额</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-section">
|
||||
<el-col :span="24">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<h3>用户增长趋势</h3>
|
||||
<el-select v-model="userChartRange" size="small" style="width: 120px;" @change="fetchUserChartData">
|
||||
<el-option label="近7天" value="7" />
|
||||
<el-option label="近30天" value="30" />
|
||||
<el-option label="近90天" value="90" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="userChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<h3>商品分类分布</h3>
|
||||
</template>
|
||||
<div ref="categoryChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<h3>订单状态分布</h3>
|
||||
</template>
|
||||
<div ref="orderStatusChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { statisticsAPI } from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: 'Statistics',
|
||||
setup() {
|
||||
const userChart = ref(null)
|
||||
const categoryChart = ref(null)
|
||||
const orderStatusChart = ref(null)
|
||||
|
||||
const userChartInstance = ref(null)
|
||||
const categoryChartInstance = ref(null)
|
||||
const orderStatusChartInstance = ref(null)
|
||||
|
||||
const userChartRange = ref('30')
|
||||
|
||||
const statsData = reactive({
|
||||
userCount: 0,
|
||||
productCount: 0,
|
||||
orderCount: 0,
|
||||
totalRevenue: 0
|
||||
})
|
||||
|
||||
const chartData = reactive({
|
||||
userGrowth: [],
|
||||
categoryDistribution: [],
|
||||
orderStatusDistribution: []
|
||||
})
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStatsData = async () => {
|
||||
try {
|
||||
// 模拟数据
|
||||
statsData.userCount = 1234
|
||||
statsData.productCount = 567
|
||||
statsData.orderCount = 890
|
||||
statsData.totalRevenue = 123456.78
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户图表数据
|
||||
const fetchUserChartData = async () => {
|
||||
try {
|
||||
// 模拟数据
|
||||
chartData.userGrowth = {
|
||||
dates: ['1月1日', '1月2日', '1月3日', '1月4日', '1月5日', '1月6日', '1月7日'],
|
||||
counts: [10, 25, 15, 30, 20, 35, 40]
|
||||
}
|
||||
renderUserChart()
|
||||
} catch (error) {
|
||||
console.error('获取用户图表数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类图表数据
|
||||
const fetchCategoryChartData = async () => {
|
||||
try {
|
||||
// 模拟数据
|
||||
chartData.categoryDistribution = [
|
||||
{ name: '鲜花', value: 45 },
|
||||
{ name: '盆栽', value: 30 },
|
||||
{ name: '种子', value: 15 },
|
||||
{ name: '工具', value: 10 }
|
||||
]
|
||||
renderCategoryChart()
|
||||
} catch (error) {
|
||||
console.error('获取分类图表数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单状态图表数据
|
||||
const fetchOrderStatusChartData = async () => {
|
||||
try {
|
||||
// 模拟数据
|
||||
chartData.orderStatusDistribution = [
|
||||
{ name: '待支付', value: 20 },
|
||||
{ name: '已支付', value: 50 },
|
||||
{ name: '已发货', value: 15 },
|
||||
{ name: '已完成', value: 10 },
|
||||
{ name: '已取消', value: 5 }
|
||||
]
|
||||
renderOrderStatusChart()
|
||||
} catch (error) {
|
||||
console.error('获取订单状态图表数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染用户增长趋势图
|
||||
const renderUserChart = () => {
|
||||
if (userChart.value) {
|
||||
if (!userChartInstance.value) {
|
||||
userChartInstance.value = echarts.init(userChart.value)
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.userGrowth.dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
data: chartData.userGrowth.counts,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {}
|
||||
}]
|
||||
}
|
||||
|
||||
userChartInstance.value.setOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染商品分类分布图
|
||||
const renderCategoryChart = () => {
|
||||
if (categoryChart.value) {
|
||||
if (!categoryChartInstance.value) {
|
||||
categoryChartInstance.value = echarts.init(categoryChart.value)
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
series: [{
|
||||
name: '商品分类',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData.categoryDistribution
|
||||
}]
|
||||
}
|
||||
|
||||
categoryChartInstance.value.setOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染订单状态分布图
|
||||
const renderOrderStatusChart = () => {
|
||||
if (orderStatusChart.value) {
|
||||
if (!orderStatusChartInstance.value) {
|
||||
orderStatusChartInstance.value = echarts.init(orderStatusChart.value)
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
series: [{
|
||||
name: '订单状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData.orderStatusDistribution
|
||||
}]
|
||||
}
|
||||
|
||||
orderStatusChartInstance.value.setOption(option)
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const handleResize = () => {
|
||||
if (userChartInstance.value) {
|
||||
userChartInstance.value.resize()
|
||||
}
|
||||
if (categoryChartInstance.value) {
|
||||
categoryChartInstance.value.resize()
|
||||
}
|
||||
if (orderStatusChartInstance.value) {
|
||||
orderStatusChartInstance.value.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStatsData()
|
||||
fetchUserChartData()
|
||||
fetchCategoryChartData()
|
||||
fetchOrderStatusChartData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
return {
|
||||
userChart,
|
||||
categoryChart,
|
||||
orderStatusChart,
|
||||
userChartRange,
|
||||
statsData,
|
||||
fetchUserChartData
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.statistics-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.statistics-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-icon.users {
|
||||
background: #e8f5e8;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.stat-icon.products {
|
||||
background: #e3f2fd;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.stat-icon.orders {
|
||||
background: #fff3e0;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.stat-icon.revenue {
|
||||
background: #fce4ec;
|
||||
color: #E91E63;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-header h3 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 340px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
test content
|
||||
@@ -0,0 +1 @@
|
||||
test content
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **Base URL**: `http://localhost:3200/api/v1`
|
||||
- **Base URL**:
|
||||
- Node.js后端: `http://localhost:3000/api/v1`
|
||||
- Java后端: `http://localhost:3200`
|
||||
- **认证方式**: Bearer Token (JWT)
|
||||
- **响应格式**: JSON
|
||||
- **字符编码**: UTF-8
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
- SQLite(开发环境)
|
||||
- MySQL(生产环境)
|
||||
- Redis
|
||||
- Java
|
||||
- Spring Boot
|
||||
- Maven
|
||||
|
||||
## 文档结构
|
||||
- 需求文档: 项目业务需求和功能说明
|
||||
|
||||
12
docs/项目总览.md
12
docs/项目总览.md
@@ -1,10 +1,10 @@
|
||||
# 爱鉴花项目总览
|
||||
|
||||
## 项目概述
|
||||
爱鉴花是一款通过AI图片识别植物类型的微信小程序应用,为用户提供花卉相关信息、购买、配送等服务。
|
||||
## 项目简介
|
||||
爱鉴花是一个集花卉识别、植物知识科普、在线商城于一体的综合性微信小程序平台。用户可以通过拍照识别花卉,获取详细的植物信息,同时可以在商城中购买相关产品。
|
||||
|
||||
## 项目组成
|
||||
1. **微信小程序 (uni-app)** - 用户端应用,提供植物识别、商城购物、配送服务等功能
|
||||
1. **微信小程序 (uni-app)** - 前端用户界面,提供植物识别、植物知识、商城购物、配送服务等功能
|
||||
2. **后端接口 (Node.js)** - 提供RESTful API服务,包括植物识别、用户管理、商品管理、订单管理等
|
||||
3. **后台管理系统 (Vue3)** - 管理后台,用于用户管理、商品管理、订单管理、数据统计等
|
||||
4. **官方网站 (HTML5 Bootstrap)** - 公司展示网站,提供产品介绍、公司信息、联系方式等
|
||||
@@ -13,8 +13,10 @@
|
||||
|
||||
## 技术架构
|
||||
- **前端技术栈**: uni-app、Vue3、Element Plus、Bootstrap
|
||||
- **后端技术栈**: Node.js、Express.js、MySQL(生产环境)、SQLite(开发环境)、Redis
|
||||
- **开发工具**: HBuilderX、VSCode、Git
|
||||
- **后端技术栈**:
|
||||
- Node.js、Express.js、MySQL(生产环境)、SQLite(开发环境)、Redis
|
||||
- Java Spring Boot(新后端,用于替代部分Node.js功能)
|
||||
- **开发工具**: HBuilderX、VSCode、Git、Maven
|
||||
- **部署环境**: Nginx、Docker、云服务器
|
||||
|
||||
## 项目文档
|
||||
|
||||
105
java-backend/pom.xml
Normal file
105
java-backend/pom.xml
Normal file
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.aijianhua</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>爱鉴花后端服务</name>
|
||||
<description>爱鉴花小程序Java版后端服务</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Connector -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.28</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.9.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Flyway -->
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.aijianhua.backend;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.aijianhua.backend.config;
|
||||
|
||||
import com.aijianhua.backend.security.JwtAuthenticationEntryPoint;
|
||||
import com.aijianhua.backend.security.JwtAuthenticationFilter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Autowired
|
||||
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
|
||||
|
||||
@Autowired
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http.csrf().disable()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/api/v1/auth/**").permitAll()
|
||||
.antMatchers("/health").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.exceptionHandling()
|
||||
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
|
||||
.and()
|
||||
.sessionManagement()
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
||||
|
||||
// 添加JWT过滤器
|
||||
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.aijianhua.backend.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class SwaggerResourceConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("swagger-ui.html")
|
||||
.addResourceLocations("classpath:/META-INF/resources/");
|
||||
|
||||
registry.addResourceHandler("/webjars/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.aijianhua.backend.config;
|
||||
|
||||
import com.aijianhua.backend.interceptor.JwtInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private JwtInterceptor jwtInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册JWT拦截器,排除认证相关接口
|
||||
registry.addInterceptor(jwtInterceptor)
|
||||
.addPathPatterns("/api/v1/**")
|
||||
.excludePathPatterns("/api/v1/auth/**");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.aijianhua.backend.controller;
|
||||
|
||||
import com.aijianhua.backend.dto.ApiResponse;
|
||||
import com.aijianhua.backend.dto.LoginRequest;
|
||||
import com.aijianhua.backend.dto.RegisterRequest;
|
||||
import com.aijianhua.backend.dto.UserResponse;
|
||||
import com.aijianhua.backend.entity.User;
|
||||
import com.aijianhua.backend.service.AuthService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import javax.validation.Valid;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
public class AuthController {
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public ApiResponse<Map<String, Object>> register(@Valid @RequestBody RegisterRequest registerRequest) {
|
||||
User user = authService.register(registerRequest);
|
||||
|
||||
// 生成JWT token
|
||||
String token = authService.generateToken(user);
|
||||
|
||||
// 构造响应数据
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("user_id", user.getId());
|
||||
data.put("username", user.getUsername());
|
||||
data.put("phone", user.getPhone());
|
||||
data.put("email", user.getEmail());
|
||||
data.put("user_type", user.getUserType());
|
||||
data.put("token", token);
|
||||
|
||||
return ApiResponse.created(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ApiResponse<Map<String, Object>> login(@Valid @RequestBody LoginRequest loginRequest) {
|
||||
User user = authService.login(loginRequest);
|
||||
|
||||
// 生成JWT token
|
||||
String token = authService.generateToken(user);
|
||||
|
||||
// 构造响应数据
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("user_id", user.getId());
|
||||
data.put("username", user.getUsername());
|
||||
data.put("phone", user.getPhone());
|
||||
data.put("email", user.getEmail());
|
||||
data.put("user_type", user.getUserType());
|
||||
data.put("avatar_url", user.getAvatarUrl());
|
||||
data.put("token", token);
|
||||
|
||||
return ApiResponse.success("登录成功", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/me")
|
||||
public ApiResponse<UserResponse> getCurrentUser(@RequestAttribute("userId") Long userId) {
|
||||
UserResponse userResponse = authService.getUserInfo(userId);
|
||||
return ApiResponse.success(userResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.aijianhua.backend.controller;
|
||||
|
||||
import com.aijianhua.backend.dto.ApiResponse;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 健康检查控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/health")
|
||||
public class HealthController {
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<Map<String, Object>> healthCheck() {
|
||||
Map<String, Object> healthInfo = new HashMap<>();
|
||||
healthInfo.put("status", "UP");
|
||||
healthInfo.put("timestamp", LocalDateTime.now());
|
||||
healthInfo.put("service", "aijianhua-backend");
|
||||
|
||||
return ApiResponse.success(healthInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.aijianhua.backend.controller;
|
||||
|
||||
import com.aijianhua.backend.dto.ApiResponse;
|
||||
import com.aijianhua.backend.dto.PageResponse;
|
||||
import com.aijianhua.backend.dto.UploadResponse;
|
||||
import com.aijianhua.backend.entity.Upload;
|
||||
import com.aijianhua.backend.service.UploadService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/upload")
|
||||
public class UploadController {
|
||||
|
||||
@Autowired
|
||||
private UploadService uploadService;
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*/
|
||||
@PostMapping
|
||||
public ApiResponse<UploadResponse> uploadFile(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "type", required = false) String type,
|
||||
HttpServletRequest request) throws IOException {
|
||||
|
||||
// 从请求中获取用户ID
|
||||
Long userId = (Long) request.getAttribute("userId");
|
||||
|
||||
// 上传文件
|
||||
Upload upload = uploadService.uploadFile(file, type, userId);
|
||||
|
||||
// 构造响应数据
|
||||
UploadResponse uploadResponse = new UploadResponse();
|
||||
uploadResponse.setUrl(upload.getFilePath());
|
||||
uploadResponse.setFilename(upload.getStoredName());
|
||||
uploadResponse.setOriginalName(upload.getOriginalName());
|
||||
uploadResponse.setSize(upload.getFileSize());
|
||||
uploadResponse.setMimeType(upload.getMimeType());
|
||||
uploadResponse.setUploadType(upload.getUploadType());
|
||||
|
||||
return ApiResponse.success("上传成功", uploadResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传文件列表
|
||||
*/
|
||||
@GetMapping
|
||||
public PageResponse<Upload> getUploads(
|
||||
@RequestParam(value = "page", defaultValue = "1") int page,
|
||||
@RequestParam(value = "limit", defaultValue = "10") int limit,
|
||||
@RequestParam(value = "type", required = false) String type,
|
||||
HttpServletRequest request) {
|
||||
|
||||
// 从请求中获取用户ID
|
||||
Long userId = (Long) request.getAttribute("userId");
|
||||
|
||||
// 获取文件列表
|
||||
Page<Upload> uploads = uploadService.getUploads(userId, type, page, limit);
|
||||
|
||||
return PageResponse.success(uploads);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除上传文件
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Map<String, String>> deleteUpload(
|
||||
@PathVariable Long id,
|
||||
HttpServletRequest request) throws IOException {
|
||||
|
||||
// 从请求中获取用户ID
|
||||
Long userId = (Long) request.getAttribute("userId");
|
||||
|
||||
// 删除文件
|
||||
uploadService.deleteUpload(id, userId);
|
||||
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("message", "删除成功");
|
||||
return ApiResponse.success(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.aijianhua.backend.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ApiResponse<T> {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public ApiResponse() {}
|
||||
|
||||
public ApiResponse(Integer code, String message, T data) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<>(200, "操作成功", data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
return new ApiResponse<>(200, message, data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> created(T data) {
|
||||
return new ApiResponse<>(201, "创建成功", data);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(Integer code, String message) {
|
||||
return new ApiResponse<>(code, message, null);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return new ApiResponse<>(500, message, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.aijianhua.backend.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
@NotBlank(message = "登录账号不能为空")
|
||||
private String login;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.aijianhua.backend.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
@Data
|
||||
public class PageResponse<T> {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private PageData<T> data;
|
||||
|
||||
public PageResponse() {}
|
||||
|
||||
public PageResponse(Integer code, String message, PageData<T> data) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static <T> PageResponse<T> success(Page<T> page) {
|
||||
PageData<T> pageData = new PageData<>();
|
||||
pageData.setItems(page.getContent());
|
||||
pageData.setPagination(new Pagination(
|
||||
page.getNumber() + 1,
|
||||
page.getSize(),
|
||||
page.getTotalElements(),
|
||||
page.getTotalPages()
|
||||
));
|
||||
return new PageResponse<>(200, "获取成功", pageData);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PageData<T> {
|
||||
private Iterable<T> items;
|
||||
private Pagination pagination;
|
||||
|
||||
public void setItems(Iterable<T> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public void setPagination(Pagination pagination) {
|
||||
this.pagination = pagination;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Pagination {
|
||||
private Integer page;
|
||||
private Integer limit;
|
||||
private Long total;
|
||||
private Integer pages;
|
||||
|
||||
public Pagination() {}
|
||||
|
||||
public Pagination(Integer page, Integer limit, Long total, Integer pages) {
|
||||
this.page = page;
|
||||
this.limit = limit;
|
||||
this.total = total;
|
||||
this.pages = pages;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.aijianhua.backend.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
@Data
|
||||
public class RegisterRequest {
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, message = "密码长度不能少于6位")
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
private String phone;
|
||||
|
||||
private String email;
|
||||
|
||||
private String userType = "farmer";
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getUserType() {
|
||||
return userType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.aijianhua.backend.dto;
|
||||
|
||||
public class UploadResponse {
|
||||
private String url;
|
||||
private String filename;
|
||||
private String originalName;
|
||||
private Long size;
|
||||
private String mimeType;
|
||||
private String uploadType;
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public String getOriginalName() {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
public void setOriginalName(String originalName) {
|
||||
this.originalName = originalName;
|
||||
}
|
||||
|
||||
public Long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(Long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public void setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
public String getUploadType() {
|
||||
return uploadType;
|
||||
}
|
||||
|
||||
public void setUploadType(String uploadType) {
|
||||
this.uploadType = uploadType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.aijianhua.backend.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class UserResponse {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String userType;
|
||||
private String avatarUrl;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime lastLogin;
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getUserType() {
|
||||
return userType;
|
||||
}
|
||||
|
||||
public void setUserType(String userType) {
|
||||
this.userType = userType;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastLogin() {
|
||||
return lastLogin;
|
||||
}
|
||||
|
||||
public void setLastLogin(LocalDateTime lastLogin) {
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.aijianhua.backend.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "uploads")
|
||||
public class Upload {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "original_name", nullable = false)
|
||||
private String originalName;
|
||||
|
||||
@Column(name = "stored_name", nullable = false)
|
||||
private String storedName;
|
||||
|
||||
@Column(name = "file_path", nullable = false)
|
||||
private String filePath;
|
||||
|
||||
@Column(name = "file_size", nullable = false)
|
||||
private Long fileSize;
|
||||
|
||||
@Column(name = "mime_type", nullable = false)
|
||||
private String mimeType;
|
||||
|
||||
@Column(name = "file_type")
|
||||
private String fileType;
|
||||
|
||||
@Column(name = "upload_type", nullable = false)
|
||||
private String uploadType;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String status = "active";
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getOriginalName() {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
public void setOriginalName(String originalName) {
|
||||
this.originalName = originalName;
|
||||
}
|
||||
|
||||
public String getStoredName() {
|
||||
return storedName;
|
||||
}
|
||||
|
||||
public void setStoredName(String storedName) {
|
||||
this.storedName = storedName;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public Long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(Long fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public void setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
public void setFileType(String fileType) {
|
||||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
public String getUploadType() {
|
||||
return uploadType;
|
||||
}
|
||||
|
||||
public void setUploadType(String uploadType) {
|
||||
this.uploadType = uploadType;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.aijianhua.backend.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String username;
|
||||
|
||||
@Column(name = "password_hash", nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String phone;
|
||||
|
||||
@Column(unique = true)
|
||||
private String email;
|
||||
|
||||
@Column(name = "user_type", nullable = false)
|
||||
private String userType;
|
||||
|
||||
@Column(name = "avatar_url")
|
||||
private String avatarUrl;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "last_login")
|
||||
private LocalDateTime lastLogin;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getUserType() {
|
||||
return userType;
|
||||
}
|
||||
|
||||
public void setUserType(String userType) {
|
||||
this.userType = userType;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastLogin() {
|
||||
return lastLogin;
|
||||
}
|
||||
|
||||
public void setLastLogin(LocalDateTime lastLogin) {
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.aijianhua.backend.exception;
|
||||
|
||||
public class BusinessException extends RuntimeException {
|
||||
private int code = 400;
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public BusinessException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.aijianhua.backend.exception;
|
||||
|
||||
import com.aijianhua.backend.dto.ApiResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理参数验证异常(RequestBody参数验证)
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(MethodArgumentNotValidException ex) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
ex.getBindingResult().getAllErrors().forEach((error) -> {
|
||||
String fieldName = ((FieldError) error).getField();
|
||||
String errorMessage = error.getDefaultMessage();
|
||||
errors.put(fieldName, errorMessage);
|
||||
});
|
||||
return ResponseEntity.badRequest().body(new ApiResponse<>(400, "参数验证失败", errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数验证异常(方法调用时的验证)
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<ApiResponse<Map<String, String>>> handleConstraintViolationException(ConstraintViolationException ex) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
|
||||
String propertyPath = violation.getPropertyPath().toString();
|
||||
String message = violation.getMessage();
|
||||
errors.put(propertyPath, message);
|
||||
}
|
||||
return ResponseEntity.badRequest().body(new ApiResponse<>(400, "参数验证失败", errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleBusinessException(BusinessException ex) {
|
||||
return ResponseEntity.status(ex.getCode()).body(new ApiResponse<>(ex.getCode(), ex.getMessage(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理未授权异常
|
||||
*/
|
||||
@ExceptionHandler(UnauthorizedException.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleUnauthorizedException(UnauthorizedException ex) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ApiResponse<>(401, ex.getMessage(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理其他所有未处理的异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Object>> handleGenericException(Exception ex) {
|
||||
// 记录日志
|
||||
ex.printStackTrace();
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse<>(500, "系统内部错误", null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.aijianhua.backend.exception;
|
||||
|
||||
public class UnauthorizedException extends RuntimeException {
|
||||
public UnauthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.aijianhua.backend.interceptor;
|
||||
|
||||
import com.aijianhua.backend.util.JwtUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class JwtInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String authorizationHeader = request.getHeader("Authorization");
|
||||
|
||||
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
|
||||
String token = authorizationHeader.substring(7);
|
||||
|
||||
if (jwtUtil.validateToken(token) && !jwtUtil.isTokenExpired(token)) {
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
request.setAttribute("userId", userId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 未提供有效的认证token
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"未提供有效的认证token\",\"data\":null}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.aijianhua.backend.repository;
|
||||
|
||||
import com.aijianhua.backend.entity.Upload;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface UploadRepository extends JpaRepository<Upload, Long> {
|
||||
Page<Upload> findByUserId(Long userId, Pageable pageable);
|
||||
Page<Upload> findByUserIdAndUploadType(Long userId, String uploadType, Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.aijianhua.backend.repository;
|
||||
|
||||
import com.aijianhua.backend.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Optional<User> findByUsernameOrPhoneOrEmailAndStatus(String username, String phone, String email, Integer status);
|
||||
Optional<User> findByUsername(String username);
|
||||
Optional<User> findByPhone(String phone);
|
||||
Optional<User> findByEmail(String email);
|
||||
Boolean existsByUsername(String username);
|
||||
Boolean existsByPhone(String phone);
|
||||
Boolean existsByEmail(String email);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.aijianhua.backend.security;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException, ServletException {
|
||||
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
|
||||
// 返回未授权的JSON响应
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"未提供有效的认证token\",\"data\":null}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.aijianhua.backend.security;
|
||||
|
||||
import com.aijianhua.backend.entity.User;
|
||||
import com.aijianhua.backend.repository.UserRepository;
|
||||
import com.aijianhua.backend.util.JwtUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String authorizationHeader = request.getHeader("Authorization");
|
||||
|
||||
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
|
||||
String token = authorizationHeader.substring(7);
|
||||
|
||||
if (jwtUtil.validateToken(token) && !jwtUtil.isTokenExpired(token)) {
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
|
||||
// 从数据库获取用户信息
|
||||
User user = userRepository.findById(userId).orElse(null);
|
||||
|
||||
if (user != null && user.getStatus() == 1) {
|
||||
// 创建认证对象
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
// 设置安全上下文
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.aijianhua.backend.service;
|
||||
|
||||
import com.aijianhua.backend.dto.LoginRequest;
|
||||
import com.aijianhua.backend.dto.RegisterRequest;
|
||||
import com.aijianhua.backend.dto.UserResponse;
|
||||
import com.aijianhua.backend.entity.User;
|
||||
import com.aijianhua.backend.exception.BusinessException;
|
||||
import com.aijianhua.backend.repository.UserRepository;
|
||||
import com.aijianhua.backend.util.JwtUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
public User register(RegisterRequest registerRequest) {
|
||||
// 检查用户是否已存在
|
||||
if (userRepository.existsByUsername(registerRequest.getUsername()) ||
|
||||
userRepository.existsByPhone(registerRequest.getPhone()) ||
|
||||
(registerRequest.getEmail() != null && userRepository.existsByEmail(registerRequest.getEmail()))) {
|
||||
throw new BusinessException("用户名、手机号或邮箱已存在");
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
User user = new User();
|
||||
user.setUsername(registerRequest.getUsername());
|
||||
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
|
||||
user.setPhone(registerRequest.getPhone());
|
||||
user.setEmail(registerRequest.getEmail());
|
||||
user.setUserType(registerRequest.getUserType());
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
public User login(LoginRequest loginRequest) {
|
||||
// 查询用户(支持用户名、手机号、邮箱登录)
|
||||
Optional<User> userOptional = userRepository.findByUsernameOrPhoneOrEmailAndStatus(
|
||||
loginRequest.getLogin(),
|
||||
loginRequest.getLogin(),
|
||||
loginRequest.getLogin(),
|
||||
1
|
||||
);
|
||||
|
||||
if (!userOptional.isPresent()) {
|
||||
throw new BusinessException("用户不存在或已被禁用");
|
||||
}
|
||||
|
||||
User user = userOptional.get();
|
||||
|
||||
// 验证密码
|
||||
if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
|
||||
throw new BusinessException("密码不正确");
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
user.setLastLogin(LocalDateTime.now());
|
||||
userRepository.save(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成JWT token
|
||||
*/
|
||||
public String generateToken(User user) {
|
||||
return jwtUtil.generateToken(user.getId(), user.getUsername(), user.getUserType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
public UserResponse getUserInfo(Long userId) {
|
||||
Optional<User> userOptional = userRepository.findById(userId);
|
||||
if (!userOptional.isPresent()) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
User user = userOptional.get();
|
||||
UserResponse userResponse = new UserResponse();
|
||||
userResponse.setId(user.getId());
|
||||
userResponse.setUsername(user.getUsername());
|
||||
userResponse.setPhone(user.getPhone());
|
||||
userResponse.setEmail(user.getEmail());
|
||||
userResponse.setUserType(user.getUserType());
|
||||
userResponse.setAvatarUrl(user.getAvatarUrl());
|
||||
userResponse.setCreatedAt(user.getCreatedAt());
|
||||
userResponse.setLastLogin(user.getLastLogin());
|
||||
|
||||
return userResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.aijianhua.backend.service;
|
||||
|
||||
import com.aijianhua.backend.entity.Upload;
|
||||
import com.aijianhua.backend.exception.BusinessException;
|
||||
import com.aijianhua.backend.repository.UploadRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class UploadService {
|
||||
|
||||
@Autowired
|
||||
private UploadRepository uploadRepository;
|
||||
|
||||
private static final String UPLOAD_DIR = "uploads/";
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*/
|
||||
public Upload uploadFile(MultipartFile file, String uploadType, Long userId) throws IOException {
|
||||
// 创建上传目录
|
||||
String typeDir = uploadType != null ? uploadType : "common";
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR + typeDir);
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String fileExtension = "";
|
||||
if (originalFilename != null && originalFilename.contains(".")) {
|
||||
fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
}
|
||||
String storedName = typeDir + "_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8) + fileExtension;
|
||||
|
||||
// 保存文件
|
||||
Path filePath = uploadPath.resolve(storedName);
|
||||
Files.write(filePath, file.getBytes());
|
||||
|
||||
// 保存文件记录到数据库
|
||||
Upload upload = new Upload();
|
||||
upload.setUserId(userId);
|
||||
upload.setOriginalName(originalFilename);
|
||||
upload.setStoredName(storedName);
|
||||
upload.setFilePath("/" + uploadPath.toString() + "/" + storedName);
|
||||
upload.setFileSize(file.getSize());
|
||||
upload.setMimeType(file.getContentType());
|
||||
upload.setFileType(getFileType(file.getContentType()));
|
||||
upload.setUploadType(typeDir);
|
||||
upload.setCreatedAt(LocalDateTime.now());
|
||||
upload.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return uploadRepository.save(upload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
*/
|
||||
private String getFileType(String mimeType) {
|
||||
if (mimeType == null) {
|
||||
return "other";
|
||||
}
|
||||
if (mimeType.startsWith("image/")) {
|
||||
return "image";
|
||||
}
|
||||
if (mimeType.startsWith("video/")) {
|
||||
return "video";
|
||||
}
|
||||
if (mimeType.startsWith("audio/")) {
|
||||
return "audio";
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传文件列表
|
||||
*/
|
||||
public Page<Upload> getUploads(Long userId, String type, int page, int limit) {
|
||||
Pageable pageable = PageRequest.of(page - 1, limit, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
|
||||
if (type != null && !type.isEmpty()) {
|
||||
return uploadRepository.findByUserIdAndUploadType(userId, type, pageable);
|
||||
}
|
||||
|
||||
return uploadRepository.findByUserId(userId, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除上传文件
|
||||
*/
|
||||
public void deleteUpload(Long id, Long userId) throws IOException {
|
||||
// 查询文件信息
|
||||
Upload upload = uploadRepository.findById(id).orElse(null);
|
||||
if (upload == null || !upload.getUserId().equals(userId)) {
|
||||
throw new BusinessException("文件不存在");
|
||||
}
|
||||
|
||||
// 删除物理文件
|
||||
Path filePath = Paths.get(upload.getFilePath().substring(1)); // 移除开头的 "/"
|
||||
if (Files.exists(filePath)) {
|
||||
Files.delete(filePath);
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
uploadRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.aijianhua.backend.util;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private Long expiration;
|
||||
|
||||
/**
|
||||
* 生成JWT token
|
||||
*/
|
||||
public String generateToken(Long userId, String username, String userType) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("username", username);
|
||||
claims.put("user_type", userType);
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(username)
|
||||
.setIssuedAt(new Date(System.currentTimeMillis()))
|
||||
.setExpiration(new Date(System.currentTimeMillis() + expiration))
|
||||
.signWith(SignatureAlgorithm.HS512, secret)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT token
|
||||
*/
|
||||
public Boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
|
||||
return Long.parseLong(claims.get("userId").toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取用户名
|
||||
*/
|
||||
public String getUsernameFromToken(String token) {
|
||||
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取用户类型
|
||||
*/
|
||||
public String getUserTypeFromToken(String token) {
|
||||
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
|
||||
return claims.get("user_type").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查token是否过期
|
||||
*/
|
||||
public Boolean isTokenExpired(String token) {
|
||||
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
|
||||
Date expiration = claims.getExpiration();
|
||||
return expiration.before(new Date());
|
||||
}
|
||||
}
|
||||
437
java-backend/src/main/resources/api-docs.yaml
Normal file
437
java-backend/src/main/resources/api-docs.yaml
Normal file
@@ -0,0 +1,437 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: 爱鉴花小程序API文档
|
||||
description: 爱鉴花小程序后端API接口文档
|
||||
version: 1.0.0
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080/api/v1
|
||||
description: 本地开发服务器
|
||||
|
||||
paths:
|
||||
/auth/register:
|
||||
post:
|
||||
summary: 用户注册
|
||||
description: 用户注册接口,支持用户名、手机号、邮箱注册
|
||||
operationId: register
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 注册成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseUser'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'409':
|
||||
description: 用户已存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/auth/login:
|
||||
post:
|
||||
summary: 用户登录
|
||||
description: 用户登录接口,支持用户名、手机号、邮箱登录
|
||||
operationId: login
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 登录成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseUser'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'401':
|
||||
description: 用户名或密码错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/auth/me:
|
||||
get:
|
||||
summary: 获取当前用户信息
|
||||
description: 获取当前登录用户信息
|
||||
operationId: getCurrentUser
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseUser'
|
||||
'401':
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'404':
|
||||
description: 用户不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/upload:
|
||||
post:
|
||||
summary: 上传文件
|
||||
description: 上传文件接口,支持图片、文档等文件类型
|
||||
operationId: uploadFile
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
'200':
|
||||
description: 上传成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseUpload'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'401':
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'500':
|
||||
description: 服务器内部错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/upload/list:
|
||||
get:
|
||||
summary: 获取上传文件列表
|
||||
description: 获取当前用户上传的文件列表
|
||||
operationId: getUploads
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Upload'
|
||||
'401':
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/upload/{id}:
|
||||
delete:
|
||||
summary: 删除上传文件
|
||||
description: 删除指定ID的上传文件
|
||||
operationId: deleteUpload
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: 删除成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseString'
|
||||
'401':
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'404':
|
||||
description: 文件不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: 服务健康检查
|
||||
description: 检查服务运行状态
|
||||
operationId: healthCheck
|
||||
responses:
|
||||
'200':
|
||||
description: 服务正常
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseHealth'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
||||
schemas:
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
- phone
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: 用户名
|
||||
password:
|
||||
type: string
|
||||
minLength: 6
|
||||
description: 密码
|
||||
phone:
|
||||
type: string
|
||||
description: 手机号
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 邮箱
|
||||
userType:
|
||||
type: string
|
||||
default: farmer
|
||||
description: 用户类型
|
||||
|
||||
LoginRequest:
|
||||
type: object
|
||||
required:
|
||||
- login
|
||||
- password
|
||||
properties:
|
||||
login:
|
||||
type: string
|
||||
description: 登录凭证(用户名/手机号/邮箱)
|
||||
password:
|
||||
type: string
|
||||
description: 密码
|
||||
|
||||
ApiResponseMap:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 响应码
|
||||
message:
|
||||
type: string
|
||||
description: 响应消息
|
||||
data:
|
||||
type: object
|
||||
description: 响应数据
|
||||
|
||||
ApiResponseUser:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 响应码
|
||||
message:
|
||||
type: string
|
||||
description: 响应消息
|
||||
data:
|
||||
$ref: '#/components/schemas/UserResponse'
|
||||
|
||||
ApiResponseUpload:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 响应码
|
||||
message:
|
||||
type: string
|
||||
description: 响应消息
|
||||
data:
|
||||
$ref: '#/components/schemas/UploadResponse'
|
||||
|
||||
ApiResponseError:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 错误码
|
||||
message:
|
||||
type: string
|
||||
description: 错误消息
|
||||
data:
|
||||
type: object
|
||||
nullable: true
|
||||
description: 错误数据
|
||||
|
||||
UserResponse:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: 用户ID
|
||||
username:
|
||||
type: string
|
||||
description: 用户名
|
||||
phone:
|
||||
type: string
|
||||
description: 手机号
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 邮箱
|
||||
userType:
|
||||
type: string
|
||||
description: 用户类型
|
||||
avatarUrl:
|
||||
type: string
|
||||
description: 头像URL
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 创建时间
|
||||
lastLogin:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 最后登录时间
|
||||
|
||||
UploadResponse:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
description: 文件访问URL
|
||||
filename:
|
||||
type: string
|
||||
description: 存储文件名
|
||||
originalName:
|
||||
type: string
|
||||
description: 原始文件名
|
||||
size:
|
||||
type: integer
|
||||
description: 文件大小
|
||||
mimeType:
|
||||
type: string
|
||||
description: MIME类型
|
||||
uploadType:
|
||||
type: string
|
||||
description: 上传类型
|
||||
|
||||
PageResponseUpload:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 响应码
|
||||
message:
|
||||
type: string
|
||||
description: 响应消息
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Upload'
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
|
||||
Upload:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: 文件ID
|
||||
userId:
|
||||
type: integer
|
||||
description: 用户ID
|
||||
originalName:
|
||||
type: string
|
||||
description: 原始文件名
|
||||
storedName:
|
||||
type: string
|
||||
description: 存储文件名
|
||||
filePath:
|
||||
type: string
|
||||
description: 文件路径
|
||||
fileSize:
|
||||
type: integer
|
||||
description: 文件大小
|
||||
mimeType:
|
||||
type: string
|
||||
description: MIME类型
|
||||
fileType:
|
||||
type: string
|
||||
description: 文件类型
|
||||
uploadType:
|
||||
type: string
|
||||
description: 上传类型
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 创建时间
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 更新时间
|
||||
|
||||
Pagination:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
description: 当前页码
|
||||
limit:
|
||||
type: integer
|
||||
description: 每页数量
|
||||
total:
|
||||
type: integer
|
||||
description: 总记录数
|
||||
pages:
|
||||
type: integer
|
||||
description: 总页数
|
||||
47
java-backend/src/main/resources/application.yml
Normal file
47
java-backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
server:
|
||||
port: 3200
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: aijianhua-backend
|
||||
datasource:
|
||||
url: jdbc:mysql://129.211.213.226:9527/xlxumudata?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: aiotAiot123!
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 10000
|
||||
max-lifetime: 1800000
|
||||
connection-test-query: SELECT 1
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
format_sql: true
|
||||
open-in-view: false
|
||||
flyway:
|
||||
enabled: false
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 10MB
|
||||
max-request-size: 10MB
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: xluMubackendSecretKey2024!
|
||||
expiration: 604800000 # 7天
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.aijianhua: INFO
|
||||
org.springframework.web: INFO
|
||||
org.hibernate.SQL: INFO
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
@@ -0,0 +1,29 @@
|
||||
-- 创建用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(20) UNIQUE,
|
||||
email VARCHAR(100) UNIQUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
-- 创建文件上传表
|
||||
CREATE TABLE IF NOT EXISTS uploads (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_type VARCHAR(50) NOT NULL,
|
||||
file_size BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_phone ON users(phone);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_uploads_user_id ON uploads(user_id);
|
||||
437
java-backend/target/classes/api-docs.yaml
Normal file
437
java-backend/target/classes/api-docs.yaml
Normal file
@@ -0,0 +1,437 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: 爱鉴花小程序API文档
|
||||
description: 爱鉴花小程序后端API接口文档
|
||||
version: 1.0.0
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080/api/v1
|
||||
description: 本地开发服务器
|
||||
|
||||
paths:
|
||||
/auth/register:
|
||||
post:
|
||||
summary: 用户注册
|
||||
description: 用户注册接口,支持用户名、手机号、邮箱注册
|
||||
operationId: register
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 注册成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseUser'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'409':
|
||||
description: 用户已存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/auth/login:
|
||||
post:
|
||||
summary: 用户登录
|
||||
description: 用户登录接口,支持用户名、手机号、邮箱登录
|
||||
operationId: login
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 登录成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseUser'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'401':
|
||||
description: 用户名或密码错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/auth/me:
|
||||
get:
|
||||
summary: 获取当前用户信息
|
||||
description: 获取当前登录用户信息
|
||||
operationId: getCurrentUser
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseUser'
|
||||
'401':
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'404':
|
||||
description: 用户不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/upload:
|
||||
post:
|
||||
summary: 上传文件
|
||||
description: 上传文件接口,支持图片、文档等文件类型
|
||||
operationId: uploadFile
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
'200':
|
||||
description: 上传成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseUpload'
|
||||
'400':
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'401':
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'500':
|
||||
description: 服务器内部错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/upload/list:
|
||||
get:
|
||||
summary: 获取上传文件列表
|
||||
description: 获取当前用户上传的文件列表
|
||||
operationId: getUploads
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Upload'
|
||||
'401':
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/upload/{id}:
|
||||
delete:
|
||||
summary: 删除上传文件
|
||||
description: 删除指定ID的上传文件
|
||||
operationId: deleteUpload
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: 删除成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseString'
|
||||
'401':
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
'404':
|
||||
description: 文件不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseError'
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: 服务健康检查
|
||||
description: 检查服务运行状态
|
||||
operationId: healthCheck
|
||||
responses:
|
||||
'200':
|
||||
description: 服务正常
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponseHealth'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
||||
schemas:
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
- phone
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: 用户名
|
||||
password:
|
||||
type: string
|
||||
minLength: 6
|
||||
description: 密码
|
||||
phone:
|
||||
type: string
|
||||
description: 手机号
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 邮箱
|
||||
userType:
|
||||
type: string
|
||||
default: farmer
|
||||
description: 用户类型
|
||||
|
||||
LoginRequest:
|
||||
type: object
|
||||
required:
|
||||
- login
|
||||
- password
|
||||
properties:
|
||||
login:
|
||||
type: string
|
||||
description: 登录凭证(用户名/手机号/邮箱)
|
||||
password:
|
||||
type: string
|
||||
description: 密码
|
||||
|
||||
ApiResponseMap:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 响应码
|
||||
message:
|
||||
type: string
|
||||
description: 响应消息
|
||||
data:
|
||||
type: object
|
||||
description: 响应数据
|
||||
|
||||
ApiResponseUser:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 响应码
|
||||
message:
|
||||
type: string
|
||||
description: 响应消息
|
||||
data:
|
||||
$ref: '#/components/schemas/UserResponse'
|
||||
|
||||
ApiResponseUpload:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 响应码
|
||||
message:
|
||||
type: string
|
||||
description: 响应消息
|
||||
data:
|
||||
$ref: '#/components/schemas/UploadResponse'
|
||||
|
||||
ApiResponseError:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 错误码
|
||||
message:
|
||||
type: string
|
||||
description: 错误消息
|
||||
data:
|
||||
type: object
|
||||
nullable: true
|
||||
description: 错误数据
|
||||
|
||||
UserResponse:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: 用户ID
|
||||
username:
|
||||
type: string
|
||||
description: 用户名
|
||||
phone:
|
||||
type: string
|
||||
description: 手机号
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 邮箱
|
||||
userType:
|
||||
type: string
|
||||
description: 用户类型
|
||||
avatarUrl:
|
||||
type: string
|
||||
description: 头像URL
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 创建时间
|
||||
lastLogin:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 最后登录时间
|
||||
|
||||
UploadResponse:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
description: 文件访问URL
|
||||
filename:
|
||||
type: string
|
||||
description: 存储文件名
|
||||
originalName:
|
||||
type: string
|
||||
description: 原始文件名
|
||||
size:
|
||||
type: integer
|
||||
description: 文件大小
|
||||
mimeType:
|
||||
type: string
|
||||
description: MIME类型
|
||||
uploadType:
|
||||
type: string
|
||||
description: 上传类型
|
||||
|
||||
PageResponseUpload:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
description: 响应码
|
||||
message:
|
||||
type: string
|
||||
description: 响应消息
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Upload'
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
|
||||
Upload:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: 文件ID
|
||||
userId:
|
||||
type: integer
|
||||
description: 用户ID
|
||||
originalName:
|
||||
type: string
|
||||
description: 原始文件名
|
||||
storedName:
|
||||
type: string
|
||||
description: 存储文件名
|
||||
filePath:
|
||||
type: string
|
||||
description: 文件路径
|
||||
fileSize:
|
||||
type: integer
|
||||
description: 文件大小
|
||||
mimeType:
|
||||
type: string
|
||||
description: MIME类型
|
||||
fileType:
|
||||
type: string
|
||||
description: 文件类型
|
||||
uploadType:
|
||||
type: string
|
||||
description: 上传类型
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 创建时间
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 更新时间
|
||||
|
||||
Pagination:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
description: 当前页码
|
||||
limit:
|
||||
type: integer
|
||||
description: 每页数量
|
||||
total:
|
||||
type: integer
|
||||
description: 总记录数
|
||||
pages:
|
||||
type: integer
|
||||
description: 总页数
|
||||
47
java-backend/target/classes/application.yml
Normal file
47
java-backend/target/classes/application.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
server:
|
||||
port: 3200
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: aijianhua-backend
|
||||
datasource:
|
||||
url: jdbc:mysql://129.211.213.226:9527/xlxumudata?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: aiotAiot123!
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 10000
|
||||
max-lifetime: 1800000
|
||||
connection-test-query: SELECT 1
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
format_sql: true
|
||||
open-in-view: false
|
||||
flyway:
|
||||
enabled: false
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 10MB
|
||||
max-request-size: 10MB
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: xluMubackendSecretKey2024!
|
||||
expiration: 604800000 # 7天
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.aijianhua: INFO
|
||||
org.springframework.web: INFO
|
||||
org.hibernate.SQL: INFO
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
-- 创建用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(20) UNIQUE,
|
||||
email VARCHAR(100) UNIQUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
-- 创建文件上传表
|
||||
CREATE TABLE IF NOT EXISTS uploads (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_type VARCHAR(50) NOT NULL,
|
||||
file_size BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_phone ON users(phone);
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_uploads_user_id ON uploads(user_id);
|
||||
@@ -0,0 +1,28 @@
|
||||
com/aijianhua/backend/repository/UploadRepository.class
|
||||
com/aijianhua/backend/security/JwtAuthenticationFilter.class
|
||||
com/aijianhua/backend/controller/UploadController.class
|
||||
com/aijianhua/backend/service/AuthService.class
|
||||
com/aijianhua/backend/dto/UserResponse.class
|
||||
com/aijianhua/backend/dto/UploadResponse.class
|
||||
com/aijianhua/backend/util/JwtUtil.class
|
||||
com/aijianhua/backend/dto/PageResponse$Pagination.class
|
||||
com/aijianhua/backend/dto/ApiResponse.class
|
||||
com/aijianhua/backend/config/SecurityConfig.class
|
||||
com/aijianhua/backend/interceptor/JwtInterceptor.class
|
||||
com/aijianhua/backend/config/WebMvcConfig.class
|
||||
com/aijianhua/backend/Application.class
|
||||
com/aijianhua/backend/dto/RegisterRequest.class
|
||||
com/aijianhua/backend/service/UploadService.class
|
||||
com/aijianhua/backend/exception/GlobalExceptionHandler.class
|
||||
com/aijianhua/backend/security/JwtAuthenticationEntryPoint.class
|
||||
com/aijianhua/backend/entity/User.class
|
||||
com/aijianhua/backend/repository/UserRepository.class
|
||||
com/aijianhua/backend/exception/UnauthorizedException.class
|
||||
com/aijianhua/backend/config/SwaggerResourceConfig.class
|
||||
com/aijianhua/backend/dto/LoginRequest.class
|
||||
com/aijianhua/backend/dto/PageResponse.class
|
||||
com/aijianhua/backend/dto/PageResponse$PageData.class
|
||||
com/aijianhua/backend/entity/Upload.class
|
||||
com/aijianhua/backend/controller/HealthController.class
|
||||
com/aijianhua/backend/controller/AuthController.class
|
||||
com/aijianhua/backend/exception/BusinessException.class
|
||||
@@ -0,0 +1,26 @@
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/security/JwtAuthenticationFilter.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/config/SecurityConfig.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/exception/UnauthorizedException.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/dto/UserResponse.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/dto/ApiResponse.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/controller/UploadController.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/exception/BusinessException.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/entity/User.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/dto/UploadResponse.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/service/AuthService.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/util/JwtUtil.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/repository/UserRepository.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/interceptor/JwtInterceptor.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/repository/UploadRepository.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/controller/HealthController.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/security/JwtAuthenticationEntryPoint.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/config/WebMvcConfig.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/entity/Upload.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/Application.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/dto/LoginRequest.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/exception/GlobalExceptionHandler.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/dto/PageResponse.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/dto/RegisterRequest.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/config/SwaggerResourceConfig.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/controller/AuthController.java
|
||||
/Users/ainongkeji/code/vue/aijianhua/java-backend/src/main/java/com/aijianhua/backend/service/UploadService.java
|
||||
Reference in New Issue
Block a user