[Proyecto de mini programa fácil] Visualización de detalles del producto + comentarios, visualización de comentarios, Me gusta de comentarios + colección de productos [el backend está desarrollado en base al sistema de gestión Ruoyi]

Efecto de interfaz

【ilustrar】

  • Las imágenes de los productos en la interfaz son de Xianyu, si hay alguna infracción, contáctenos para eliminarlas.

【Detalles de producto】
Insertar descripción de la imagen aquí

【Comentario】
Insertar descripción de la imagen aquí

Implementación de interfaz

herramienta js

La función de esta clase de herramienta es calcular la relación de aspecto de la imagen dada la dirección URL de una imagen. El propósito de calcular la relación de aspecto es permitir que la imagen se muestre en una proporción normal.

/**
 * 获取uuid
 */
export default {
    
    
	/**
	 * 获取高宽比 乘以 100%
	 */
	getAspectRatio(url) {
    
    
		uni.getImageInfo({
    
    
			src: url,
			success: function(res) {
    
    
				let aspectRatio = res.height * 100.0 / res.width;
				// console.log("aspectRatio:" + aspectRatio);
				return aspectRatio + "%";
			}
		});
	},
}
export default {
    
    
	/**
	 * 日期格式化
	 */
	formatDateToString(date) {
    
    
	 return new Date(date).toLocaleString();
	},
}

página

<template>
	<view class="container">
		<u-toast ref="uToast"></u-toast>
		<view class="userItem">
			<view class="userProfile">
				<u--image :src="productVo.avatar" width="35" height="35" shape="circle"></u--image>
				<view style="width: 10px;"></view>
				<view>
					<view class="nickname">{
   
   {productVo.nickname}}</view>
					<view class="other">10分钟前来过 广东工业大学大学城校区</view>
				</view>
			</view>
			<view class="follow" @click="follow" v-if="hadFollow==false">
				<view>
					<u-icon name="plus" color="#ffffff" style="font-weight: bold;" size="15"></u-icon>
				</view>
				<view style="margin-left: 10rpx;font-size: 15px;">
					关 注
				</view>
			</view>
			<view class="followed" @click="cancelFollow" v-else>
				<view style="font-size: 15px;color: #C2C2C2;">
					已 关 注
				</view>
			</view>
		</view>
		<view class="productItem">
			<view class="top">
				<view class="price">¥<text class="number">{
   
   {productVo.price}}</text>/{
   
   {productVo.unit}}</view>
				<view class="browseInformation">
					{
   
   {product.starNum}}人想要 | {
   
   {product.readNum}}个浏览
				</view>
			</view>
			<view class="productDetail">
				{
   
   {productVo.description}}
			</view>
			<u--image :showLoading="true" v-for="(pic,index) in productVo.picList" :src="pic" width="100%"
				:height="getAspectRatio(pic)" radius="10" mode="widthFix"></u--image>
		</view>

		<view class="commentView">
			<view style="color: #3D3D3D;">
				{
   
   {commentNum}}条评论
			</view>
			<view v-for="(commentItem,index) in commentVoList">
				<view class="commentItem">
					<view style="display: flex;">
						<u--image :src="commentItem.userAvatar" width="30" height="30" shape="circle"></u--image>
						<view style="width: 10px;"></view>
						<view @click="clickShowBottomPopup(1, commentItem.id,commentItem.userNickName)">
							<view class="nickname">{
   
   {commentItem.userNickName}}</view>
							<view class="content">
								{
   
   {commentItem.content}}
							</view>
							<view class="dateAndPosition">{
   
   {formatDateToString(commentItem.createTime)}}</view>
						</view>
					</view>
					<view style="display: inline-block;text-align: center;">
						<u-icon name="thumb-up" size="28" @click="likeComment(commentItem.id,commentItem)"
							v-if="commentItem.isLike==0"></u-icon>
						<u-icon name="thumb-up-fill" color="#2B92FF" size="28"
							@click="cancelLikeComment(commentItem.id,commentItem)" v-else></u-icon>
						<view style="font-size: 12px;color: #B9B9B9;">
							{
   
   {commentItem.likeNum}}
						</view>
					</view>

				</view>
				<view class="sonCommentItem" v-for="(commentItem1,index1) in commentItem.children">
					<view style="display: flex;">
						<u--image :src="commentItem1.userAvatar" width="30" height="30" shape="circle"></u--image>
						<view style="width: 10px;"></view>
						<view @click="clickShowBottomPopup(1, commentItem1.id,commentItem1.userNickName)">
							<view class="nickname">{
   
   {commentItem1.userNickName}}</view>
							<view class="content">
								<text style="font-size: 14px;">
									回复了<text style="color:#B9B9B9 ;">{
   
   {commentItem1.toUserNickName}}</text></text>
								<text>
									{
   
   { commentItem1.content }}
								</text>
							</view>
							<view class="dateAndPosition">{
   
   {formatDateToString(commentItem1.createTime)}}</view>
						</view>
					</view>
					<view style="display: inline-block;text-align: center;">
						<u-icon name="thumb-up" size="28" @click="likeComment(commentItem1.id,commentItem1)"
							v-if="commentItem1.isLike==0"></u-icon>
						<u-icon name="thumb-up-fill" color="#2B92FF" size="28"
							@click="cancelLikeComment(commentItem1.id, commentItem1)" v-else></u-icon>
						<view style="font-size: 12px;color: #B9B9B9;">
							{
   
   {commentItem1.likeNum}}
						</view>
					</view>
				</view>

			</view>

		</view>

		<view class="footer">
			<view>
				<view class="item" @click="clickShowBottomPopup(0, productVo.id,)">
					<u-icon name="chat" size="28"></u-icon>
					<view class="comment">评论</view>
				</view>
				<view class="item" @click="starProduct()" v-if="hadStar==false">
					<u-icon name="star" size="28"></u-icon>
					<view class="comment">我想要</view>
				</view>
				<view class="item" @click="cancelStar()" v-if="hadStar==true">
					<u-icon name="star-fill" color="#2B92FF" size="28"></u-icon>
					<view class="comment" style="color: #2B92FF">已收藏</view>
				</view>
			</view>

			<view class="chat">
				<u-icon name="chat" color="#ffffff" size="18"></u-icon>
				<view style="width: 5px;"></view>
				私 聊
			</view>
		</view>

		<!-- 底部弹出框:用于输入评论 -->
		<!-- @close="this.showBottomPopup=false" 点击遮罩层关闭弹框  -->
		<u-popup :show="showBottomPopup" mode="bottom" :round="10" @close="this.showBottomPopup=false">
			<view class="commentPopup">
				<u--textarea v-model="comment.content" :placeholder="commentPlaceHolder" autoHeight height="200"
					border="surround"></u--textarea>
				<view class="commentButton" @click="commitComment()">
					<u-icon name="chat" color="#ffffff" size="18"></u-icon>
					<view style="width: 5px;"></view>
					评 论
				</view>
			</view>
		</u-popup>

	</view>
