三种东西永远不要放到数据库里

本文讨论了不当使用数据库可能导致的问题,如降低读写速度、增大备份难度及增加访问层级。文章建议图片、短生命周期数据和日志文件应采用更适合的存储方式。

导读:作者Frank Wiles发表了一篇博文,Frank Wiles曾在很多演讲里说过,改进你的系统的最好的方法是先避免做“蠢事”。并不是说你或你开发的东西“蠢”,只是有些决定很容易被人们忽略掉其暗含的牵连,认识不到这样做对系统维护尤其是系统升级带来多大的麻烦。作为一个顾问,像这样的事情我到处都能见到,我还从来没有见过做出这样的决定的人有过好的结果的。

图片,文件,二进制数据

既然数据库支持BLOB类型的数据,把文件塞进BLOB字段里一定没有错了!?错,不是这样的!别的先不提,在很多数据库语言里,处理大字段都不是很容易。

把文件存放在数据库里有很多问题:

  • 对数据库的读/写的速度永远都赶不上文件系统处理的速度
  • 数据库备份变的巨大,越来越耗时间
  • 对文件的访问需要穿越你的应用层和数据库层

这后两个是真正的杀手。把图片缩略图存到数据库里?很好,那你就不能使用nginx或其它类型的轻量级服务器来处理它们了。

给自己行个方便吧,在数据库里只简单的存放一个磁盘上你的文件的相对路径,或者使用S3或CDN之类的服务。

短生命期数据

使用情况统计数据,测量数据,GPS定位数据,session数据,任何只是短时间内对你有用,或经常变化的数据。如果你发现自己正在使用定时任务从某个表里删除有效期只有一小时,一天或数周的数据,那说明你没有找对正确的做事情的方法。使用redisstatsd/graphiteRiak,它们都是干这种事情更合适的工具。这建议也适用于对于收集那些短生命期的数据。

当然,用挖土机在后花园里种土豆也是可行的,但相比起从储物间里拿出一把铲子,你预约一台挖土机、等它赶到你的园子里挖坑,这显然更慢。你要选择合适的工具来处理手头上的事。

日志文件

把日志数据存放到数据库里,表面上看起来似乎不错,而且“将来也许我需要对这些数据进行复杂的查询”,这样的话很得人心。这样做并不是一个特别差的做法,但如果你把日志数据和你的产品数据存放到一个数据库里就非常不好了。

也许你的日志记录做的很保守,每次web请求只产生一条日志。对于整个网站的每个事件来说,这仍然会产生大量的数据库插入操作,争夺你用户需要的数据库资源。如果你的日志级别设置为verbose或debug,那等着看你的数据库着火吧。

你应该使用一些比如Splunk Loggly或纯文本文件来存放你的日志数据。这样去查看它们也许会不方便,但这样的时候不多,甚至有时候你需要写出一些代码来分析出你想要的答案,但总的来说是值得的。

可是稍等一下,你是那片不一样的雪花,你遇到的问题会如此的不同,所以,如果你把上面提到的三种东西中的某一种放到了数据库里也不会有问题。不,你错了,不,你不特殊。相信我。

译文出自:外刊IT评论

英文出自:revsys

