【Spring Boot 快速入门】七、阿里云 OSS 文件上传
这里写自定义目录标题
- 准备阿里云 OSS
- 参照官方 SDK 编写入门程序
- 案例数据准备
- 案例集成阿里云 OSS
- 前端测试代码
- app.js
- style.css
- index.html
- 效果图
准备阿里云 OSS
- 注册登录阿里云,然后点击控制台,在左上角菜单栏搜索对象存储 OSS,点击并开通
- 点击 Bucket 列表并新建一个 Bucket,填写 Bucket 名称和地域
- 点击头像下拉框,点击 AccessKey
- 创建 AccessKey,获取并保存 AccessKey ID 和 AccessKey Secret
- 记下 Bucket 名称、Endpoint、AccessKey ID 和 AccessKey Secret,后续配置要使用
参照官方 SDK 编写入门程序
SDK 文档地址:对象存储 SDK
在代码中引入依赖:
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.17.4</version>
</dependency>
如果使用的是Java 9及以上的版本,则需要添加以下JAXB相关依赖:
<dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version>
</dependency>
<dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>2.3.3</version>
</dependency>
使用文档中的示例代码:
import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;public class Demo {public static void main(String[] args) throws Exception {// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();// 填写Bucket名称,例如examplebucket。String bucketName = "examplebucket";// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。String objectName = "exampledir/exampleobject.txt";// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。String filePath= "D:\\localpath\\examplefile.txt";// 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。String region = "cn-hangzhou";// 创建OSSClient实例。// 当OSSClient实例不再使用时,调用shutdown方法以释放资源。ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();clientBuilderConfiguration.setSignatureVersion(SignVersion.V4); OSS ossClient = OSSClientBuilder.create().endpoint(endpoint).credentialsProvider(credentialsProvider).clientConfiguration(clientBuilderConfiguration).region(region) .build();try {InputStream inputStream = new FileInputStream(filePath);// 创建PutObjectRequest对象。PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);// 创建PutObject请求。PutObjectResult result = ossClient.putObject(putObjectRequest);} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (ClientException ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {if (ossClient != null) {ossClient.shutdown();}}}
}
案例数据准备
数据库表结构:
CREATE TABLE book_category (id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',name VARCHAR(100) NOT NULL COMMENT '分类名称',description VARCHAR(255) COMMENT '分类描述'
) COMMENT='图书分类表';CREATE TABLE book_info (id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '图书ID',book_name VARCHAR(200) NOT NULL COMMENT '书名',author VARCHAR(100) COMMENT '作者',isbn VARCHAR(20) UNIQUE COMMENT 'ISBN编号',publisher VARCHAR(100) COMMENT '出版社',publish_date DATE COMMENT '出版日期',category_id BIGINT NOT NULL COMMENT '分类ID',image VARCHAR(255) COMMENT '图书封面',description TEXT COMMENT '简介',create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',update_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',CONSTRAINT fk_info_category FOREIGN KEY (category_id) REFERENCES book_category(id)
) COMMENT='图书信息表';
图书分类的相关代码如下:
- 实体类 BookCategory:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookCategory {private Long id;private String name;private String description;
}
- controller 类 BookCategoryController:
import com.Scarletkite.pojo.BookCategory;
import com.Scarletkite.response.Result;
import com.Scarletkite.service.BookCategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;@RestController
public class BookCategoryController {@Autowiredprivate BookCategoryService bookCategoryService;// 查询所有图书分类@GetMapping("/getAllBookCategory")public Result getAllBookCategory() {List<BookCategory> bookCategoryList = bookCategoryService.getAllBookCategory();return Result.success(bookCategoryList);}// 删除图书分类@DeleteMapping("/deleteBookCategory/{id}")public Result deleteBookCategory(@PathVariable Long id) {bookCategoryService.deleteBookCategory(id);return Result.success();}// 新增图书分类@PostMapping("/addBookCategory")public Result addBookCategory(@RequestBody BookCategory bookCategory) {bookCategoryService.addBookCategory(bookCategory);return Result.success();}// 修改图书分类@PutMapping("/updateBookCategory{id}")public Result updateBookCategory(@PathVariable Long id) {bookCategoryService.updateBookCategory(id);return Result.success();}
}
- service 接口 BookCategoryService:
import com.Scarletkite.pojo.BookCategory;import java.util.List;public interface BookCategoryService {// 查询所有图书分类List<BookCategory> getAllBookCategory();// 添加图书类别void addBookCategory(BookCategory bookCategory);// 删除图书分类void deleteBookCategory(Long id);// 修改图书分类void updateBookCategory(Long id);
}
- service 接口实现类 BookCategoryServiceImp:
import com.Scarletkite.mapper.BookCategoryMapper;
import com.Scarletkite.pojo.BookCategory;
import com.Scarletkite.service.BookCategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class BookCategoryServiceImp implements BookCategoryService {@Autowiredprivate BookCategoryMapper bookCategoryMapper;// 查询所有图书分类@Overridepublic List<BookCategory> getAllBookCategory() {return bookCategoryMapper.getAllBookCategory();}// 新增图书分类@Overridepublic void addBookCategory(BookCategory bookCategory){bookCategoryMapper.addBookCategory(bookCategory);}// 删除图书分类@Overridepublic void deleteBookCategory(Long id) {bookCategoryMapper.deleteBookCategory(id);}// 修改图书分类@Overridepublic void updateBookCategory(Long id) {bookCategoryMapper.updateBookCategory(id);}
}
- mapper 接口 BookCategoryMapper:
import com.Scarletkite.pojo.BookCategory;
import org.apache.ibatis.annotations.*;import java.util.List;@Mapper
public interface BookCategoryMapper {// 查询所有图书分类@Select("select * from book_category")List<BookCategory> getAllBookCategory();// 新增图书分类@Insert("insert into book_category (name, description) values (#{name}, #{description})")void addBookCategory(BookCategory bookCategory);// 删除图书分类@Delete("delete from book_category where id = #{id}")void deleteBookCategory(Long id);// 修改图书分类@Update("update book_category set name = #{name}, description = #{description} where id = #{id}")void updateBookCategory(Long id);
}
图书信息相关代码如下:
- 实体类 BookInfo:
import java.time.LocalDateTime;
import java.util.Date;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookInfo {private Long id;private String bookName;private String author;private String isbn;private String publisher;private Date publishDate;private Long categoryId;private String image;private String description;private LocalDateTime createTime;private LocalDateTime updateTime;
}
- controller 类:
import com.Scarletkite.pojo.BookInfo;
import com.Scarletkite.pojo.PageBean;
import com.Scarletkite.response.Result;
import com.Scarletkite.service.BookInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.Date;@RestController
public class BookInfoController {@Autowiredprivate BookInfoService bookInfoService;// 新增图书信息@PostMapping("/addBookInfo")public Result addBookInfo(@RequestBody BookInfo bookInfo) {bookInfoService.addBookInfo(bookInfo);return Result.success();}// 查询所有图书信息@GetMapping("/getAllBookInfo")public Result getAllBookInfo(@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "10") Integer pageSize,@RequestParam(required = false) String bookName,@RequestParam(required = false) String author,@RequestParam(required = false) String isbn,@RequestParam(required = false) String publisher,@RequestParam(required = false) Date publishDate,@RequestParam(required = false) Long categoryId) {PageBean pageBean = bookInfoService.getAllBookInfo(page, pageSize, bookName, author, isbn, publisher, publishDate, categoryId);return Result.success(pageBean);}// 根据id回显图书信息@GetMapping("/getBookInfoById/{id}")public Result getBookInfoById(@PathVariable Long id) {BookInfo bookInfo = bookInfoService.getBookInfoById(id);return Result.success(bookInfo);}// 更新图书信息@PutMapping("/updateBookInfo")public Result updateBookInfo(@RequestBody BookInfo bookInfo) {//bookInfo.setId(id);bookInfoService.updateBookInfo(bookInfo);return Result.success();}// 删除图书信息@DeleteMapping("/deleteBookInfo/{id}")public Result deleteBookInfo(@PathVariable Long id) {bookInfoService.deleteBookInfo(id);return Result.success();}
}
- service 接口 BookInfoService:
import com.Scarletkite.pojo.BookInfo;
import com.Scarletkite.pojo.PageBean;
import java.util.Date;public interface BookInfoService {// 新增图书信息void addBookInfo(BookInfo bookInfo);// 查询所有图书信息PageBean getAllBookInfo(Integer page, Integer pageSize, String bookName, String author,String isbn, String publisher, Date publishDate, Long categoryId);// 根据id查询图书信息BookInfo getBookInfoById(Long id);// 更新图书信息void updateBookInfo(BookInfo bookInfo);// 删除图书信息void deleteBookInfo(Long id);
}
- service 接口实现类 BookInfoServiceImp:
import com.Scarletkite.mapper.BookInfoMapper;
import com.Scarletkite.pojo.BookInfo;
import com.Scarletkite.pojo.PageBean;
import com.Scarletkite.service.BookInfoService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;@Service
public class BookInfoServiceImp implements BookInfoService {@Autowiredprivate BookInfoMapper bookInfoMapper;// 新增图书信息@Overridepublic void addBookInfo(BookInfo bookInfo) {bookInfo.setCreateTime(LocalDateTime.now());bookInfo.setUpdateTime(LocalDateTime.now());bookInfoMapper.addBookInfo(bookInfo);}// 查询所有图书信息@Overridepublic PageBean getAllBookInfo(Integer page, Integer pageSize, String bookName, String author,String isbn, String publisher, Date publishDate, Long categoryId) {// 1. 设置分页参数PageHelper.startPage(page, pageSize);// 2. 执行查询List<BookInfo> bookInfoList = bookInfoMapper.getAllBookInfo(bookName, author, isbn, publisher, publishDate, categoryId);Page<BookInfo> p = (Page<BookInfo>) bookInfoList;// 3. 封装PageBeanPageBean pageBean = new PageBean(p.getTotal(), p.getResult());return pageBean;}// 根据id查询图书信息@Overridepublic BookInfo getBookInfoById(Long id) {return bookInfoMapper.getBookInfoById(id);}// 更新图书信息@Overridepublic void updateBookInfo(BookInfo bookInfo) {bookInfo.setUpdateTime(LocalDateTime.now());bookInfoMapper.updateBookInfo(bookInfo);}// 删除图书信息@Overridepublic void deleteBookInfo(Long id) {bookInfoMapper.deleteBookInfo(id);}
}
- mapper 接口:
import com.Scarletkite.pojo.BookInfo;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;import java.util.Date;
import java.util.List;@Mapper
public interface BookInfoMapper {// 新增图书信息@Insert("insert into book_info (book_name, author, isbn, publisher, " +"publish_date, category_id, image, description, create_time, update_time) " +"values (#{bookName}, #{author}, #{isbn}, #{publisher}, #{publishDate}, " +"#{categoryId}, #{image}, #{description}, #{createTime}, #{updateTime})")void addBookInfo(BookInfo bookInfo);// 查询所有图书信息//@Select("select * from book_info")List<BookInfo> getAllBookInfo(String bookName, String author, String isbn,String publisher, Date publishDate, Long categoryId);// 根据id查询图书信息@Select("select * from book_info where id = #{id}")BookInfo getBookInfoById(Long id);// 更新图书信息void updateBookInfo(BookInfo bookInfo);// 删除图书信息@Delete("delete from book_info where id = #{id}")void deleteBookInfo(Long id);
}
- XML 映射文件 BookInfoMapper:
<update id="updateBookInfo">update book_info<set><if test="bookName != null">book_name = #{bookName},</if><if test="author != null">author = #{author},</if><if test="publisher != null">publisher = #{publisher},</if><if test="publishDate != null">publish_date = #{publishDate},</if><if test="categoryId != null">category_id = #{categoryId},</if><if test="image != null">image = #{image},</if><if test="description != null">description = #{description},</if>update_time = #{updateTime}</set>where id = #{id}
</update><select id="getAllBookInfo" resultType="com.Scarletkite.pojo.BookInfo">select * from book_info<where><if test="bookName != null and bookName != ''">book_name like concat('%', #{bookName}, '%')</if><if test="author != null and author != ''">and author like concat('%', #{author}, '%')</if><if test="isbn != null and isbn != ''">and isbn like concat('%', #{isbn}, '%')</if><if test="publisher != null and publisher != ''">and publisher like concat('%', #{publisher}, '%')</if><if test="publishDate != null">and publish_date = #{publishDate}</if><if test="categoryId != null">and category_id = #{categoryId}</if></where>order by id desc
</select>
案例集成阿里云 OSS
以下是上传功能的逻辑图:
- 首先上传图片,然后通过 UploadController 上传到阿里云 OSS 中,并返回访问图片的 URL
- 点击添加,通过 BookInfoController 来进行新增操作
将文档中的示例代码改为一个工具类 AliOSSUtils:
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import java.io.InputStream;
import java.util.UUID;@Component
public class AliOSSUtils {private String endpoint = "";private String accessKeyId = "";private String accessKeySecret = "";private String bucketName = "";public String upload(MultipartFile file) throws Exception {// 获取上传文件的输入流InputStream inputStream = file.getInputStream();// 避免文件覆盖String originalFilename = file.getOriginalFilename();String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));// 上传文件到OSSOSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);ossClient.putObject(bucketName, fileName, inputStream);//文件访问路径String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;// 关闭ossClientossClient.shutdown();return url;// 把上传到oss的路径返回}
}
新增一个 controller 类 UploadController:
import com.Scarletkite.response.Result;
import com.Scarletkite.utils.AliOSSUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;@RestController
public class UploadController {@Autowiredprivate AliOSSUtils aliOSSUtils;@PostMapping("/upload")public Result upload(MultipartFile image) throws Exception {// 调用阿里云OSS工具类来进行上传String url = aliOSSUtils.upload(image);return Result.success(url);}
}
这样就实现了文件的上传功能
前端测试代码
以下是用来对后端进行测试用的前端代码
app.js
// 全局变量
let currentPage = 1;
let pageSize = 10;
let categories = [];// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {loadCategories();loadBooks();
});// 显示不同的功能区域
function showSection(section) {// 隐藏所有区域document.getElementById('books-section').style.display = 'none';document.getElementById('categories-section').style.display = 'none';// 显示选中的区域document.getElementById(section + '-section').style.display = 'block';// 更新导航状态document.querySelectorAll('.nav-link').forEach(link => {link.classList.remove('active');});event.target.classList.add('active');// 根据选中的区域加载数据if (section === 'books') {loadBooks();} else if (section === 'categories') {loadCategories();}
}// 加载图书分类
async function loadCategories() {try {console.log('请求分类数据...');const response = await fetch('/getAllBookCategory');console.log('分类响应状态:', response.status);if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}const result = await response.json();console.log('分类响应数据:', result);if (result.code === 200) {categories = result.data || [];updateCategorySelects();updateCategoriesTable();} else {showAlert('加载分类失败: ' + result.message, 'danger');}} catch (error) {console.error('加载分类失败:', error);showAlert('加载分类失败: ' + error.message, 'danger');}
}// 更新分类下拉选择框
function updateCategorySelects() {const selects = [document.getElementById('searchCategory'),document.querySelector('select[name="categoryId"]'),document.getElementById('editCategoryId')];selects.forEach(select => {if (select) {// 保留第一个选项const firstOption = select.firstElementChild;select.innerHTML = '';select.appendChild(firstOption);// 添加分类选项categories.forEach(category => {const option = document.createElement('option');option.value = category.id;option.textContent = category.name;select.appendChild(option);});}});
}// 更新分类表格
function updateCategoriesTable() {const tbody = document.getElementById('categoriesTableBody');tbody.innerHTML = '';categories.forEach(category => {const row = document.createElement('tr');row.innerHTML = `<td>${category.id}</td><td>${category.name}</td><td>${category.description || '-'}</td><td><button class="btn btn-sm btn-outline-danger" onclick="deleteCategory(${category.id})"><i class="bi bi-trash"></i> 删除</button></td>`;tbody.appendChild(row);});
}// 加载图书列表
async function loadBooks(page = 1) {try {currentPage = page;const bookName = document.getElementById('searchBookName')?.value || '';const author = document.getElementById('searchAuthor')?.value || '';const categoryId = document.getElementById('searchCategory')?.value || '';const params = new URLSearchParams({page: currentPage,pageSize: pageSize});// 只添加非空参数if (bookName.trim()) {params.append('bookName', bookName.trim());}if (author.trim()) {params.append('author', author.trim());}if (categoryId) {params.append('categoryId', categoryId);}console.log('请求URL:', `/getAllBookInfo?${params}`);const response = await fetch(`/getAllBookInfo?${params}`);console.log('响应状态:', response.status);if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}const result = await response.json();console.log('响应数据:', result);if (result.code === 200) {updateBooksTable(result.data.rows || []);updatePagination(result.data.total, currentPage, pageSize);} else {showAlert('加载图书失败: ' + result.message, 'danger');}} catch (error) {console.error('加载图书失败:', error);showAlert('加载图书失败: ' + error.message, 'danger');}
}// 更新图书表格
// 处理图片URL的辅助函数
function processImageUrl(imageUrl) {// 添加调试信息console.log('原始图片URL:', imageUrl);if (!imageUrl) {console.log('图片URL为空,使用占位符');return 'https://via.placeholder.com/60x80?text=No+Image';}// 如果是完整的HTTP/HTTPS URL(OSS),直接使用if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {console.log('检测到完整URL(OSS),直接使用:', imageUrl);return imageUrl;}// 如果是相对路径(本地存储),确保以/开头if (!imageUrl.startsWith('/')) {imageUrl = '/' + imageUrl;}console.log('处理后的本地路径URL:', imageUrl);return imageUrl;
}function updateBooksTable(books) {const tbody = document.getElementById('booksTableBody');tbody.innerHTML = '';books.forEach(book => {const category = categories.find(c => c.id === book.categoryId);const categoryName = category ? category.name : '-';const publishDate = book.publishDate ? new Date(book.publishDate).toLocaleDateString() : '-';const imageUrl = processImageUrl(book.image);const row = document.createElement('tr');row.innerHTML = `<td><img src="${imageUrl}" alt="封面" class="book-image" onerror="this.src='https://via.placeholder.com/60x80?text=No+Image'"></td><td>${book.bookName}</td><td>${book.author}</td><td>${book.isbn}</td><td>${book.publisher}</td><td>${publishDate}</td><td>${categoryName}</td><td><button class="btn btn-sm btn-outline-primary me-1" onclick="editBook(${book.id})"><i class="bi bi-pencil"></i></button><button class="btn btn-sm btn-outline-danger" onclick="deleteBook(${book.id})"><i class="bi bi-trash"></i></button></td>`;tbody.appendChild(row);});
}// 更新分页
function updatePagination(total, pageNum, pageSize) {const pagination = document.getElementById('booksPagination');pagination.innerHTML = '';const totalPages = Math.ceil(total / pageSize);// 上一页const prevLi = document.createElement('li');prevLi.className = `page-item ${pageNum <= 1 ? 'disabled' : ''}`;prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadBooks(${pageNum - 1})">上一页</a>`;pagination.appendChild(prevLi);// 页码for (let i = Math.max(1, pageNum - 2); i <= Math.min(totalPages, pageNum + 2); i++) {const li = document.createElement('li');li.className = `page-item ${i === pageNum ? 'active' : ''}`;li.innerHTML = `<a class="page-link" href="#" onclick="loadBooks(${i})">${i}</a>`;pagination.appendChild(li);}// 下一页const nextLi = document.createElement('li');nextLi.className = `page-item ${pageNum >= totalPages ? 'disabled' : ''}`;nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadBooks(${pageNum + 1})">下一页</a>`;pagination.appendChild(nextLi);
}// 搜索图书
function searchBooks() {loadBooks(1);
}// 添加图书
async function addBook() {const form = document.getElementById('addBookForm');// 验证必填字段if (!form.bookName.value.trim()) {alert('请输入图书名称!');return;}if (!form.author.value.trim()) {alert('请输入作者!');return;}if (!form.isbn.value.trim()) {alert('请输入ISBN!');return;}if (!form.publisher.value.trim()) {alert('请输入出版社!');return;}if (!form.categoryId.value) {alert('请选择图书分类!');return;}try {let imageUrl = null;// 如果有图片文件,先上传图片if (form.image.files[0]) {const imageFormData = new FormData();imageFormData.append('image', form.image.files[0]);const uploadResponse = await fetch('/upload', {method: 'POST',body: imageFormData});if (!uploadResponse.ok) {throw new Error('图片上传失败');}const uploadResult = await uploadResponse.json();if (uploadResult.code === 200) {imageUrl = uploadResult.data;} else {throw new Error('图片上传失败: ' + uploadResult.message);}}// 构建图书信息JSON对象const bookData = {bookName: form.bookName.value.trim(),author: form.author.value.trim(),isbn: form.isbn.value.trim(),publisher: form.publisher.value.trim(),publishDate: form.publishDate.value || null,categoryId: parseInt(form.categoryId.value),description: form.description.value || '',image: imageUrl};console.log('发送添加图书请求...', bookData);const response = await fetch('/addBookInfo', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(bookData)});console.log('响应状态:', response.status);if (!response.ok) {const errorText = await response.text();console.log('错误响应内容:', errorText);throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);}const result = await response.json();console.log('响应数据:', result);if (result.code === 200) {showAlert('添加图书成功!', 'success');bootstrap.Modal.getInstance(document.getElementById('addBookModal')).hide();form.reset();loadBooks();} else {showAlert('添加图书失败: ' + result.message, 'danger');}} catch (error) {console.error('添加图书失败:', error);showAlert('添加图书失败: ' + error.message, 'danger');}
}// 编辑图书
async function editBook(id) {try {// 使用后端接口获取图书详情const response = await fetch(`/getBookInfoById/${id}`);const result = await response.json();if (result.code === 200) {const bookInfo = result.data;console.log('获取到的图书信息:', bookInfo);// 填充编辑表单document.getElementById('editBookId').value = id;document.getElementById('editBookName').value = bookInfo.bookName || '';document.getElementById('editAuthor').value = bookInfo.author || '';document.getElementById('editIsbn').value = bookInfo.isbn || '';document.getElementById('editPublisher').value = bookInfo.publisher || '';// 处理日期格式if (bookInfo.publishDate) {const date = new Date(bookInfo.publishDate);const formattedDate = date.toISOString().split('T')[0];document.getElementById('editPublishDate').value = formattedDate;} else {document.getElementById('editPublishDate').value = '';}// 先更新分类下拉框选项updateCategorySelects();// 然后设置分类值和描述document.getElementById('editCategoryId').value = bookInfo.categoryId || '';document.getElementById('editDescription').value = bookInfo.description || '';// 清空文件输入框document.getElementById('editImage').value = '';// 处理图片显示const imagePreview = document.getElementById('editImagePreview');const noImageText = document.getElementById('editNoImage');if (bookInfo.image) {// 使用统一的图片URL处理函数imagePreview.src = processImageUrl(bookInfo.image);imagePreview.style.display = 'block';noImageText.style.display = 'none';} else {imagePreview.style.display = 'none';noImageText.style.display = 'block';}// 显示编辑模态框new bootstrap.Modal(document.getElementById('editBookModal')).show();} else {showAlert('获取图书信息失败: ' + result.message, 'danger');}} catch (error) {console.error('获取图书信息失败:', error);showAlert('获取图书信息失败: ' + error.message, 'danger');}
}// 更新图书
async function updateBook() {const id = document.getElementById('editBookId').value;const imageInput = document.getElementById('editImage');try {let imageUrl = null;// 如果用户选择了新的图片文件,先上传图片if (imageInput.files[0]) {const imageFormData = new FormData();imageFormData.append('image', imageInput.files[0]);const uploadResponse = await fetch('/upload', {method: 'POST',body: imageFormData});if (!uploadResponse.ok) {throw new Error('图片上传失败');}const uploadResult = await uploadResponse.json();if (uploadResult.code === 200) {imageUrl = uploadResult.data;} else {throw new Error('图片上传失败: ' + uploadResult.message);}}// 构建更新数据const bookData = {id: parseInt(id),bookName: document.getElementById('editBookName').value,author: document.getElementById('editAuthor').value,isbn: document.getElementById('editIsbn').value,publisher: document.getElementById('editPublisher').value,publishDate: document.getElementById('editPublishDate').value || null,categoryId: parseInt(document.getElementById('editCategoryId').value),description: document.getElementById('editDescription').value};// 如果上传了新图片,则更新图片URLif (imageUrl) {bookData.image = imageUrl;}console.log('发送更新图书请求...', bookData);const response = await fetch('/updateBookInfo', {method: 'PUT',headers: {'Content-Type': 'application/json'},body: JSON.stringify(bookData)});const result = await response.json();if (result.code === 200) {showAlert('更新图书成功!', 'success');bootstrap.Modal.getInstance(document.getElementById('editBookModal')).hide();loadBooks();} else {showAlert('更新图书失败: ' + result.message, 'danger');}} catch (error) {console.error('更新图书失败:', error);showAlert('更新图书失败: ' + error.message, 'danger');}
}// 删除图书
async function deleteBook(id) {if (!confirm('确定要删除这本图书吗?')) {return;}try {const response = await fetch(`/deleteBookInfo/${id}`, {method: 'DELETE'});const result = await response.json();if (result.code === 200) {showAlert('删除图书成功!', 'success');loadBooks();} else {showAlert('删除图书失败: ' + result.message, 'danger');}} catch (error) {console.error('删除图书失败:', error);showAlert('删除图书失败: ' + error.message, 'danger');}
}// 添加分类
async function addCategory() {const form = document.getElementById('addCategoryForm');const formData = new FormData(form);const categoryData = {name: formData.get('name'),description: formData.get('description')};try {const response = await fetch('/addBookCategory', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(categoryData)});const result = await response.json();if (result.code === 200) {showAlert('添加分类成功!', 'success');bootstrap.Modal.getInstance(document.getElementById('addCategoryModal')).hide();form.reset();loadCategories();} else {showAlert('添加分类失败: ' + result.message, 'danger');}} catch (error) {console.error('添加分类失败:', error);showAlert('添加分类失败: ' + error.message, 'danger');}
}// 删除分类
async function deleteCategory(id) {if (!confirm('确定要删除这个分类吗?')) {return;}try {const response = await fetch(`/deleteBookCategory/${id}`, {method: 'DELETE'});const result = await response.json();if (result.code === 200) {showAlert('删除分类成功!', 'success');loadCategories();} else {showAlert('删除分类失败: ' + result.message, 'danger');}} catch (error) {console.error('删除分类失败:', error);showAlert('删除分类失败: ' + error.message, 'danger');}
}// 图片预览功能
document.addEventListener('DOMContentLoaded', function() {// 添加图书时的图片预览功能const imageInput = document.getElementById('image');if (imageInput) {imageInput.addEventListener('change', function(e) {const file = e.target.files[0];const preview = document.getElementById('imagePreview');if (file) {const reader = new FileReader();reader.onload = function(e) {preview.src = e.target.result;preview.style.display = 'block';};reader.readAsDataURL(file);} else {preview.style.display = 'none';}});}// 编辑图书时的图片预览功能const editImageInput = document.getElementById('editImage');if (editImageInput) {editImageInput.addEventListener('change', function(e) {const file = e.target.files[0];const preview = document.getElementById('editImagePreview');const noImageText = document.getElementById('editNoImage');if (file) {const reader = new FileReader();reader.onload = function(e) {preview.src = e.target.result;preview.style.display = 'block';if (noImageText) {noImageText.style.display = 'none';}};reader.readAsDataURL(file);}});}
});// 显示提示消息
function showAlert(message, type = 'info') {// 创建提示框const alertDiv = document.createElement('div');alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';alertDiv.innerHTML = `${message}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;document.body.appendChild(alertDiv);// 3秒后自动消失setTimeout(() => {if (alertDiv.parentNode) {alertDiv.remove();}}, 3000);
}
style.css
/* 自定义样式 */
:root {--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);--primary-color: #667eea;--secondary-color: #764ba2;--success-color: #28a745;--danger-color: #dc3545;--warning-color: #ffc107;--info-color: #17a2b8;--light-color: #f8f9fa;--dark-color: #343a40;
}/* 全局样式 */
body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;background-color: #f5f6fa;
}/* 侧边栏样式 */
.sidebar {min-height: 100vh;background: var(--primary-gradient);box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}.sidebar h4 {font-weight: 600;text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}.nav-link {color: rgba(255,255,255,0.8) !important;transition: all 0.3s ease;border-radius: 8px;margin-bottom: 5px;font-weight: 500;
}.nav-link:hover, .nav-link.active {color: white !important;background-color: rgba(255,255,255,0.15);transform: translateX(5px);
}.nav-link i {width: 20px;text-align: center;
}/* 卡片样式 */
.card {border: none;box-shadow: 0 4px 15px rgba(0,0,0,0.08);transition: all 0.3s ease;border-radius: 12px;
}.card:hover {transform: translateY(-3px);box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}.card-header {background: var(--primary-gradient);color: white;border-radius: 12px 12px 0 0 !important;font-weight: 600;
}/* 按钮样式 */
.btn-primary {background: var(--primary-gradient);border: none;border-radius: 8px;font-weight: 500;transition: all 0.3s ease;
}.btn-primary:hover {background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);transform: translateY(-2px);box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}.btn-outline-primary {border-color: var(--primary-color);color: var(--primary-color);border-radius: 6px;transition: all 0.3s ease;
}.btn-outline-primary:hover {background: var(--primary-gradient);border-color: var(--primary-color);transform: translateY(-1px);
}.btn-outline-danger {border-radius: 6px;transition: all 0.3s ease;
}.btn-outline-danger:hover {transform: translateY(-1px);
}/* 表格样式 */
.table {border-radius: 8px;overflow: hidden;
}.table thead th {background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);border: none;font-weight: 600;color: var(--dark-color);padding: 15px;
}.table tbody tr {transition: all 0.2s ease;
}.table tbody tr:hover {background-color: rgba(102, 126, 234, 0.05);transform: scale(1.01);
}.table tbody td {padding: 12px 15px;vertical-align: middle;border-color: #f1f3f4;
}/* 图书封面样式 */
.book-image {width: 60px;height: 80px;object-fit: cover;border-radius: 6px;box-shadow: 0 2px 8px rgba(0,0,0,0.15);transition: transform 0.2s ease;
}.book-image:hover {transform: scale(1.1);
}/* 表格容器 */
.table-container {max-height: 600px;overflow-y: auto;border-radius: 8px;
}.table-container::-webkit-scrollbar {width: 6px;
}.table-container::-webkit-scrollbar-track {background: #f1f1f1;border-radius: 3px;
}.table-container::-webkit-scrollbar-thumb {background: var(--primary-color);border-radius: 3px;
}.table-container::-webkit-scrollbar-thumb:hover {background: var(--secondary-color);
}/* 分页样式 */
.pagination .page-link {border: none;color: var(--primary-color);border-radius: 6px;margin: 0 2px;transition: all 0.2s ease;
}.pagination .page-link:hover {background: var(--primary-gradient);color: white;transform: translateY(-1px);
}.pagination .page-item.active .page-link {background: var(--primary-gradient);border-color: var(--primary-color);
}/* 模态框样式 */
.modal-content {border: none;border-radius: 12px;box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}.modal-header {background: var(--primary-gradient);color: white;border-radius: 12px 12px 0 0;border-bottom: none;
}.modal-header .btn-close {filter: invert(1);
}.modal-body {padding: 30px;
}.modal-footer {border-top: 1px solid #e9ecef;padding: 20px 30px;
}/* 表单样式 */
.form-control, .form-select {border: 2px solid #e9ecef;border-radius: 8px;padding: 12px 15px;transition: all 0.3s ease;
}.form-control:focus, .form-select:focus {border-color: var(--primary-color);box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}.form-label {font-weight: 600;color: var(--dark-color);margin-bottom: 8px;
}/* 搜索区域样式 */
.search-card {background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);border: 1px solid #e9ecef;
}/* 提示框样式 */
.alert {border: none;border-radius: 8px;font-weight: 500;box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}.alert-success {background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);color: #155724;
}.alert-danger {background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);color: #721c24;
}.alert-info {background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%);color: #0c5460;
}/* 响应式设计 */
@media (max-width: 768px) {.sidebar {min-height: auto;}.table-container {max-height: 400px;}.modal-dialog {margin: 10px;}.card {margin-bottom: 20px;}
}/* 加载动画 */
.loading {display: inline-block;width: 20px;height: 20px;border: 3px solid rgba(255,255,255,.3);border-radius: 50%;border-top-color: #fff;animation: spin 1s ease-in-out infinite;
}@keyframes spin {to { transform: rotate(360deg); }
}/* 空状态样式 */
.empty-state {text-align: center;padding: 60px 20px;color: #6c757d;
}.empty-state i {font-size: 4rem;margin-bottom: 20px;opacity: 0.5;
}.empty-state h5 {margin-bottom: 10px;color: #495057;
}/* 统计卡片 */
.stats-card {background: var(--primary-gradient);color: white;border-radius: 12px;padding: 20px;text-align: center;
}.stats-card h3 {font-size: 2.5rem;font-weight: 700;margin-bottom: 5px;
}.stats-card p {margin: 0;opacity: 0.9;
}
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>图书管理系统测试界面</title><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet"><link href="style.css" rel="stylesheet">
</head>
<body><div class="container-fluid"><div class="row"><!-- 侧边栏 --><div class="col-md-2 sidebar p-3"><h4 class="text-white mb-4"><i class="bi bi-book"></i> 图书管理系统</h4><ul class="nav nav-pills flex-column"><li class="nav-item mb-2"><a class="nav-link active" href="#" onclick="showSection('books')"><i class="bi bi-book-fill me-2"></i>图书信息管理</a></li><li class="nav-item mb-2"><a class="nav-link" href="#" onclick="showSection('categories')"><i class="bi bi-tags-fill me-2"></i>分类管理</a></li></ul></div><!-- 主内容区 --><div class="col-md-10 p-4"><!-- 图书管理区域 --><div id="books-section"><div class="d-flex justify-content-between align-items-center mb-4"><h2><i class="bi bi-book-fill text-primary me-2"></i>图书管理</h2><button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addBookModal"><i class="bi bi-plus-circle me-1"></i>添加图书</button></div><!-- 搜索过滤器 --><div class="card mb-4"><div class="card-body"><div class="row g-3"><div class="col-md-3"><input type="text" class="form-control" id="searchBookName" placeholder="图书名称"></div><div class="col-md-3"><input type="text" class="form-control" id="searchAuthor" placeholder="作者"></div><div class="col-md-3"><select class="form-select" id="searchCategory"><option value="">选择分类</option></select></div><div class="col-md-3"><button class="btn btn-outline-primary w-100" onclick="searchBooks()"><i class="bi bi-search me-1"></i>搜索</button></div></div></div></div><!-- 图书列表 --><div class="card"><div class="card-body"><div class="table-container"><table class="table table-hover"><thead class="table-light"><tr><th>封面</th><th>书名</th><th>作者</th><th>ISBN</th><th>出版社</th><th>出版日期</th><th>分类</th><th>操作</th></tr></thead><tbody id="booksTableBody"><!-- 动态加载图书数据 --></tbody></table></div><!-- 分页 --><nav aria-label="Page navigation" class="mt-3"><ul class="pagination justify-content-center" id="booksPagination"><!-- 动态生成分页 --></ul></nav></div></div></div><!-- 分类管理区域 --><div id="categories-section" style="display: none;"><div class="d-flex justify-content-between align-items-center mb-4"><h2><i class="bi bi-tags-fill text-primary me-2"></i>分类管理</h2><button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCategoryModal"><i class="bi bi-plus-circle me-1"></i>添加分类</button></div><!-- 分类列表 --><div class="card"><div class="card-body"><table class="table table-hover"><thead class="table-light"><tr><th>ID</th><th>分类名称</th><th>描述</th><th>操作</th></tr></thead><tbody id="categoriesTableBody"><!-- 动态加载分类数据 --></tbody></table></div></div></div></div></div></div><!-- 添加图书模态框 --><div class="modal fade" id="addBookModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">添加图书</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="addBookForm" enctype="multipart/form-data"><div class="row g-3"><div class="col-md-6"><label class="form-label">图书名称 *</label><input type="text" class="form-control" name="bookName" required></div><div class="col-md-6"><label class="form-label">作者 *</label><input type="text" class="form-control" name="author" required></div><div class="col-md-6"><label class="form-label">ISBN *</label><input type="text" class="form-control" name="isbn" required></div><div class="col-md-6"><label class="form-label">出版社 *</label><input type="text" class="form-control" name="publisher" required></div><div class="col-md-6"><label class="form-label">出版日期</label><input type="date" class="form-control" name="publishDate"></div><div class="col-md-6"><label class="form-label">分类 *</label><select class="form-select" name="categoryId" required><option value="">选择分类</option></select></div><div class="col-12"><label class="form-label">封面图片</label><input type="file" class="form-control" name="image" accept="image/*"></div><div class="col-12"><label class="form-label">描述</label><textarea class="form-control" name="description" rows="3"></textarea></div></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" onclick="addBook()">添加</button></div></div></div></div><!-- 编辑图书模态框 --><div class="modal fade" id="editBookModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">编辑图书</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="editBookForm"><input type="hidden" id="editBookId"><div class="row g-4"><!-- 左侧:当前封面 --><div class="col-md-4"><div class="text-center"><label class="form-label fw-bold">当前封面</label><div id="editCurrentImage" class="border rounded p-3" style="min-height: 300px; display: flex; align-items: center; justify-content: center;"><img id="editImagePreview" src="" alt="图书封面" class="img-fluid rounded" style="max-width: 100%; max-height: 280px; display: none;"><p id="editNoImage" class="text-muted mb-0">暂无封面图片</p></div></div></div><!-- 右侧:表单字段 --><div class="col-md-8"><div class="row g-3"><div class="col-md-6"><label class="form-label">图书名称 *</label><input type="text" class="form-control" id="editBookName" required></div><div class="col-md-6"><label class="form-label">作者 *</label><input type="text" class="form-control" id="editAuthor" required></div><div class="col-md-6"><label class="form-label">ISBN *</label><input type="text" class="form-control" id="editIsbn" required></div><div class="col-md-6"><label class="form-label">出版社 *</label><input type="text" class="form-control" id="editPublisher" required></div><div class="col-md-6"><label class="form-label">出版日期</label><input type="date" class="form-control" id="editPublishDate"></div><div class="col-md-6"><label class="form-label">分类 *</label><select class="form-select" id="editCategoryId" required><option value="">选择分类</option></select></div><div class="col-12"><label class="form-label">更换封面图片</label><input type="file" class="form-control" id="editImage" accept="image/*"><small class="form-text text-muted">如需更换封面,请选择新的图片文件</small></div><div class="col-12"><label class="form-label">描述</label><textarea class="form-control" id="editDescription" rows="4"></textarea></div></div></div></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" onclick="updateBook()">保存</button></div></div></div></div><!-- 添加分类模态框 --><div class="modal fade" id="addCategoryModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">添加分类</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><form id="addCategoryForm"><div class="mb-3"><label class="form-label">分类名称 *</label><input type="text" class="form-control" name="name" required></div><div class="mb-3"><label class="form-label">描述</label><textarea class="form-control" name="description" rows="3"></textarea></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" onclick="addCategory()">添加</button></div></div></div></div><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script><script src="app.js"></script>
</body>
</html>
效果图