</template>

<script>
	import pictureApi from "@/utils/picture.js";
	import {
      
      
		addFollow,
		hadFollowSomeone,
		cancelFollowSomeone
	} from "@/api/market/follow.js";
	import {
      
      
		starProduct,
		cancelStar,
		hadStar
	} from "@/api/market/star.js";
	import {
      
      
		addComment,
		listCommentVoOfProduct
	} from "@/api/market/comment.js";
	import dateUtil from "@/utils/date.js";
	import {
      
      
		likeComment,
		cancelLikeComment
	} from "@/api/market/commentLike.js"
	import {
      
      
		getProduct
	} from "@/api/market/prodct.js"

	export default {
      
      
		data() {
      
      
			return {
      
      
				productVo: {
      
      },
				product: {
      
      },
				// 是否已经关注商品主人
				hadFollow: false,
				// 是否已经收藏商品
				hadStar: false,
				// 是否显示底部弹出框
				showBottomPopup: false,
				// 评论
				comment: {
      
      
					itemId: undefined,
					type: undefined,
					content: '',
					isTop: 0
				},
				// 存储商品对应的评论集合
				commentVoList: [],
				// 评论数量
				commentNum: undefined,
				commentPlaceHolder: "",
			}
		},
		methods: {
      
      
			/**
			 * 获取高宽比 乘以 100%
			 */
			getAspectRatio(url) {
      
      
				// uni.getImageInfo({
      
      
				// 	src: url,
				// 	success: function(res) {
      
      
				// 		let aspectRatio = res.height * 100.0 / res.width;
				// 		// console.log("aspectRatio:" + aspectRatio);
				// 		return aspectRatio + "%";
				// 	}
				// });
				return pictureApi.getAspectRatio(url);
			},
			/**
			 * 关注用户
			 */
			follow() {
      
      
				let data = {
      
      
					followedId: this.productVo.userId
				}
				addFollow(data).then(res => {
      
      
					this.hadFollow = true;
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "关注成功",
						duration: 300
					})
				}).catch(err => {
      
      
					this.$refs.uToast.show({
      
      
						type: 'error',
						message: err.msg,
						duration: 300
					})
				})
			},
			/**
			 * 取消关注
			 */
			cancelFollow() {
      
      
				cancelFollowSomeone(this.productVo.userId).then(res => {
      
      
					this.hadFollow = false;
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "取消关注成功",
						duration: 300
					})
				})
			},
			/**
			 * 查询是否已经关注了用户
			 */
			searchWhetherFollow() {
      
      
				hadFollowSomeone(this.productVo.userId).then(res => {
      
      
					// console.log("res:" + JSON.stringify(res));
					this.hadFollow = res.hadFollow;
					// console.log("this.hadFollow :" + this.hadFollow);
				})
			},
			/**
			 * 收藏商品
			 */
			starProduct() {
      
      
				starProduct(this.productVo.id).then(res => {
      
      
					this.hadStar = true;
					this.getProduct();
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "收藏成功",
						duration: 300
					})
				})
			},
			/**
			 * 取消收藏
			 */
			cancelStar() {
      
      
				cancelStar(this.productVo.id).then(res => {
      
      
					this.hadStar = false;
					this.getProduct();
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "取消收藏成功",
						duration: 300
					})
				})
			},
			/**
			 * 点赞评论
			 */
			likeComment(commentId, comment) {
      
      
				// console.log("comment:" + JSON.stringify(comment))
				likeComment(commentId).then(res => {
      
      
					comment.isLike = 1;
					comment.likeNum += 1;
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "点赞成功",
						duration: 300
					})
				})
			},
			/**
			 * 取消点赞评论
			 */
			cancelLikeComment(commentId, comment) {
      
      
				cancelLikeComment(commentId).then(res => {
      
      
					comment.isLike = 0;
					comment.likeNum -= 1;
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "取消点赞成功",
						duration: 300
					})
				})
			},
			/**
			 * 查询是否已经关注了用户
			 */
			searchWhetherStar() {
      
      
				hadStar(this.productVo.id).then(res => {
      
      
					// console.log("res:" + JSON.stringify(res));
					this.hadStar = res.hadStar;
					// console.log("this.hadFollow :" + this.hadFollow);
				})
			},
			/**
			 * 显示底部弹出框
			 */
			clickShowBottomPopup(type, itemId, username = undefined) {
      
      
				this.showBottomPopup = true;
				this.comment.type = type;
				this.comment.itemId = itemId;
				if (type == 0) {
      
      
					this.commentPlaceHolder = "想要了解更多信息,可以评论让商品主人看见哟";
				} else {
      
      
					this.commentPlaceHolder = "正在回复" + username + "";
				}
			},
			/**
			 * 发表评论
			 */
			commitComment() {
      
      
				// console.log("发送评论,comment:" + JSON.stringify(this.comment))
				addComment(this.comment).then(res => {
      
      
					this.showBottomPopup = false;
					this.comment.content = '';
					this.listCommentVoOfProduct();
					this.$refs.uToast.show({
      
      
						type: 'success',
						message: "评论发送成功",
						duration: 300
					})
				})
			},
			/**
			 * 获取商品对应的所有评论
			 */
			listCommentVoOfProduct() {
      
      
				listCommentVoOfProduct(this.productVo.id).then(res => {
      
      
					// console.log("listCommentVoOfProduct:" + JSON.stringify(res));
					this.commentVoList = res.tree;
					this.commentNum = res.commentNum;
				})
			},
			/**
			 * 格式化日期
			 * @param {Object} date
			 */
			formatDateToString(dateStr) {
      
      
				let date = new Date(dateStr);
				// 月份需要加一
				return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
			},
			/**
			 * 获取商品详细信息,同时增加阅读量
			 */
			getProduct() {
      
      
				getProduct(this.productVo.id).then(res => {
      
      
					console.log("product:" + JSON.stringify(res.data));
					this.product = res.data;
				})
			}
		},
		onLoad(e) {
      
      
			this.productVo = JSON.parse(decodeURIComponent(e.productVo));
			this.searchWhetherFollow();
			this.searchWhetherStar();
			this.listCommentVoOfProduct();
			this.getProduct();
			// console.log("productVo:" + JSON.stringify(productVo));
		}
	}
