docs: 更新项目文档,完善需求和技术细节

This commit is contained in:
ylweng
2025-09-11 01:31:53 +08:00
parent e90c183c90
commit 59d24e74f4
70 changed files with 3487 additions and 6 deletions

View 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>

View 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>

View File

@@ -0,0 +1 @@
test content

View File

@@ -0,0 +1 @@
test content

View File

@@ -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

View File

@@ -9,6 +9,9 @@
- SQLite开发环境
- MySQL生产环境
- Redis
- Java
- Spring Boot
- Maven
## 文档结构
- 需求文档: 项目业务需求和功能说明

View File

@@ -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
View 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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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/");
}
}

View File

@@ -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/**");
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,7 @@
package com.aijianhua.backend.exception;
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) {
super(message);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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}");
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View 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: 总页数

View 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"

View File

@@ -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);

View 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: 总页数

View 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"

View File

@@ -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);

View File

@@ -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

View File

@@ -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