下面代码修改页面的数据不能返回到提货地址的下拉框里面:<template> <div class="container"> <el-tabs type="border-card"> <el-tab-pane :label="title"> <el-form ref="form" :model="form" :rules="rules" label-width="120px"> <el-form-item label="介绍图片:" prop="cover"> <div style="display: flex;"> <div class="flex-row"> <el-upload class="avatar-uploader" action :http-request="selectPicUpload" :show-file-list="false" :before-upload="beforeAvatarUpload" :on-change="handleAvatarChange"> <img v-if="form.cover" :src="form.cover" class="avatar"> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload> </div> </div> </el-form-item> <el-row> <el-col :span="12"> <el-form-item label="团购名称:" prop="name"> <el-input class="input" v-model="form.name" placeholder="请输入团购名称" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="自动截止:" prop="isAutoEnd"> <el-switch v-model="form.isAutoEnd" :active-value="true" :inactive-value="false" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="团购内容:" prop="detailInfo"> <el-input class="input" v-model="form.detailInfo" placeholder="请输入团购内容" /> <!-- <Tinymce v-model="form.detailInfo" :disabled="allDisabled"></Tinymce> --> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="开始时间:" prop="startTime"> <el-date-picker clearable v-model="form.startTime" type="datetime" default-time="12:00:00" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择活动开始时间"> </el-date-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="截止时间:" prop="endTime"> <el-date-picker clearable v-model="form.endTime" type="datetime" default-time="12:00:00" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择活动截止时间"> </el-date-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="最大接龙人数" prop="maxJoinNum"> <el-input class="input" v-model="form.maxJoinNum" placeholder="请输入" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="最小成交金额" > <el-input class="input" v-model="form.minAmount" placeholder="请输入" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="提货地址:" prop="addressIds"> <el-select v-model="form.addressIds" filterable placeholder="请选择" multiple> <el-option v-for="item in addressoptions" :key="item.id" :label="item.addressName" :value="item.id"> </el-option> </el-select> </el-form-item> </el-col> </el-row> <el-form-item label="其它信息:"> <el-checkbox-group v-model="form.selectedItemsOther"> <el-checkbox label="isGetRealName">真实姓名</el-checkbox> <el-checkbox label="isGetPhone">手机号码</el-checkbox> </el-checkbox-group> </el-form-item> <el-form-item label="功能选择:"> <el-checkbox-group v-model="form.selectedItemsFunction"> <el-checkbox label="isHideList">发布最新接龙列表</el-checkbox> <el-checkbox label="isHideOrderAmount">接龙金额仅创建人可见</el-checkbox> <el-checkbox label="canCancel">禁止用户取消订单</el-checkbox> </el-checkbox-group> </el-form-item> <div> <el-form-item label="商品信息:" prop="products"> <el-table :data="form.products" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="55" align="center" /> <el-table-column label="商品名称" align="center" prop="product.name"> <template slot-scope="{row,$index}"> <el-input v-model="row.name" placeholder="请输入名称"></el-input> </template>" </el-table-column> <el-table-column label="商品价格" align="center" prop="products.price"> <template slot-scope="{row,$index}"> <el-input v-model="row.price" placeholder="请输入名称"></el-input> </template>" </el-table-column> <el-table-column label="商品数量" align="center" prop="products.soldNum"> <template slot-scope="{row,$index}"> <el-input v-model="row.soldNum" placeholder="请输入名称"><el-button icon="el-icon-remove-outline" @click="addmount()"></el-button></el-input> </template> </el-table-column> <el-table-column label="每单限制购买量" align="center" prop="products.limitNum" width="180"> <template slot-scope="{row,$index}"> <el-input v-model="row.limitNum" placeholder="请输入名称"></el-input> </template> </el-table-column> <el-table-column label="商品图片" align="center" prop="image" width="100"> <template slot-scope="{row,$index}"> <el-upload class="avatar-uploader" action :http-request="(fileObj) => selectProductUpload(fileObj, $index)" :show-file-list="false" :before-upload="beforeAvatarUpload"> <img v-if="row.image" :src="row.image" class="avatar"> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload> </template> </el-table-column> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template slot-scope="scope"> <!-- <el-button size="mini" type="text" v-if="scope.row.status != '4'" icon="el-icon-edit" @click="openApproval(scope.row)" v-hasPermi="['solitaire:list:order']">添加 </el-button > --> <el-button @click="addproduct()"> 添加</el-button> <el-button type="danger" icon="el-icon-delete" :disabled="form.products.length <= 1" @click="removeRow(scope.$index)">删除</el-button> </template> </el-table-column> </el-table> </el-form-item> </div> </el-form> <!-- 修改/新增 --> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="handleBack">取 消</el-button> </div> </el-tab-pane> </el-tabs> </div> </template> <script> import { imgUrl } from '@/utils/request' import { getISOTime, checkLongitudeValidator, checkPointIsNumber } from '@/utils/parsing' import { checkPhone } from '@/utils/validate' import mapView from '@/components/MapContainer/index.vue' import { addGroupShop, editGroupBuy, getGroupBuyAddressPage, getGroupBuyDetail } from '@/api/solitaire/solitaire' import { uploadPic } from '@/api/activity/activity' import { onMounted, beforeDestroy } from 'vue' import { reset } from 'echarts/lib/visual/seriesColor' export default { components: { mapView }, onMounted() { }, watch: { '$route.query'(newVal) { if (!newVal.id) { this.title = "新增接龙"; this.resetForm(); // 重置表单方法 } } }, data() { return { addressoptions: [], addressList: [], currentTime: new Date().toLocaleTimeString(), timer: null, // 第一个复选框组绑定值 // 第二个复选框组绑定值 selectedItemsFunction: [], checkList: ['选中且禁用', '复选框 A'], checked: true, // 单个复选框的绑定 dataList: [], title: "新增接龙", // 封面图片 selectImgUrl: '', queryParams: {}, currentImgIndex: 0, // 封面图片类型选择 imgDialogVisible: false, // 表单数据 form: { selectedItemsOther: [], selectedItemsFunction: [], ids: null, // addressIds: ['1'], addressIds: [], canCancel: 0, cover: '', detailInfo: null, endTime: null, id: 0, isAutoEnd: 0, isHideList: false, isHideOrderAmount: false, isJoin: 0, maxJoinNum: 0, minAmount:null, name: "", products: [ { id: 0, image: imgUrl, limitNum: 0, name: "", parentId: 0, price: 0, soldNum: 0, totalNum: 0 } ], realEndTime: "", startTime: "", status: 0, }, // 表单校验 rules: { products: [ { validator: (rule, value, callback) => { // 自定义校验逻辑 if (!value || value.length === 0) { callback(new Error('至少需要添加一个商品')); return; } const isValid = value.every(item => item.name && item.price !== undefined && item.price !== null && item.image && item.image !== imgUrl // 确保不是默认图片 ); if (!isValid) { callback(new Error('请完善所有商品信息(名称、价格和图片必填)')); } else { callback(); } }, trigger: 'change' // 可以根据需要改为'blur'或保持'change' } ], addressIds: [ { required: true, message: '请选择提货地址', trigger: ['change', 'blur'] } ], cover: [ { required: true, message: '请选择活动图片', trigger: ['change', 'blur'] } ], name: [ { required: true, message: '请输入团购名称', trigger: 'change' } ], othermessage: [ { type: 'array', required: true, message: '请选择其它信息', trigger: 'change' } ], selectability: [ { type: 'array', required: true, message: '请选择功能', trigger: 'change' }, ], shopmessage: [ { required: true, message: '请输入商品信息', trigger: 'change' }, ], detailInfo: [ { required: true, message: '请输入团购内容', trigger: 'change' } ], maxJoinNum: [ { required: true, message: '最大人数不能为空', trigger: 'change' } ], minAmount: [ { required: true, message: '最小成交金额不能为空', trigger: 'change' } ], endTime: [ { required: true, message: '结束时间必须选择', trigger: 'change' } ], startTime: [ { required: true, message: '开始时间不能为空', trigger: 'change' } ], description: [ { required: true, message: '活动开始内容不能为空', trigger: 'change' } ], address: [ { required: true, message: '地点不能为空', trigger: 'change' } ], conditionRequire: [ { required: true, message: '请输入条件说明', trigger: 'change' } ], // signStartDatetime: [ // { required: true, message: '报名开始时间必须选择', trigger: 'change' } // ], signEndDatetime: [ { required: true, message: '活动截止时间必须选择', trigger: 'change' } ], jobUserVOLists: [ { type: 'array', required: true, message: '请添加岗位需求', trigger: 'change' } ], }, // 图片前缀 cover: imgUrl, // //分页 column: { pageSize: null, pageNum: null }, // 队伍列表 // 岗位表单校验 jobRules: { jobName: [ { required: true, message: '请输入岗位名称', trigger: 'change' } ], jobCount: [ { required: true, message: '请输入岗位人数', trigger: 'change' } ], workHour: [ { required: true, message: '请输入工时', trigger: 'change' } ], pointPercent: [ { required: true, message: '请输入积分(每人每小时)', trigger: 'change' }, { validator: checkPointIsNumber, message: '积分必须为数字' } ], checkInTime: [ { required: true, message: '请输入签到时间', trigger: 'change' } ], checkOutTime: [ { required: true, message: '请输入签退时间', trigger: 'change' } ], checkLocation: [ { required: true, message: '请选择签到地点', trigger: 'change' } ] }, } }, created() { this.getAddressList() if (!this.$route.query.id) { this.$router.replace({ ...this.$route, query: {} }); } else { this.queryParams = this.$route.query; console.log(this.queryParams.id, '888') if (this.queryParams.id) { // 编辑 this.title = '修改接龙' getGroupBuyDetail(this.queryParams.id).then(res => { const boolConvert = val => val === 1; console.log(res, '---') const { cover, id } = res; console.log(res.isGetRealName) this.form = { ...res, isAutoEnd: res.isAutoEnd = 1 ? true : false, id, cover, addressIds:res.addressIds || [], selectedItemsOther: [], selectedItemsFunction: [], products:res.products || [{ image:imgUrl }] } if (res.isGetRealName === 1) { this.form.selectedItemsOther.push('isGetRealName') } if (res.isGetPhone === 1) { this.form.selectedItemsOther.push('isGetPhone') } if (res.isHideList === 1) { this.form.selectedItemsFunction.push('isHideList') } if (res.isHideOrderAmount === 1) { this.form.selectedItemsFunction.push('isHideOrderAmount') } if (res.canCancel === 1) { this.form.selectedItemsFunction.push('canCancel') } }) }} }, methods: { onMounted() { }, getAddressList() { getGroupBuyAddressPage().then(res => { this.addressoptions = res.pageData }) }, beforeDestroy() { clearInterval(this.timer); }, addproduct() { this.form.products.push({ /* 初始化新商品 */ }); this.$nextTick(() => { this.$refs.form.validateField('products'); // 触发products字段校验 }); }, removeRow(index) { // 添加长度检查 if (this.form.products.length <= 1) { this.$message.warning('至少需要保留一个商品'); return; } this.form.products.splice(index, 1); this.$nextTick(() => { this.$refs.form.validateField('products'); }); }, // 在图片上传成功后的回调中也触发校验 selectProductUpload(obj, index) { uploadPic().then(res => { this.$set(this.form.products, index, { ...this.form.products[index], image: res}) this.$nextTick(() => { this.$refs.form.validateField('products'); }); }); }, handleProductAvatarChange(file) { if (!file || !file.raw) return; const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { // 限制最大尺寸(合理值:800×600) const MAX_SIZE = 800; let width = img.width; let height = img.height; // 按比例缩放到最大尺寸 if (width > MAX_SIZE || height > MAX_SIZE) { const scale = Math.min(MAX_SIZE / width, MAX_SIZE / height); width *= scale; height *= scale; } // 画布压缩处理 const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); // 关键优化:降低质量并生成更短Data URL this.form.products.image = canvas.toDataURL(file.type, 0.01); // 压缩质量65% }; img.src = e.target.result; }; reader.readAsDataURL(file.raw); }, handleAvatarChange(file) { if (!file || !file.raw) return; const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { // 限制最大尺寸(合理值:800×600) const MAX_SIZE = 800; let width = img.width; let height = img.height; // 按比例缩放到最大尺寸 if (width > MAX_SIZE || height > MAX_SIZE) { const scale = Math.min(MAX_SIZE / width, MAX_SIZE / height); width *= scale; height *= scale; } // 画布压缩处理 const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); // 关键优化:降低质量并生成更短Data URL this.form.cover = canvas.toDataURL(file.type, 0.01); // 压缩质量65% }; img.src = e.target.result; }; reader.readAsDataURL(file.raw); }, beforeAvatarUpload(file) { const validTypes = ['image/jpeg', 'image/png', 'image/webp']; // 移除GIF减小潜在数据量 const MAX_SIZE_MB = 1.2; // 更严格的大小限制 const isImage = validTypes.includes(file.type); const isSizeValid = file.size / 1024 / 1024 < MAX_SIZE_MB; if (!isImage) { this.$message.error('仅支持 JPG/PNG/WEBP 格式'); return false; } if (!isSizeValid) { this.$message.error(`图片大小不能超过 ${MAX_SIZE_MB}MB`); return false; } return true; }, // updateOtherInfo() { // this.form.isGetRealName = this.selectedItemsOther.includes('真实姓名'); // this.form.isGetPhone = this.selectedItemsOther.includes('手机号码'); // }, // 更新"功能选择"表单值 updateFunctionSelection() { this.form.isHideList = this.selectedItemsFunction.includes('发布最新接龙列表'); this.form.isHideOrderAmount = this.selectedItemsFunction.includes('接龙金额仅创建人可见'); // "禁止用户取消订单"需要特殊处理:选中时canCancel=false this.form.canCancel = !this.selectedItemsFunction.includes('禁止用户取消订单'); }, // 获取活动详情 // getDetail(status) { // getGroupBuyDetail(this.queryParams.id, status).then(response => { // this.queryParams.id, status // this.form = response // console.log(response, '789') // }) // }, // 表单重置 reset() { this.form = { addressIds: [], cover: null, canCancel: 0, ids: null, detailInfo: null, endTime: null, id: 0, isAutoEnd: 0, isGetPhone: 0, isGetRealName: 0, isHideList: 0, isHideOrderAmount: 0, isJoin: 0, maxJoinNum: 0, minAmount: 0, name: "", products: [ { id: 0, image: imgUrl, limitNum: 0, name: "", parentId: 0, price: 0, soldNum: 0, totalNum: 0 } ], realEndTime: "", startTime: "", status: 0, } }, selectPicUpload(obj) { let fd = new FormData() //参数的格式是formData格式的 fd.append('file', obj.file) //参数 uploadPic(fd, { modularName: 'ACTIVITY' }).then(res => { this.form.cover = res }) }, selectProductUpload(obj, index) { let fd = new FormData(); fd.append('file', obj.file); uploadPic(fd, { modularName: 'ACTIVITY' }).then(res => { // 直接更新对应索引位置的图片URL this.$set(this.form.products, index, { ...this.form.products[index], image: res }); }).catch(error => { console.error('商品图片上传失败:', error); this.$message.error('商品图片上传失败'); }); }, // // 取消关闭弹窗 // closeDialog() { // this.currentImgIndex = 0 // this.selectImgUrl = '' // this.imgDialogVisible = false // }, // // 确认默认图片关闭弹窗 // submitPic() { // this.form.cover = this.selectImgUrl // this.imgDialogVisible = false // }, hasCommonValue(arr) { let seen = new Set(); // 用于存储已经遇到的值 for (let obj of arr) { for (let value of obj.jobUsers) { // 如果值已经存在于 set 中,返回 true if (seen.has(value.userId)) { return true; } // 将当前值加入 set seen.add(value.userId); } } return false; // 如果没有重复的值,返回 false }, /** 提交按钮 */ submitForm() { console.log(this.form.products.image, '000') let newForm = { ids: this.form.ids, addressIds: this.form.addressIds, canCancel: this.form.selectedItemsFunction.includes('canCancel') ? 1 : 0, cover: this.form.cover, detailInfo: this.form.detailInfo, endTime: this.form.endTime, id: this.form.id, isAutoEnd: this.form.isAutoEnd == true ? 1 : 0, isGetPhone: this.form.selectedItemsOther.includes('isGetPhone') ? 1 : 0, isGetRealName: this.form.selectedItemsOther.includes('isGetRealName') ? 1 : 0, isHideList: this.form.selectedItemsFunction.includes('isHideList') ? 0 : 1, isHideOrderAmount: this.selectedItemsFunction.includes('isHideOrderAmount') ? 1 : 0, isJoin: this.form.isJoin, maxJoinNum: this.form.maxJoinNum, minAmount: this.form.minAmount, name: this.form.name, // 遍历数组中的每个对象 products: this.form.products.map(item => ({ id: item.id, image: item.image, limitNum: item.limitNum, name: item.name, parentId: item.parentId, price: item.price, soldNum: item.soldNum, totalNum: item.totalNum })), realEndTime: this.form.realEndTime, startTime: this.form.startTime, status: this.form.status, // 修正为this.form.status }; console.log(newForm, this.form.id) //判断每个时间段是否有岗位 // let isJobEmpty = this.form.jobUserVOLists.some(item => item.jobList.length == 0) this.$refs['form'].validate(valid => { if (valid) { if (this.form.id) { editGroupBuy(newForm).then(response => { console.log(response, '00') this.$modal.msgSuccess('修改成功') this.$router.back(-1) }) } else { if (this.form.name != '' && this.form.cover != '') { addGroupShop(newForm).then(response => { console.log(response) this.$modal.msgSuccess('新增成功') this.$router.back(-1) }) } else { console.log(this.form.ids, '09') this.$modal.msgError('请完善信息') } } } }) }, handleBack() { this.$router.back(-1) }, // handleSelectionChange(selection) { this.ids = selection.map(item => item.pkId) this.single = selection.length !== 1 this.multiple = !selection.length }, // 新增时间段 changeStatus() { if (this.currentTime) { this.timer = setInterval(() => { this.currentTime = new Date().toLocaleTimeString(); console.log(this.currentTime, '90') }, 1000); } }, // 改变时间段的初始时间 changeStartTime(value, timeIndex) { console.log(this.form.jobUserVOLists[timeIndex].jobList) if (this.form.jobUserVOLists[timeIndex].jobList) { this.form.jobUserVOLists[timeIndex].jobList.forEach(item => { item.checkInTime = value if (item.checkOutTime && item.checkInTime) { item.workHour = getISOTime(item.checkInTime, item.checkOutTime) if (item.jobUsers) { item.jobUsers.forEach(itemson => { itemson.workHour = item.workHour itemson.point = parseInt(item.pointPercent) * item.workHour }) } } }) } }, // 改变时间段的初始时间 changeEndTime(value, timeIndex) { if (this.form.jobUserVOLists[timeIndex].jobList) { this.form.jobUserVOLists[timeIndex].jobList.forEach(item => { item.checkOutTime = value if (item.checkOutTime && item.checkInTime) { item.workHour = getISOTime(item.checkInTime, item.checkOutTime) if (item.jobUsers) { item.jobUsers.forEach(itemson => { itemson.workHour = item.workHour itemson.point = parseInt(item.pointPercent) * item.workHour }) } } }) } }, // 改变时间段的签到纬度 changeLatitude(value, timeIndex) { if (this.form.jobUserVOLists[timeIndex].jobList) { this.form.jobUserVOLists[timeIndex].jobList.forEach(item => { item.checkLatitude = value }) } }, // 改变时间段的签到经度 changeLongitude(value, timeIndex) { if (this.form.jobUserVOLists[timeIndex].jobList) { this.form.jobUserVOLists[timeIndex].jobList.forEach(item => { item.checkLongitude = value }) } }, // 跳转 toPrck() { window.open('https://lbs.amap.com/tools/picker') }, // 删除时间段 deleteTime(timeIndex) { this.$modal.confirm('是否确认该时间段?').then(function () { }).then(() => { this.form.jobUserVOLists.splice(timeIndex, 1) this.changeNumber() }).catch(() => { }) }, // 删除岗位按钮 // 活动最大人数改变 changeNumber() { this.form.maxCount = 0 this.form.jobUserVOLists.forEach(item => { item.jobList.forEach(itemson => { this.form.maxCount += itemson.jobUsers.length }) }) }, // 岗位人数改变 //岗位志愿者选择 } } </script> <style scoped> .container { background-color: #f5f5f5; padding: 1%; } .title_type { font-size: 24px; font-weight: 700; margin-bottom: 20px; } .dialog-footer { display: flex; flex-direction: row; justify-content: center; align-items: center; padding-bottom: 30px; } .avatar-uploader .el-upload { border: 1px solid #f8f3f3; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; } .avatar-uploader .el-upload:hover { border-color: #409EFF; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 120px; height: 120px; line-height: 120px; text-align: center; } .coverImage { width: 214px; height: 129px; border-radius: 6px; cursor: pointer; } .select_mask { position: absolute; z-index: 2; background-color: rgba(127, 125, 121, 0.5); text-align: center; top: 0; bottom: 0; left: 0; right: 0; width: 214px; height: 129px; display: flex; justify-content: center; align-items: center; } .avatar { width: 50px; height: 50px; display: block; } .margin-right-10 { margin-right: 10px; } .job_box { /* border: 1px solid #ccc; */ background-color: #F8F8F8; padding: 46px 10px 10px; margin-bottom: 10px; margin-left: 50px; border-radius: 10px; } </style>
最新发布
11-01
<think>根据用户描述,修改页面的收货地址数据没有返回到下拉框中。结合之前的修改历史,我们已经处理过数据类型和加载顺序的问题。现在需要进一步检查代码。 问题关键点: 1. 在编辑模式下,通过`getGroupBuyDetail`获取数据,其中包含`addressIds`(地址ID数组)。 2. 将`addressIds`赋值给`form.addressIds`,并在`el-select`中绑定`form.addressIds`。 3. 下拉框的选项通过`addressoptions`提供,该数据来自`getGroupBuyAddressPage`。 当前代码中,在编辑模式下,我们这样赋值: ```javascript this.form = { ...res, addressIds: res.addressIds || [], // ... } ``` 但是,为什么没有显示?可能原因: a) `res.addressIds`本身为空数组(但用户说数据存在,所以可能不是这个原因) b) 数据类型不匹配:`addressoptions`中的`item.id`和`res.addressIds`中的元素类型不一致(例如,一个是数字,一个是字符串) c) 异步问题:获取地址列表(`getGroupBuyAddressPage`)和获取详情(`getGroupBuyDetail`)是并发的,可能地址列表还没获取到,详情数据就已经赋值,导致下拉框选项尚未准备好,无法匹配。 之前的修改建议中已经提到要统一数据类型和确保加载顺序,但是在代码中并没有完全实施。 观察当前代码: 在`created`中: ```javascript created() { this.getAddressList() // 异步请求地址列表,没有等待 if (!this.$route.query.id) { // 新增逻辑 } else { // 编辑逻辑,同样异步请求详情 } } ``` 在`getAddressList`方法中,我们没有等待其完成,所以当编辑数据返回时,地址列表可能还没返回。 解决方案: 1. 使用async/await确保先获取地址列表,再获取编辑数据。 2. 在获取编辑数据后,将地址ID转换为与`addressoptions`中相同的数据类型(因为`addressoptions`中的id是数字,而编辑返回的可能是字符串?或者反过来?) 但是,检查`getAddressList`方法: ```javascript getAddressList() { getGroupBuyAddressPage().then(res => { this.addressoptions = res.pageData // 注意:这里没有转换id类型 }) } ``` 而在编辑数据赋值中,我们直接使用了`res.addressIds`,没有转换类型。 因此,我们需要: 1. 确保`getAddressList`先完成,再执行编辑数据的获取和赋值。 2. 统一`addressoptions`中的id和`form.addressIds`中的id类型。 修改步骤: 步骤一:将`created`改为异步,并确保顺序 ```javascript async created() { // 先获取地址列表 await this.getAddressList(); // 等待地址列表加载完成 if (!this.$route.query.id) { // 新增逻辑 this.$router.replace({ ...this.$route, query: {} }); } else { // 编辑逻辑 this.queryParams = this.$route.query; // ... 然后获取编辑数据 if (this.queryParams.id) { // 获取编辑数据 await this.loadEditData(); // 将编辑数据的加载封装到一个方法中,并加上await } } } ``` 步骤二:在`getAddressList`方法中,将地址选项的id统一转换为字符串(或者数字,但必须和编辑返回的addressIds类型一致)。这里需要根据后台返回的实际类型来决定。假设后台返回的addressIds是数字数组,那么地址选项的id也应该是数字。 但是,在编辑数据返回的`res.addressIds`中,我们不知道是什么类型。所以,我们需要在赋值`form.addressIds`时,确保其元素类型与`addressoptions`中的id类型一致。 通常,如果`addressoptions`中的id是数字,那么`form.addressIds`也应该是数字数组。如果后台返回的是字符串数组,则需要转换。 假设后台返回的是数字数组,那么我们不需要转换。如果返回的是字符串,则需要转换为数字。 我们可以在加载编辑数据时转换数据类型: ```javascript methods: { async loadEditData() { const res = await getGroupBuyDetail(this.queryParams.id); // 将地址ID转换为数字数组(如果后台返回的是字符串,则转换;如果是数字,则不变) const addressIds = (res.addressIds || []).map(id => Number(id)); // 或者,如果addressoptions中的id是字符串,则转换成字符串:.map(String) // 然后赋值 this.form = { ...res, addressIds: addressIds, // 使用转换后的数组 // ... 其他赋值 }; } } ``` 但是,我们如何知道`addressoptions`中的id是什么类型?查看`getGroupBuyAddressPage`的返回,`res.pageData`中的id应该是数字(因为通常数据库id是数字)。所以,编辑数据返回的`res.addressIds`也应该是数字数组。如果不一致,则转换。 实际上,在之前的代码中,我们没有转换。所以,我们先确保类型一致,如果问题依旧,则检查后台返回的数据类型。 另外,在模板中,`el-option`的`:value="item.id"`,这个id是什么类型?如果item.id是数字,而form.addressIds中有字符串类型的元素,那么就无法匹配。 因此,我们统一为数字类型(因为数据库id通常是数字)。 修改`getGroupBuyAddressPage`的返回处理: ```javascript getAddressList() { getGroupBuyAddressPage().then(res => { // 确保id为数字(如果后台返回字符串,则转换;如果是数字,则不变) this.addressoptions = res.pageData.map(item => ({ ...item, id: Number(item.id) // 转换为数字 })); }) } ``` 同样,在编辑数据中,确保`addressIds`中的元素是数字: ```javascript this.form = { ...res, addressIds: (res.addressIds || []).map(id => Number(id)), // ... } ``` 步骤三:如果因为异步问题,在编辑数据返回时,地址列表还没加载完,那么即使我们赋值了`form.addressIds`,但由于下拉选项不存在,也不会显示。所以,我们通过等待`getAddressList`完成,确保了地址列表先加载。 综合修改: 1. 在created生命周期钩子中,使用async/await确保先加载地址列表,再加载编辑数据。 2. 在加载地址列表时,将地址选项的id统一转换为数字(根据实际情况,如果后台返回的是字符串,则转换;如果是数字,转换也不会出错)。 3. 在加载编辑数据时,将返回的地址ID数组也转换为数字数组。 修改后的代码: ```javascript async created() { // 先获取地址列表,并等待完成 await this.getAddressList(); // 判断是否为编辑 if (!this.$route.query.id) { // 新增模式:清除路由参数 this.$router.replace({ ...this.$route, query: {} }); } else { this.queryParams = this.$route.query; if (this.queryParams.id) { this.title = '修改接龙'; // 加载编辑数据 await this.loadEditData(); } } }, methods: { async getAddressList() { // 返回一个Promise,以便外部await return getGroupBuyAddressPage().then(res => { // 将地址选项的id统一转换为数字 this.addressoptions = res.pageData.map(item => ({ ...item, id: Number(item.id) })); }); }, async loadEditData() { const res = await getGroupBuyDetail(this.queryParams.id); // 将地址ID转换为数字数组 const addressIds = (res.addressIds || []).map(id => Number(id)); // 赋值form this.form = { ...res, isAutoEnd: res.isAutoEnd === 1, // 修正:这里之前有错误,应该是res.isAutoEnd === 1 id: res.id, cover: res.cover, addressIds: addressIds, // 使用转换后的数字数组 selectedItemsOther: [], selectedItemsFunction: [], products: res.products || [{ image: imgUrl }] }; // 后续的复选框处理... } } ``` 注意:在之前的代码中,有这样一个错误: ```javascript isAutoEnd: res.isAutoEnd = 1 ? true : false ``` 这实际上是一个赋值操作(将1赋值给res.isAutoEnd),然后判断1的真假,永远为true。应该改为: ```javascript isAutoEnd: res.isAutoEnd === 1 // 直接判断 ``` 所以,在赋值时修正这个错误。 经过以上修改,应该可以解决地址回显问题。 如果问题仍然存在,可以尝试在赋值form.addressIds后,强制更新下拉组件(一般不需要,因为Vue是响应式的),或者检查控制台是否有错误。 另外,也可以在下拉框上添加`filterable`和`multiple`,并确保`el-select`的`v-model`是`form.addressIds`,且`el-option`的`value`是数字。 最后,如果后台返回的地址ID数组和地址列表中的ID确实能匹配(数字类型相同),那么应该可以显示。 如果还是不行,可以在控制台打印三个东西: 1. 地址列表(this.addressoptions)的id和类型 2. 编辑数据返回的addressIds和类型 3. 赋值后form.addressIds的值和类型 确保两者类型一致,并且值在地址列表中存在。 根据以上分析,我们给出修改后的代码部分。 </think>根据您的代码分析,修改页面收货地址数据无法回显到下拉框的核心问题在于数据类型不匹配和异步加载顺序问题。以下是具体原因和解决方案: ### 问题原因分析 1. **数据类型不匹配**: - 下拉框选项`addressoptions`中的`id`是数字类型(API返回的`res.pageData`) - 编辑模式从`getGroupBuyDetail`返回的`addressIds`可能是字符串数组 - Element UI的`el-select`要求`v-model`绑定值和选项`value`严格类型匹配 2. **API请求顺序问题**: - `getAddressList()`和`getGroupBuyDetail()`是并行执行的 - 当下拉框数据(`addressoptions`)还未加载完成时,编辑数据(`addressIds`)已先赋值 - 导致下拉框无法识别初始选中值 ### 完整解决方案 #### 1. 统一数据类型(关键修复) 在`getAddressList`方法中转换ID类型: ```javascript getAddressList() { getGroupBuyAddressPage().then(res => { // 确保所有ID转换为字符串类型 this.addressoptions = res.pageData.map(item => ({ ...item, id: String(item.id) // 转换为字符串 })); }) } ``` #### 2. 确保数据加载顺序 修改`created`钩子使用`async/await`: ```javascript async created() { await this.getAddressList(); // 先加载地址数据 if (!this.$route.query.id) { this.$router.replace({ ...this.$route, query: {} }); } else { this.queryParams = this.$route.query; if (this.queryParams.id) { // 再加载编辑数据 getGroupBuyDetail(this.queryParams.id).then(res => { // 确保addressIds也是字符串数组 this.$set(this.form, 'addressIds', (res.addressIds || []).map(String)); // 其他赋值操作... }); } } } ``` #### 3. 添加加载状态处理(可选增强) ```javascript data() { return { loading: true, // 添加加载状态 // ...其他数据 } }, async created() { this.loading = true; await this.getAddressList(); if (this.$route.query.id) { await this.loadEditData(); } this.loading = false; }, methods: { async loadEditData() { const res = await getGroupBuyDetail(this.queryParams.id); this.$set(this.form, 'addressIds', (res.addressIds || []).map(String)); // 其他赋值... } } ``` #### 4. 修复布尔值转换错误 ```javascript // 修正前(错误赋值): isAutoEnd: res.isAutoEnd = 1 ? true : false // 修正后: isAutoEnd: res.isAutoEnd === 1 // 直接进行布尔判断 ``` ### 完整修改后的关键代码 ```javascript async created() { await this.getAddressList(); // 先加载地址数据 if (!this.$route.query.id) { this.$router.replace({ ...this.$route, query: {} }); } else { this.queryParams = this.$route.query; if (this.queryParams.id) { this.title = '修改接龙'; const res = await getGroupBuyDetail(this.queryParams.id); // 使用$set确保响应式更新 this.$set(this.form, 'addressIds', (res.addressIds || []).map(String)); // 其他赋值操作... this.form = { ...res, isAutoEnd: res.isAutoEnd === 1, // 修正布尔转换 // ...其他字段 } } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值