</script>

<style lang="scss">
	.container {
      
      
		// padding: 20rpx;
		background: #F7F7F7;

		.userItem {
      
      
			display: flex;
			align-items: center;
			justify-content: space-between;
			background: #ffffff;
			padding: 20rpx;

			.userProfile {
      
      
				display: flex;

				.nickname {
      
      
					color: #202020;
					font-weight: bold;
					font-size: 14px;
				}

				.other {
      
      
					color: #A6A4A5;
					font-size: 11px;
				}
			}

			.follow {
      
      
				display: flex;
				align-items: center;
				font-weight: bold;
				color: #ffffff;
				background: #2B92FF;
				border-radius: 20px;
				padding: 4px 8px;
			}

			.followed {
      
      
				background: #F6F6F6;
				border-radius: 20px;
				padding: 4px 8px;
			}
		}

		.productItem {
      
      
			background: #ffffff;
			padding: 20rpx;

			.top {
      
      
				display: flex;
				align-items: center;
				justify-content: space-between;

				.price {
      
      
					color: #F84442;
					font-weight: bold;

					.number {
      
      
						font-size: 30px;
					}
				}

				.browseInformation {
      
      
					color: #A6A4A5;
					font-size: 14px;
				}
			}

			.productDetail {
      
      
				margin-top: 20rpx;
				margin-bottom: 10rpx;
				color: #4C4C4C;
				font-size: 15px;
				line-height: 30px;
				font-weight: bold;
			}
		}

		.commentView {
      
      
			margin-top: 10px;
			// 用来预留展示 footer 的高度,不然footer会挡住评论
			margin-bottom: calc(60px + 10rpx);
			background: #ffffff;
			padding: 30rpx 30rpx;

			.nickname {
      
      
				font-size: 14px;
				color: #B9B9B9;
			}

			.content {
      
      
				margin: 5px;
				// 解决英文字符串、数字不换行的问题
				word-break: break-all;
				word-wrap: break-word;
			}

			.dateAndPosition {
      
      
				font-size: 11px;
				color: #B9B9B9;
			}

			.commentItem {
      
      
				display: flex;
				margin: 10px;
				justify-content: space-between;
			}

			.sonCommentItem {
      
      
				display: flex;
				margin: 10px 10px 10px 50px;
				justify-content: space-between;
			}

		}

		.footer {
      
      
			padding: 20rpx;
			position: fixed;
			// right: 20rpx;
			bottom: 0rpx;
			background: #ffffff;
			height: 60px;
			width: 710rpx;
			padding-top: 2px;

			display: flex;
			align-items: center;
			justify-content: space-between;

			.item {
      
      
				display: inline-block;
				text-align: center;
				margin-right: 10px;

				.comment {
      
      
					font-size: 10px;
				}
			}

			.chat {
      
      
				display: flex;
				align-items: center;
				background-color: #2B92FF;
				border-radius: 20px;
				padding: 7px;
				color: #ffffff;
				// margin-right: 20px;
				font-size: 12px;
			}

		}

		.commentPopup {
      
      
			display: flex;
			padding: 10px;
			min-height: 200rpx;

			.commentButton {
      
      
				background-color: #2B92FF;
				border-radius: 5px;
				padding: 7px;
				color: #ffffff;
				font-size: 12px;
				height: 20px;
				display: flex;
				align-items: center;
			}
		}


	}
