智能协同云图库: 引入分类标签表,无需中间关联表
前言
目前代码中(看完第四期)的标签是写死的,既然鱼总给了扩展思路,那么就实现它。
思路
- 建一张
category_tag分类标签表; - 通过字段
type进行区分是属于分类还是标签; - 不需要建立中间的关联表,保持原有图片表设计;
- 把
picture中的tags字段的存储方式改变一下,原来存的JSON字符串改为存分类标签表的 ID 即可,多个通过,号隔开; - 通过
FIND_IN_SET函数进行查找即可。
实现
分类标签表 SQL
▼MySQL复制代码-- 分类标签表 CREATE TABLE IF NOT EXISTS category_tag ( id BIGINT AUTO_INCREMENT COMMENT 'id' PRIMARY KEY, type TINYINT NOT NULL COMMENT '类型(0-分类、1-标签)', name VARCHAR(128) NOT NULL COMMENT '名称', useNum INT DEFAULT 0 NOT NULL COMMENT '使用数量', userId BIGINT NOT NULL COMMENT '创建用户 id', createTime DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间', editTime DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '编辑时间', updateTime DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', isDelete TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除', INDEX idx_name (name), INDEX idx_userId (userId) ) COMMENT '分类标签表' COLLATE = utf8mb4_unicode_ci;
生成实体类、Mapper 接口、Service 接口和实现;
- 略
实体类调整代码
新建 CategoryTagVO
▼Java复制代码/** * 分类标签响应类 */ @Data public class CategoryTagVO implements Serializable { /** * id */ private Long id; /** * 类型(0-分类、1-标签) */ private Integer type; /** * 名称 */ private String name; /** * 使用数量 */ private Integer useNum; private static final long serialVersionUID = 1L; /** * 对象转封装类 */ public static CategoryTagVO objToVo(CategoryTag categoryTag) { if (categoryTag == null) { return null; } CategoryTagVO categoryTagVO = new CategoryTagVO(); BeanUtils.copyProperties(categoryTag, categoryTagVO); return categoryTagVO; } }
新建 CategoryTagEnum
▼Java复制代码@Getter public enum CategoryTagEnum { CATEGORY("分类", 0), TAG("标签", 1); private final String text; private final Integer value; CategoryTagEnum(String text, Integer value) { this.text = text; this.value = value; } /** * 根据 value 获取枚举 * * @param value 枚举值的value * @return 枚举值 */ public static CategoryTagEnum getEnumByValue(Integer value) { if (ObjUtil.isEmpty(value)) { return null; } for (CategoryTagEnum anEnum : CategoryTagEnum.values()) { if (anEnum.value.equals(value)) { return anEnum; } } return null; } }
修改 PictureVO
- 注释或删掉下面注释掉的代码,并加入新的转换代码。
- 原因:因为数据库中存储的格式已经变了,所以不需要进行JSON处理。
▼Java复制代码/** * 封装类转对象 */ public static Picture voToObj(PictureVO pictureVO) { if (pictureVO == null) { return null; } Picture picture = new Picture(); BeanUtils.copyProperties(pictureVO, picture); // 类型不同,需要转换 // picture.setTags(JSONUtil.toJsonStr(pictureVO.getTags())); // 把VO的标签List转为字符串 if (pictureVO.getTags() != null && !pictureVO.getTags().isEmpty()) { picture.setTags(String.join(",", pictureVO.getTags())); } return picture; } /** * 对象转封装类 */ public static PictureVO objToVo(Picture picture) { if (picture == null) { return null; } PictureVO pictureVO = new PictureVO(); BeanUtils.copyProperties(picture, pictureVO); // 类型不同,需要转换 // pictureVO.setTags(JSONUtil.toList(picture.getTags(), String.class)); // 把原来标签格式转为List if (StrUtil.isNotBlank(picture.getTags())) { pictureVO.setTags(Arrays.stream(picture.getTags().split(",")).collect(Collectors.toList())); } return pictureVO; }
修改 PictureTagCategory
- 在该类中新增下面两个字段,方便前端遍历。
▼Java复制代码/** * 标签列表 */ private List<CategoryTagVO> tagVOList; /** * 分类列表 */ private List<CategoryTagVO> categoryVOList;
修改 PictureEditRequest 和 PictureUpdateRequest
- 在该类中新增下面字段。
- 原因:这个字段的作用是方便前端在选择标签的时候是自己输入的标签,此时需要把这个标签获取到并存入到数据库中。
▼Java复制代码/** * 输入的标签列表 */ private List<String> inputTagList;
功能模块代码
主页功能修改
修改内容
- 上方分类和标签查询和渲染;
- 下方图片展示的分类和标签;
后端代码: 新增分类标签接口
▼Java复制代码@Slf4j @RestController @RequestMapping("/categoryTag") public class CategoryTagController { @Resource private UserService userService; @Resource private CategoryTagService categoryTagService; /** * 获取分类标签数据 */ @GetMapping("/data/vo") public BaseResponse<PictureTagCategory> pictureCategoryTagData() { List<CategoryTag> categoryTagList = categoryTagService.list(); List<CategoryTagVO> voList = categoryTagList.stream().map(CategoryTagVO::objToVo).collect(Collectors.toList()); Map<Integer, List<CategoryTagVO>> typeMap = voList.stream().collect(Collectors.groupingBy(CategoryTagVO::getType)); PictureTagCategory pictureTagCategory = new PictureTagCategory(); pictureTagCategory.setCategoryVOList(typeMap.get(CategoryTagEnum.CATEGORY.getValue())); pictureTagCategory.setTagVOList(typeMap.get(CategoryTagEnum.TAG.getValue())); return ResultUtils.success(pictureTagCategory); } }
前端代码:HomePage.vue
- 页面完整代码如下,可以根据自己的进行对比那些地方修改了。
▼html复制代码<template> <div id="homePage"> <!-- 搜索框 --> <div class="search-bar"> <a-input-search placeholder="从海量图片中搜索" v-model:value="searchParams.searchText" enter-button="搜索" size="large" @search="doSearch" /> </div> <!-- 分类 + 标签 --> <a-tabs v-model:activeKey="selectedCategory" @change="doSearch"> <a-tab-pane key="all" tab="全部" /> <!--<a-tab-pane v-for="category in categoryList" :key="category" :tab="category" />--> <a-tab-pane v-for="category in categoryVOList" :key="category.id" :tab="category.name" /> </a-tabs> <div class="tag-bar"> <span style="margin-right: 8px">标签:</span> <a-space :size="[0, 8]" wrap> <!--<a-checkable-tag v-for="(tag, index) in tagList" :key="tag" v-model:checked="selectedTagList[index]" @change="doSearch" >--> <a-checkable-tag v-for="(tag, index) in tagVOList" :key="tag.id" v-model:checked="selectedTagList[index]" @change="doSearch" > {{ tag.name }} </a-checkable-tag> </a-space> </div> <!-- 图片列表 --> <a-list :grid="{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 4, xl: 5, xxl: 6 }" :data-source="dataList" :pagination="pagination" :loading="loading" > <template #renderItem="{ item: picture }"> <a-list-item style="padding: 0"> <!-- 单张图片 --> <a-card hoverable @click="doClickPicture(picture)"> <!-- 单张图片 --> <template #cover> <img style="height: 180px; object-fit: cover" :alt="picture.name" :src="picture.url" /> </template> <a-card-meta :title="picture.name"> <template #description> <a-flex> <a-tag color="green"> {{ picture.category ?? '默认' }} </a-tag> <a-tag v-for="tag in picture.tags" :key="tag"> {{ tag }} </a-tag> </a-flex> </template> </a-card-meta> </a-card> </a-list-item> </template> </a-list> </div> </template> <script setup lang="ts"> // 数据 import { computed, onMounted, reactive, ref } from 'vue' import { message } from 'ant-design-vue' import { listPictureTagCategoryUsingGet, listPictureVoByPageUsingPost, } from '@/api/pictureController' import { useRouter } from 'vue-router' import { pictureCategoryTagDataUsingGet } from '@/api/categoryTagController' const dataList = ref([]) const total = ref(0) const loading = ref(true) const selectedCategory = ref<string>('all') const selectedTagList = ref<string[]>([]) const categoryVOList = ref<API.CategoryTagVO[]>([]) const tagVOList = ref<API.CategoryTagVO[]>([]) // 获取标签和分类选项 const getTagCategoryOptions = async () => { // 新的分类和标签 const res = await pictureCategoryTagDataUsingGet() if (res.data.code === 0 && res.data.data) { // 转换成下拉选项组件接受的格式 categoryVOList.value = res.data.data.categoryVOList ?? [] tagVOList.value = res.data.data.tagVOList ?? [] } else { message.error('加载分类标签失败,' + res.data.message) } // const res = await listPictureTagCategoryUsingGet() // if (res.data.code === 0 && res.data.data) { // // 转换成下拉选项组件接受的格式 // categoryList.value = res.data.data.categoryList ?? [] // tagList.value = res.data.data.tagList ?? [] // } else { // message.error('加载分类标签失败,' + res.data.message) // } } // 搜索条件 const searchParams = reactive<API.PictureQueryRequest>({ current: 1, pageSize: 12, sortField: 'createTime', sortOrder: 'descend', }) // 分页参数 const pagination = computed(() => { return { current: searchParams.current ?? 1, pageSize: searchParams.pageSize ?? 10, total: total.value, // 切换页号时,会修改搜索参数并获取数据 onChange: (page, pageSize) => { searchParams.current = page searchParams.pageSize = pageSize fetchData() }, } }) // 获取数据 const fetchData = async () => { loading.value = true // 转换搜索参数 const params = { ...searchParams, tags: [], } // 选择的分类 if (selectedCategory.value !== 'all') { params.category = selectedCategory.value } // 选择的标签 selectedTagList.value.forEach((useTag, index) => { if (useTag) { // params.tags.push(tagList.value[index]) params.tags.push(tagVOList.value[index].id) } }) const res = await listPictureVoByPageUsingPost(params) if (res.data.data) { dataList.value = res.data.data.records ?? [] total.value = res.data.data.total ?? 0 } else { message.error('获取数据失败,' + res.data.message) } loading.value = false } onMounted(() => { getTagCategoryOptions() }) const router = useRouter() // 跳转至图片详情 const doClickPicture = (picture) => { router.push({ path: `/picture/${picture.id}`, }) } const doSearch = () => { // 重置搜索条件 searchParams.current = 1 fetchData() } // 页面加载时请求一次 onMounted(() => { fetchData() }) </script> <style scoped> #homePage .search-bar { max-width: 480px; margin: 0 auto 16px; } #homePage .tag-bar { margin-bottom: 20px; } </style>
图片详情修改
后端代码:PictureServiceImpl
- 展示修改的方法的全部代码。
▼Java复制代码/** * 获取图片封装类 * * @param picture 图片 * @param request HttpServletRequest * @return 图片封装类 */ @Override public PictureVO getPictureVO(Picture picture, HttpServletRequest request) { // 对象转封装类 PictureVO pictureVO = PictureVO.objToVo(picture); // 关联查询用户信息 Long userId = picture.getUserId(); if (userId != null && userId > 0) { User user = userService.getById(userId); UserVO userVO = userService.getUserVO(user); pictureVO.setUser(userVO); } // 查询当前图片的分类和标签的信息 List<String> ctIds = new ArrayList<>(); if (StrUtil.isNotBlank(picture.getTags())) { ctIds.addAll(Arrays.stream(picture.getTags().split(",")).collect(Collectors.toList())); } if (StrUtil.isNotBlank(picture.getCategory())) { ctIds.add(picture.getCategory()); } if (!ctIds.isEmpty()) { List<CategoryTag> categoryTagList = categoryTagService.list(new LambdaQueryWrapper<CategoryTag>() .in(CategoryTag::getId, ctIds) ); Map<Integer, List<CategoryTag>> typeMap = categoryTagList.stream().collect(Collectors.groupingBy(CategoryTag::getType)); if (typeMap.get(CategoryTagEnum.CATEGORY.getValue()) != null) { pictureVO.setCategory(typeMap.get(CategoryTagEnum.CATEGORY.getValue()).get(0).getName()); } if (typeMap.get(CategoryTagEnum.TAG.getValue()) != null) { pictureVO.setTags(typeMap.get(CategoryTagEnum.TAG.getValue()) .stream() .map(CategoryTag::getName) .collect(Collectors.toList())); } } return pictureVO; }
图片管理修改
后端代码:PictureController
- 展示修改的方法的全部代码。
▼Java复制代码/** * 分页获取图片列表(仅管理员可用) */ @PostMapping("/list/page") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse<Page<Picture>> listPictureByPage(@RequestBody PictureQueryRequest pictureQueryRequest) { long current = pictureQueryRequest.getCurrent(); long size = pictureQueryRequest.getPageSize(); // 查询数据库 Page<Picture> picturePage = pictureService.page(new Page<>(current, size), pictureService.getQueryWrapper(pictureQueryRequest)); // 处理分类和标签 for (Picture picture : picturePage.getRecords()) { // 查询当前图片的分类和标签的信息 List<String> ctIds = new ArrayList<>(); if (StrUtil.isNotBlank(picture.getTags())) { ctIds.addAll(Arrays.stream(picture.getTags().split(",")).collect(Collectors.toList())); } if (StrUtil.isNotBlank(picture.getCategory())) { ctIds.add(picture.getCategory()); } if (!ctIds.isEmpty()) { List<CategoryTag> categoryTagList = categoryTagService.list(new LambdaQueryWrapper<CategoryTag>() .in(CategoryTag::getId, ctIds) ); Map<Integer, List<CategoryTag>> typeMap = categoryTagList.stream().collect(Collectors.groupingBy(CategoryTag::getType)); if (typeMap.get(CategoryTagEnum.CATEGORY.getValue()) != null) { picture.setCategory(typeMap.get(CategoryTagEnum.CATEGORY.getValue()).get(0).getName()); } if (typeMap.get(CategoryTagEnum.TAG.getValue()) != null) { picture.setTags(JSONUtil.toJsonStr( typeMap.get(CategoryTagEnum.TAG.getValue()) .stream() .map(CategoryTag::getName) .collect(Collectors.toList()) )); } } } return ResultUtils.success(picturePage); }
创建图片修改、更新图片修改
前端代码:AddPicturePage.vue
- 页面完整代码如下,可以根据自己的进行对比那些地方修改了。
▼html复制代码<template> <div id="addPicturePage"> <h2 style="margin-bottom: 16px"> {{ route.query?.id ? '修改图片' : '创建图片' }} </h2> <PictureUpload :picture="picture" :onSuccess="onSuccess" /> <a-form layout="vertical" :model="pictureForm" @finish="handleSubmit"> <a-form-item label="名称" name="name"> <a-input v-model:value="pictureForm.name" placeholder="请输入名称" /> </a-form-item> <a-form-item label="简介" name="introduction"> <a-textarea v-model:value="pictureForm.introduction" placeholder="请输入简介" :rows="2" autoSize allowClear /> </a-form-item> <a-form-item label="分类" name="category"> <!--<a-auto-complete--> <!-- v-model:value="pictureForm.category"--> <!-- :options="categoryOptions"--> <!-- placeholder="请输入分类"--> <!-- allowClear--> <!--/>--> <!--<a-form-item label="标签" name="tags">--> <!-- <a-select--> <!-- v-model:value="pictureForm.tags"--> <!-- :options="tagOptions"--> <!-- mode="tags"--> <!-- placeholder="请输入标签"--> <!-- allowClear--> <!-- />--> <!--</a-form-item>--> <a-select v-model:value="pictureForm.category" :options="categoryOptions" placeholder="请输入分类" allowClear /> </a-form-item> <a-form-item label="标签" name="tags"> <a-select v-model:value="selectedTag" :options="tagOptions" mode="tags" placeholder="请输入标签" allowClear /> </a-form-item> <a-form-item> <a-button type="primary" html-type="submit" style="width: 100%"> {{ route.query?.id ? '修改' : '修改' }} </a-button> </a-form-item> </a-form> </div> </template> <script lang="ts" setup> import PictureUpload from '@/components/PictureUpload.vue' import { onMounted, reactive, ref } from 'vue' import { message } from 'ant-design-vue' import { editPictureUsingPost, getPictureVoByIdUsingGet } from '@/api/pictureController' import { useRoute, useRouter } from 'vue-router' import { pictureCategoryTagDataUsingGet } from '@/api/categoryTagController' const picture = ref<API.PictureVO>() const pictureForm = reactive<API.PictureEditRequest>({}) const onSuccess = (newPicture: API.PictureVO) => { picture.value = newPicture pictureForm.name = newPicture.name } // 分类和标签下拉框 const categoryOptions = ref<string[]>([]) const tagOptions = ref<string[]>([]) // 标签下拉框的Value const tagValue = ref<string[]>([]) // 标签下拉框的Label const tagLabel = ref<string[]>([]) // 选择的标签 const selectedTag = ref<string[]>([]) const tagList = ref<string[]>([]) const inputTagList = ref<string[]>([]) const router = useRouter() /** * 提交表单 * @param values */ const handleSubmit = async (values: any) => { const pictureId = picture.value.id if (!pictureId) { return } // 处理手动输入的标签, 如果在查询出来的标签下拉框中匹配不到,则添加到 inputTagList 中 tagOptions.value.forEach((tag) => { tagValue.value.push(tag.value) }) selectedTag.value.forEach((tag) => { if (!tagValue.value.includes(tag)) { inputTagList.value.push(tag) } else { tagList.value.push(tag) } }) values.tags = tagList.value values.inputTagList = inputTagList.value const res = await editPictureUsingPost({ id: pictureId, ...values, }) if (res.data.code === 0 && res.data.data) { message.success('创建成功') // 跳转到图片详情页 router.push({ path: `/picture/${pictureId}`, }) } else { message.error('创建失败,' + res.data.message) } } // 获取标签和分类选项 const getTagCategoryOptions = async () => { // const res = await listPictureTagCategoryUsingGet() const res = await pictureCategoryTagDataUsingGet() if (res.data.code === 0 && res.data.data) { // 转换成下拉选项组件接受的格式 /*tagOptions.value = (res.data.data.tagList ?? []).map((data: string) => { return { value: data, label: data, } }) categoryOptions.value = (res.data.data.categoryList ?? []).map((data: string) => { return { value: data, label: data, } })*/ tagOptions.value = (res.data.data.tagVOList ?? []).map((data: API.CategoryTagVO) => { return { value: data.id, label: data.name, } }) categoryOptions.value = (res.data.data.categoryVOList ?? []).map((data: API.CategoryTagVO) => { return { value: data.id, label: data.name, } }) } else { message.error('加载选项失败,' + res.data.message) } } onMounted(() => { getTagCategoryOptions() }) const route = useRoute() // 获取老数据 const getOldPicture = async () => { // 获取数据 const id = route.query?.id if (id) { const res = await getPictureVoByIdUsingGet({ id: id, }) await getTagCategoryOptions() tagOptions.value.forEach((tag) => { tagLabel.value.push(tag.label) }) if (res.data.code === 0 && res.data.data) { const data = res.data.data picture.value = data pictureForm.name = data.name pictureForm.introduction = data.introduction pictureForm.category = data.category pictureForm.tags = data.tags // 处理标签数据的回填问题 const tagIds = [] tagLabel.value.forEach((tag, index) => { if (data.tags.includes(tag)) { tagIds.push(tagOptions.value[index].value) } }) selectedTag.value = tagIds } } } onMounted(() => { getOldPicture() }) </script> <style scoped> #addPicturePage { max-width: 720px; margin: 0 auto; } </style>
最后
第一次在编程导航上写东西,可能写的不是很好,主打一个分享。 分类标签的增删改操作就自己写吧。 需要注意的:
- 生成的实体类
CategoryTag中的id字段的主键策略换成IdType.ASSIGN_ID,不然默认是从1开始自增的,不修改可能会出现手动输入的标签如果跟id一样了,那么是不会新增该标签的。
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
内容推荐
学习完智能协同云图库项目后,想改造成springcloudalibaba架构的微服务项目,微服务划分如下:依赖服务:- 注册中心与配置中心: Nacos(负责服务注册发现以及各服务的动态配置管理)- 微服务网关(yunpic-backend-gateway): Gateway 聚合所有接口,统一接收处理前端请求,负责路由转发、鉴权与限流公共模块:- common 公共模块(yunpic-backe
4
云图库-以图搜图失效解决
7
个人博客
4
我在做云图库项目到部署上线阶段时,前后端已经成功部署到宝塔云上,我也能成功通过我的域名正常访问云图库,注册新账户也是注册成功且远程数据库也有信息,但是用已有的账号登录总是显示系统错误。我看了下我的redis密码正确且服务是正常运行请问有人遇到过同样的问题吗?
3
智能协同云图库-进入编辑报错解决
4