</style>

formato de fecha

A veces, el formato de fecha pasado desde el backend no es muy hermoso o conciso para mostrarlo directamente en la página de inicio. Luego, usted mismo puede escribir un método de formato de fecha para convertir la fecha al formato que necesitamos para mostrar.

/**
 * 格式化日期
 * @param {Object} date
 */
formatDateToString(dateStr) {
    
    
	let date = new Date(dateStr);
	// 月份需要加一
	return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
},

Pantalla de ajuste automático de línea en inglés

.content {
    
    
	margin: 5px;
	// 解决英文字符串、数字不换行的问题
	word-break: break-all;
	word-wrap: break-word;
}

extremo posterior

recolectar

Controlador

Para facilitar la consulta de datos del producto, al diseñar la base de datos agregué un campo redundante de número de colección a la tabla de productos, por lo que cada vez que se recolecta o cancela un producto, es necesario actualizar el número de colección de la tabla de productos.

Insertar descripción de la imagen aquí

/**
 * 收藏商品
 */
@PreAuthorize("@ss.hasPermi('market:star:star')")
@GetMapping("/starProduct/{productId}")
public AjaxResult starProduct(@PathVariable("productId") Long productId) {
    
    
    Star star = new Star();
    star.setUserId(getLoginUser().getUserId());
    star.setProductId(productId);
    boolean isStar = starService.addStar(star);
    if (isStar){
    
    
        // 需要将商品的收藏量+1
        productService.starNumPlusOne(productId);
    }
    return AjaxResult.success();
}

Servicio

package com.shm.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.domain.entity.Star;
import com.shm.mapper.StarMapper;
import com.shm.service.IStarService;
import org.springframework.stereotype.Service;

/**
* @author dam
* @description 针对表【collection(收藏表)】的数据库操作Service实现
* @createDate 2023-08-09 19:41:23
*/
@Service
public class IStarServiceImpl extends ServiceImpl<StarMapper, Star>
    implements IStarService {
    
    

    @Override
    public boolean addStar(Star star) {
    
    
        return baseMapper.addStar(star);
    }
}

mapeador

public interface StarMapper extends BaseMapper<Star> {
    
    
    boolean addStar(@Param("star") Star star);
}

Al agregar elementos a una colección, primero debe determinar que los mismos datos de recopilación no existen en la base de datos antes de realizar la operación de inserción. De lo contrario, si la red del usuario está bloqueada y el usuario envía solicitudes de recopilación varias veces, aparecerán datos sucios redundantes. en la base de datos.

<insert id="addStar">
    INSERT INTO `star` (`user_id`, `product_id`)
    SELECT #{star.userId},#{star.productId} FROM DUAL
    WHERE NOT EXISTS (
            SELECT 1 FROM `star`
            WHERE `user_id` = #{star.productId} AND `product_id` = #{star.productId} limit 1
        );
</insert>

Comentario

Controlador

/**
 * 获取商品对应的所有评论
 *
 * @param productId
 * @return
 */
@PreAuthorize("@ss.hasPermi('market:comment:list')")
@GetMapping("/listCommentVoOfProduct/{productId}")
public AjaxResult listCommentVoOfProduct(@PathVariable("productId") Long productId) {
    
    
    // 查询出商品对应的所有评论数据
    List<CommentVo> commentVoList = commentService.listCommentVoOfProduct(productId, getLoginUser().getUserId());
    int commentNum = commentVoList.size();
    // 将评论数据封装成树形结构
    List<CommentVo> tree = commentService.buildTree(commentVoList);
    return AjaxResult.success().put("tree", tree).put("commentNum", commentNum);
}

Servicio

Cabe señalar que la estructura de árbol aquí solo tiene dos capas de datos (una capa para reseñas de productos y una capa para todos los comentarios), porque es inconveniente para el mini programa mostrar demasiadas capas de datos, de lo contrario el ancho será muy grande Los usuarios deben deslizar repetidamente el dedo para ver la reseña completa
Insertar descripción de la imagen aquí

Insertar descripción de la imagen aquí

@Override
public List<CommentVo> listCommentVoOfProduct(Long productId, Long userId) {
    
    
    return commentMapper.listCommentVoOfProduct(productId, userId);
}

/**
 * 将评论数据封装成树形结构
 *
 * @param commentVoList
 * @return
 */
@Override
public List<CommentVo> buildTree(List<CommentVo> commentVoList) {
    
    
    // 将所有父级评论过滤出来
    List<CommentVo> fatherList = commentVoList.stream().filter((item) -> {
    
    
        return item.getType() == 0;
    }).collect(Collectors.toList());
    commentVoList.removeAll(fatherList);
    // 为所有父级评论寻找孩子
    for (CommentVo father : fatherList) {
    
    
        father.setChildren(new ArrayList<>());
        this.searchSon(father.getId(), father.getUserNickName(), father.getChildren(), commentVoList);
    }
    return fatherList;
}

/**
 * 寻找孩子
 *
 * @param fatherId
 * @param children
 * @param commentVoList
 */
private void searchSon(Long fatherId, String fatherNickName, List<CommentVo> children, List<CommentVo> commentVoList) {
    
    
    for (CommentVo commentVo : commentVoList) {
    
    
        if (commentVo.getItemId().equals(fatherId)) {
    
    
            commentVo.setToUserNickName(fatherNickName);
            children.add(commentVo);
            this.searchSon(commentVo.getId(), commentVo.getUserNickName(), children, commentVoList);
        }
    }
}

Mapeador

Este sql es muy complejo: puede averiguar el apodo del propietario del comentario, el avatar y el número de me gusta del comentario a la vez, y utiliza consultas recursivas para consultar continuamente los subcomentarios del comentario. No puedo garantizar la eficiencia de este SQL en este momento. Acabo de implementar la función. Si el rendimiento es insuficiente más adelante, encontraré una manera de optimizarlo.

<select id="listCommentVoOfProduct" resultType="com.ruoyi.common.core.domain.vo.CommentVo">
      SELECT
          ct.id,
          ct.user_id,
          ct.item_id,
          ct.type,
          ct.content,
          ct.create_time,
          u.nick_name AS userNickName,
          u.avatar AS userAvatar,
          CASE
              WHEN cl.user_id IS NULL THEN
                  0 ELSE 1
              END AS isLike,
          ct.LEVEL,
          COALESCE ( likeNum, 0 ) AS likeNum
      FROM
          (
              WITH RECURSIVE comment_tree AS (
                  SELECT
                      id,
                      user_id,
                      item_id,
                      type,
                      content,
                      create_time,
                      0 AS LEVEL
                  FROM
                      COMMENT
                  WHERE
                      item_id = #{productId} and type=0
                  UNION ALL
                  SELECT
                      c.id,
                      c.user_id,
                      c.item_id,
                      c.type,
                      c.content,
                      c.create_time,
                      ct.LEVEL + 1 AS LEVEL
                  FROM
                      COMMENT c
                          INNER JOIN comment_tree ct ON c.item_id = ct.id
                  WHERE
                      c.type = 1
              ) SELECT
                  *
              FROM
                  comment_tree
          ) ct
              LEFT JOIN ( SELECT comment_id, COUNT(*) AS likeNum FROM comment_like WHERE is_deleted = 0 GROUP BY comment_id ) pc ON ct.id = pc.comment_id
              LEFT JOIN sys_user AS u ON ct.user_id = u.user_id
              LEFT JOIN comment_like cl ON ct.id = cl.comment_id
              AND cl.user_id = #{userId} and cl.is_deleted =0
  </select>

mercancías

Controlador

/**
 * 获取商品详细信息
 */
@PreAuthorize("@ss.hasPermi('market:product:query')")
@GetMapping(value = "/{id}")
@Transactional // 同时处理多个表,添加事务
public AjaxResult getInfo(@PathVariable("id") Long id) {
    
    
    // 首先判断用户有没有阅读该商品
    boolean isAdd = productReadService.addRead(new ProductRead(getLoginUser().getUserId(), id));
    if (isAdd) {
    
    
        // 需要将商品的阅读量+1
        productService.readNumPlusOne(id);
    }
    return success(productService.getById(id));
}

leer

Servicio

<insert id="addRead">
    INSERT INTO `product_read` (`user_id`, `product_id`)
    SELECT #{productRead.userId},#{productRead.productId} FROM DUAL
    WHERE NOT EXISTS (
            SELECT 1 FROM `product_read`
            WHERE `user_id` = #{productRead.userId} AND `product_id` = #{productRead.productId} limit 1
        );

</insert>

Otros artículos sobre el mismo proyecto.

Para ver otros artículos sobre este proyecto, consulte la introducción del proyecto [Easy Mini Program Project], la visualización de la página del mini programa y la colección de series de artículos.

Supongo que te gusta

Origin blog.csdn.net/laodanqiu/article/details/132343876
Recomendado
Clasificación