活动介绍

n-select回显

时间: 2025-06-23 07:29:34 浏览: 9
### 解决 n-select 组件的回显问题 为了确保 `n-select` 组件能够正确地实现回显功能,需要注意几个关键点。首先,在设置多选模式下,数据类型的匹配至关重要。如果绑定的数据类型与选项中的值类型不一致,则可能导致回显失败。 对于 `n-select` 组件而言,假设其初始状态是从服务器获取了一组已选择项的信息,并希望这些项目能够在页面加载时被自动勾选出来。此时应当保证所传递给组件的选择列表(即模型绑定字段)内的每一个元素都严格对应于可选项数组里的某一项对象或唯一标识符[^2]。 具体操作上可以参照以下做法: - **初始化数据**:确保从后台接收到的数据格式符合预期,特别是当涉及到字符串和数值之间的转换时更需谨慎对待。 - **同步更新视图**:一旦确认了正确的数据结构之后,应该及时通知 Vue 实例重新渲染界面以反映出最新的更改情况。 下面是一个简单的例子来展示如何配置并使 `n-select` 正确回显之前保存过的多个选项: ```javascript // 假设这是来自服务端响应的一部分内容 const selectedIds = [1, 3]; // 已经选择了 ID 为 1 和 3 的两个条目 export default { data() { return { options: [ { label: 'Option A', value: 1 }, { label: 'Option B', value: 2 }, { label: 'Option C', value: 3 } ], modelValue: [] // 这里用来存储当前用户所做的选择,默认为空表示没有任何预置选择 }; }, created() { this.modelValue = selectedIds.map(id => id); // 将已有选择映射成适合 v-model 使用的形式 } }; ``` 此外,考虑到某些场景下的延迟加载需求,比如分页查询远程资源作为候选列表的情况,还需要特别注意处理那些尚未完全加载完成前就已经存在于本地缓存中的记录。可以通过监听事件或者手动触发刷新机制等方式确保所有必要的信息都被正确呈现给最终使用者[^3]。
阅读全文

相关推荐

<template> <el-dialog :title="!dataForm.id ? '新增电池信息' : '修改电池信息'" :close-on-click-modal="false" :visible.sync="visible" width="80%" top="5vh" custom-class="battery-form-dialog" > <el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="180px" label-position="top" > <el-collapse v-model="activePanels" accordion> <el-collapse-item title="电池基本信息" name="1"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="电池ID" prop="batteryId"> <el-input v-model="dataForm.batteryId" placeholder="请输入电池唯一标识"> </el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="电池电压(V)" prop="batteryVoltage"> <el-input v-model="dataForm.batteryVoltage" type="number"> </el-input> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="电池电流(A)" prop="batteryCurrent"> <el-input v-model="dataForm.batteryCurrent" type="number"> </el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="电池SOC(%)" prop="batterySoc"> <el-input v-model="dataForm.batterySoc" type="number" min="0" max="100"> </el-input> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="硬件版本" prop="batteryHardVersion"> <el-input v-model="dataForm.batteryHardVersion"> </el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="软件版本" prop="batterySoftVersion"> <el-input v-model="dataForm.batterySoftVersion"> </el-input> </el-form-item> </el-col> </el-row> </el-collapse-item> <el-collapse-item title="电池状态信息" name="2"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="工作模式" prop="batteryWorkMode"> <el-select v-model="dataForm.batteryWorkMode" placeholder="请选择工作模式"> <el-option label="放电模式 (0x01)" value="1"></el-option> <el-option label="充电模式 (0x10)" value="16"></el-option> <el-option label="保护模式 (0x21)" value="33"></el-option> <el-option label="待机无输出模式 (0x30)" value="48"></el-option> <el-option label="待机预放电模式 (0x31)" value="49"></el-option> <el-option label="故障需返厂 (0xFF)" value="255"></el-option> </el-select> </el-form-item> </el-col> <el-col span="12"> <el-form-item label="电池类型" prop="batteryKind"> <el-select v-model="dataForm.batteryKind" placeholder="请选择电池类型"> <el-option label="运营" value=0></el-option> <el-option label="售后" value=1></el-option> <el-option label="内测" value=2></el-option> <el-option label="报废" value=3></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-form-item label="保护状态码" prop="batteryProtectCode"> <el-tooltip effect="dark" placement="top"> bit0: 放电低温 | bit1: 放电高温 | bit2: 充电高温
bit3: 充电低温 | bit4: 放电过流 | bit5: 放电欠压
bit6: 充电过流 | bit7: 充电过压 | bit8: 短路
bit9: 温差过大 | bit10: 压差过大 | bit11: 智能充电通信超时
bit12: IC保护 | bit13: 预放电失败 | bit14: BMS与Tracker通讯超时
bit15: Gsensor故障 | bit16~31: 保留
<el-input v-model="dataForm.batteryProtectCode" placeholder="请输入保护状态码"> </el-input> </el-tooltip> </el-form-item> <el-form-item label="错误状态码" prop="batteryErrorCode"> <el-tooltip effect="dark" placement="top"> 低8位故障状态码:
bit0: 保留 | bit1: 采样线故障 | bit2: 温度传感器损坏
bit3: IC损坏 | bit4: 充电MOS损坏 | bit5: 放电MOS损坏
bit6: 失衡 | bit7: 失效
高8位工作状态:
bit8: G sensor损坏 | bit9: BMS与Tracker通讯故障
<el-input v-model="dataForm.batteryErrorCode" placeholder="请输入错误状态码"> </el-input> </el-tooltip> </el-form-item> </el-collapse-item> <el-collapse-item title="温度与电压信息" name="3"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="最高温度(℃)" prop="batteryTemperatureMax"> <el-input v-model="dataForm.batteryTemperatureMax" type="number"> </el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="最低温度(℃)" prop="batteryTemperatureMin"> <el-input v-model="dataForm.batteryTemperatureMin" type="number"> </el-input> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="最高电压(V)" prop="batteryVoltageMax"> <el-input v-model="dataForm.batteryVoltageMax" type="number"> </el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="最低电压(V)" prop="batteryVoltageMin"> <el-input v-model="dataForm.batteryVoltageMin" type="number"> </el-input> </el-form-item> </el-col> </el-row> </el-collapse-item> <el-collapse-item title="设备信息" name="4"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="设备型号" prop="model"> <el-input v-model="dataForm.model"> </el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="制造商" prop="manufacture"> <el-input v-model="dataForm.manufacture"> </el-input> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="IMEI" prop="imei"> <el-input v-model="dataForm.imei"> </el-input> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="IMSI" prop="imsi"> <el-input v-model="dataForm.imsi"> </el-input> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="ICCID" prop="iccid"> <el-input v-model="dataForm.iccid"> </el-input> </el-form-item> </el-col> </el-row> </el-collapse-item> <el-collapse-item title="GPS定位信息" name="5"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="经度" prop="longitude"> <el-input v-model="dataForm.longitude" type="number"> </el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="经度方向" prop="longitudeDirection"> <el-select v-model="dataForm.longitudeDirection"> <el-option label="东经" value="E"></el-option> <el-option label="西经" value="W"></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="纬度" prop="latitude"> <el-input v-model="dataForm.latitude" type="number"> </el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="纬度方向" prop="latitudeDirection"> <el-select v-model="dataForm.latitudeDirection"> <el-option label="北纬" value="N"></el-option> <el-option label="南纬" value="S"></el-option> </el-select> </el-form-item> </el-col> </el-row> </el-collapse-item> <el-collapse-item title="业务信息" name="6"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="业务模式" prop="businessmode"> <el-select v-model="dataForm.businessmode"> <el-option v-for="mode in businessModes" :key="mode.value" :label="mode.label" :value="mode.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="处理状态" prop="handled"> <el-select v-model="dataForm.handled"> <el-option label="已处理" :value="1"></el-option> <el-option label="待处理" :value="0"></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="执行方式" prop="todoNow"> <el-select v-model="dataForm.todoNow"> <el-option label="立即执行" :value="1"></el-option> <el-option label="等待执行" :value="0"></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="需要回复" prop="needack"> <el-select v-model="dataForm.needack"> <el-option label="是" :value="1"></el-option> <el-option label="否" :value="0"></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-form-item label="消息类型" prop="flag"> <el-select v-model="dataForm.flag"> <el-option label="设备主动请求或上报" :value="1"></el-option> <el-option label="平台主动推送或下指令" :value="2"></el-option> <el-option label="平台指令反馈的结果" :value="3"></el-option> </el-select> </el-form-item> </el-collapse-item> <el-collapse-item title="其他信息" name="7"> <el-form-item label="原始数据" prop="payload"> <el-input v-model="dataForm.payload" type="textarea" :rows="3" placeholder="请输入原始数据" ></el-input> </el-form-item> </el-collapse-item> </el-collapse> </el-form> <el-button @click="visible = false">取消</el-button> <el-button type="primary" @click="dataFormSubmit()">确定</el-button> </el-dialog> </template> <script> export default { data() { return { visible: false, activePanels: ['1'], dataForm: { id: 0, batteryId: '', batteryVoltage: '', batteryCurrent: '', batterySoc: '', batteryHardVersion: '', batterySoftVersion: '', batteryWorkMode: '', batteryKind: 0, batteryProtectCode: '', batteryErrorCode: '', batteryTemperatureMax: '', batteryTemperatureMin: '', batteryVoltageMax: '', batteryVoltageMin: '', mosStatus: '', mosTemp: '', batteryCycleTimes: '', steadyStatus: '', cellVoltage: '', model: '', manufacture: '', imei: '', imsi: '', iccid: '', trackerHardwareVersion: '', trackerSoftwareVersion: '', csq: '', networkType: '', locationMode: '', longitude: '', longitudeDirection: '', latitude: '', latitudeDirection: '', gpsSpeed: '', gpsSignal: '', satelliteNum: '', accuracy: '', flag: '', clientId: '', topic: '', productKey: '', handled: '', todoNow: '', needack: '', businessmode: '', uploadTime: '', createTime: '', updateTime: '', payload: '' }, businessModes: [ { value: 1, label: "关机模式" }, { value: 2, label: "运营模式" }, { value: 3, label: "运输模式" }, { value: 4, label: "通讯充电模式" }, { value: 5, label: "盲充模式" }, { value: 6, label: "存储模式" }, { value: 7, label: "搜寻模式" }, { value: 8, label: "返厂模式" } ], dataRule: { batteryId: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryVoltage: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryCurrent: [ { required: true, message: '不能为空', trigger: 'blur' } ], batterySoc: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryHardVersion: [ { required: true, message: '不能为空', trigger: 'blur' } ], batterySoftVersion: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryWorkMode: [ { required: true, message: '0x01(1):放电模式 0x10(16):充电模式 0x21(33):保护模式 0x30(48):待机无输出模式 0x31(49):待机预放电模式 0xFF(255):故障需返厂不能为空', trigger: 'blur' } ], batteryProtectCode: [ { required: true, message: '保护状态 bit16~bit 31 保留 bit15:Gsensor 故障 bit14:BMS 与 Tracker通讯超时 bit13: 预放电失败 bit12: IC保护 bit11: 智能充电通信超时 bit10:压差过大 bit9:温差过大 bit8:短路 bit7:充电过压 bit6:充电过流 bit5:放电欠压 bit4:放电过流 bit3:充电低温 bit2:充电高温 bit1:放电高温 bit0:放电低温不能为空', trigger: 'blur' } ], batteryErrorCode: [ { required: true, message: '转成2进制, 总共为16位,高8位为电池包工作状态,低8位为故障状态码 bit9BMS 与 Tracker通讯故障,bit8BMS 板 G sensor损坏,(8/9无效)bit7:失效,bit6:失衡,bit5:放电 MOS 损坏,bit4:充电 MOS 损坏,bit3IC 损坏,bit2:温度传感器损坏,bit1:采样线断线,虚焊等故障 保留,置 0,bit0:保留不能为空', trigger: 'blur' } ], batteryTemperatureMax: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryTemperatureMin: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryVoltageMax: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryVoltageMin: [ { required: true, message: '不能为空', trigger: 'blur' } ], mosStatus: [ { required: true, message: '不能为空', trigger: 'blur' } ], mosTemp: [ { required: true, message: '不能为空', trigger: 'blur' } ], batteryCycleTimes: [ { required: true, message: '不能为空', trigger: 'blur' } ], steadyStatus: [ { required: true, message: '不能为空', trigger: 'blur' } ], cellVoltage: [ { required: true, message: '不能为空', trigger: 'blur' } ], model: [ { required: true, message: '不能为空', trigger: 'blur' } ], manufacture: [ { required: true, message: '不能为空', trigger: 'blur' } ], imei: [ { required: true, message: '不能为空', trigger: 'blur' } ], imsi: [ { required: true, message: '不能为空', trigger: 'blur' } ], iccid: [ { required: true, message: '不能为空', trigger: 'blur' } ], trackerHardwareVersion: [ { required: true, message: '不能为空', trigger: 'blur' } ], trackerSoftwareVersion: [ { required: true, message: '不能为空', trigger: 'blur' } ], csq: [ { required: true, message: '不能为空', trigger: 'blur' } ], networkType: [ { required: true, message: '不能为空', trigger: 'blur' } ], locationMode: [ { required: true, message: '不能为空', trigger: 'blur' } ], longitude: [ { required: true, message: '不能为空', trigger: 'blur' } ], longitudeDirection: [ { required: true, message: '不能为空', trigger: 'blur' } ], latitude: [ { required: true, message: '不能为空', trigger: 'blur' } ], latitudeDirection: [ { required: true, message: '不能为空', trigger: 'blur' } ], gpsSpeed: [ { required: true, message: '不能为空', trigger: 'blur' } ], gpsSignal: [ { required: true, message: '不能为空', trigger: 'blur' } ], satelliteNum: [ { required: true, message: '不能为空', trigger: 'blur' } ], accuracy: [ { required: true, message: '不能为空', trigger: 'blur' } ], flag: [ { required: true, message: '1:设备主动请求或上报 2:平台主动推送或下指令 3:平台指令反馈的结果不能为空', trigger: 'blur' } ], clientId: [ { required: true, message: 'MQTT发布消息所需的clientId不能为空', trigger: 'blur' } ], topic: [ { required: true, message: 'MQTT主题不能为空', trigger: 'blur' } ], productKey: [ { required: true, message: '主题名称中需要的productKey不能为空', trigger: 'blur' } ], handled: [ { required: true, message: '配置是否处理 1:已处理 0:待处理不能为空', trigger: 'blur' } ], todoNow: [ { required: true, message: '是否立即执行 0:等待执行 1:立即执行不能为空', trigger: 'blur' } ], needack: [ { required: true, message: '是否回复不能为空', trigger: 'blur' } ], businessmode: [ { required: true, message: '业务模式 1:关机模式 2:运营模式 3:运输模式 4:通讯充电模式 5:盲充模式 6:存储模式 7:搜寻模式 8:返厂模式不能为空', trigger: 'blur' } ], uploadTime: [ { required: true, message: '不能为空', trigger: 'blur' } ], createTime: [ { required: true, message: '不能为空', trigger: 'blur' } ], updateTime: [ { required: true, message: '更新时间不能为空', trigger: 'blur' } ], payload: [ { required: true, message: '原始数据不能为空', trigger: 'blur' } ] } } }, computed: { // 处理状态标签 handledLabel() { return this.dataForm.handled === 1 ? "已处理" : this.dataForm.handled === 0 ? "待处理" : ""; }, // 执行方式标签 todoNowLabel() { return this.dataForm.todoNow === 1 ? "立即执行" : this.dataForm.todoNow === 0 ? "等待执行" : ""; }, // 需要回复标签 needackLabel() { return this.dataForm.needack === 1 ? "是" : this.dataForm.needack === 0 ? "否" : ""; }, // 业务模式标签 businessModeLabel() { const mode = this.businessModes.find(m => m.value == this.dataForm.businessmode); return mode ? mode.label : ""; }, // 消息类型标签 flagLabel() { switch (this.dataForm.flag) { case 1: return "设备主动请求或上报"; case 2: return "平台主动推送或下指令"; case 3: return "平台指令反馈的结果"; default: return ""; } } }, methods: { init (id) { this.dataForm.id = id || 0 this.visible = true this.$nextTick(() => { this.$refs['dataForm'].resetFields() if (this.dataForm.id) { this.$http({ url: this.$http.adornUrl(/maya/mybatteryinfo/info/${this.dataForm.id}), method: 'get', params: this.$http.adornParams() }).then(({data}) => { if (data && data.code === 0) { this.dataForm.batteryId = data.myBatteryInfo.batteryId this.dataForm.batteryVoltage = data.myBatteryInfo.batteryVoltage this.dataForm.batteryCurrent = data.myBatteryInfo.batteryCurrent this.dataForm.batterySoc = data.myBatteryInfo.batterySoc this.dataForm.batteryHardVersion = data.myBatteryInfo.batteryHardVersion this.dataForm.batterySoftVersion = data.myBatteryInfo.batterySoftVersion this.dataForm.batteryWorkMode = data.myBatteryInfo.batteryWorkMode this.dataForm.batteryKind = data.myBatteryInfo.batteryKind this.dataForm.batteryProtectCode = data.myBatteryInfo.batteryProtectCode this.dataForm.batteryErrorCode = data.myBatteryInfo.batteryErrorCode this.dataForm.batteryTemperatureMax = data.myBatteryInfo.batteryTemperatureMax this.dataForm.batteryTemperatureMin = data.myBatteryInfo.batteryTemperatureMin this.dataForm.batteryVoltageMax = data.myBatteryInfo.batteryVoltageMax this.dataForm.batteryVoltageMin = data.myBatteryInfo.batteryVoltageMin this.dataForm.mosStatus = data.myBatteryInfo.mosStatus this.dataForm.mosTemp = data.myBatteryInfo.mosTemp this.dataForm.batteryCycleTimes = data.myBatteryInfo.batteryCycleTimes this.dataForm.steadyStatus = data.myBatteryInfo.steadyStatus this.dataForm.cellVoltage = data.myBatteryInfo.cellVoltage this.dataForm.model = data.myBatteryInfo.model this.dataForm.manufacture = data.myBatteryInfo.manufacture this.dataForm.imei = data.myBatteryInfo.imei this.dataForm.imsi = data.myBatteryInfo.imsi this.dataForm.iccid = data.myBatteryInfo.iccid this.dataForm.trackerHardwareVersion = data.myBatteryInfo.trackerHardwareVersion this.dataForm.trackerSoftwareVersion = data.myBatteryInfo.trackerSoftwareVersion this.dataForm.csq = data.myBatteryInfo.csq this.dataForm.networkType = data.myBatteryInfo.networkType this.dataForm.locationMode = data.myBatteryInfo.locationMode this.dataForm.longitude = data.myBatteryInfo.longitude this.dataForm.longitudeDirection = data.myBatteryInfo.longitudeDirection this.dataForm.latitude = data.myBatteryInfo.latitude this.dataForm.latitudeDirection = data.myBatteryInfo.latitudeDirection this.dataForm.gpsSpeed = data.myBatteryInfo.gpsSpeed this.dataForm.gpsSignal = data.myBatteryInfo.gpsSignal this.dataForm.satelliteNum = data.myBatteryInfo.satelliteNum this.dataForm.accuracy = data.myBatteryInfo.accuracy this.dataForm.flag = data.myBatteryInfo.flag this.dataForm.clientId = data.myBatteryInfo.clientId this.dataForm.topic = data.myBatteryInfo.topic this.dataForm.productKey = data.myBatteryInfo.productKey this.dataForm.handled = data.myBatteryInfo.handled this.dataForm.todoNow = data.myBatteryInfo.todoNow this.dataForm.needack = data.myBatteryInfo.needack this.dataForm.businessmode = data.myBatteryInfo.businessmode this.dataForm.uploadTime = data.myBatteryInfo.uploadTime this.dataForm.createTime = data.myBatteryInfo.createTime this.dataForm.updateTime = data.myBatteryInfo.updateTime this.dataForm.payload = data.myBatteryInfo.payload } }) } }) }, // 表单提交 dataFormSubmit () { this.$refs['dataForm'].validate((valid) => { if (valid) { this.$http({ url: this.$http.adornUrl(/maya/mybatteryinfo/${!this.dataForm.id ? 'save' : 'update'}), method: 'post', data: this.$http.adornData({ 'id': this.dataForm.id || undefined, 'batteryId': this.dataForm.batteryId, 'batteryVoltage': this.dataForm.batteryVoltage, 'batteryCurrent': this.dataForm.batteryCurrent, 'batterySoc': this.dataForm.batterySoc, 'batteryHardVersion': this.dataForm.batteryHardVersion, 'batterySoftVersion': this.dataForm.batterySoftVersion, 'batteryWorkMode': this.dataForm.batteryWorkMode, 'batteryKind': this.dataForm.batteryKind, 'batteryProtectCode': this.dataForm.batteryProtectCode, 'batteryErrorCode': this.dataForm.batteryErrorCode, 'batteryTemperatureMax': this.dataForm.batteryTemperatureMax, 'batteryTemperatureMin': this.dataForm.batteryTemperatureMin, 'batteryVoltageMax': this.dataForm.batteryVoltageMax, 'batteryVoltageMin': this.dataForm.batteryVoltageMin, 'mosStatus': this.dataForm.mosStatus, 'mosTemp': this.dataForm.mosTemp, 'batteryCycleTimes': this.dataForm.batteryCycleTimes, 'steadyStatus': this.dataForm.steadyStatus, 'cellVoltage': this.dataForm.cellVoltage, 'model': this.dataForm.model, 'manufacture': this.dataForm.manufacture, 'imei': this.dataForm.imei, 'imsi': this.dataForm.imsi, 'iccid': this.dataForm.iccid, 'trackerHardwareVersion': this.dataForm.trackerHardwareVersion, 'trackerSoftwareVersion': this.dataForm.trackerSoftwareVersion, 'csq': this.dataForm.csq, 'networkType': this.dataForm.networkType, 'locationMode': this.dataForm.locationMode, 'longitude': this.dataForm.longitude, 'longitudeDirection': this.dataForm.longitudeDirection, 'latitude': this.dataForm.latitude, 'latitudeDirection': this.dataForm.latitudeDirection, 'gpsSpeed': this.dataForm.gpsSpeed, 'gpsSignal': this.dataForm.gpsSignal, 'satelliteNum': this.dataForm.satelliteNum, 'accuracy': this.dataForm.accuracy, 'flag': this.dataForm.flag, 'clientId': this.dataForm.clientId, 'topic': this.dataForm.topic, 'productKey': this.dataForm.productKey, 'handled': this.dataForm.handled, 'todoNow': this.dataForm.todoNow, 'needack': this.dataForm.needack, 'businessmode': this.dataForm.businessmode, 'uploadTime': this.dataForm.uploadTime, 'createTime': this.dataForm.createTime, 'updateTime': this.dataForm.updateTime, 'payload': this.dataForm.payload }) }).then(({data}) => { if (data && data.code === 0) { this.$message({ message: '操作成功', type: 'success', duration: 1500, onClose: () => { this.visible = false this.$emit('refreshDataList') } }) } else { this.$message.error(data.msg) } }) } }) } } } </script> <style scoped> /* 添加状态显示样式 */ .status-display { margin-top: 8px; padding: 6px 12px; background-color: #f5f7fa; border-radius: 4px; border: 1px solid #dcdfe6; font-size: 14px; color: #606266; } /* 在查看模式下隐藏选择框,显示纯文本 */ .view-mode .el-select { display: none; } .view-mode .status-display { display: block; } </style> 为什么电池类型显示默认为数字,不对,帮我修复

static void handle_client(int client_fd) { uint8_t buffer[MAX_PDU_LEN + 7]; // MBAP头7字节 + PDU ssize_t len; printf("打\n"); // 设置接收超时 /*struct timeval tv = {RECV_TIMEOUT, 0}; setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); while (running) { len = read(client_fd, buffer, sizeof(buffer)); if (len <= 0) { if (len == 0) printf("客户端主动断开连接\n"); else { perror("读取数据失败"); } break; } */ while (running) { // 设置select超时 struct timeval tv = { .tv_sec = RECV_TIMEOUT, .tv_usec = 0 }; fd_set read_fds; FD_ZERO(&read_fds); FD_SET(client_fd, &read_fds); // 等待数据到达或超时 int ret = select(client_fd + 1, &read_fds, NULL, NULL, &tv); if (ret == -1) { perror("select错误"); break; } else if (ret == 0) { fprintf(stderr, "接收超时(%d秒未活动)\n", RECV_TIMEOUT); continue;; } printf("打印\n"); // 确认有数据可读 if (FD_ISSET(client_fd, &read_fds)) { len = read(client_fd, buffer, sizeof(buffer)); if (len <= 0) { if (len == 0) printf("客户端主动断开连接\n"); else perror("读取数据失败"); break; } printf("成功\n"); // 解析MBAP头 uint16_t trans_id = (buffer[0] << 8) | buffer[1]; int pdu_len; if (parse_mbap(buffer, len, &pdu_len) != 0) { send_exception_response(client_fd, trans_id, buffer[7], 0x0B); // 0x0B: 网关路径不可用 continue; } printf("打功\n"); // 处理请求 pthread_mutex_lock(&data_mutex); printf("检查返回\n"); int resp_len = handle_modbus_request(buffer + 7); // PDU起始位置 printf("返回\n"); pthread_mutex_unlock(&data_mutex); printf("检查\n"); if (resp_len > 0) { // 构造MBAP头 uint8_t header[7] = { buffer[0], buffer[1], // 回显事务ID 0x00, 0x00, // 协议ID (uint8_t)((resp_len + 1) >> 8), // 长度高字节 (uint8_t)(resp_len + 1), // 长度低字节 SLAVE_ADDR // Unit ID }; printf("打息\n"); // 发送响应 uint8_t response[7 + MAX_PDU_LEN]; memcpy(response, header, 7); memcpy(response + 7, g_resp_buffer, resp_len); ssize_t sent = write(client_fd, response, 7 + resp_len); if (sent != 7 + resp_len) { perror("响应发送不完整"); } printf("信\n"); } } } close(client_fd); printf("客户端连接关闭\n"); }

**谷粒随享** ## 第7章 专辑/声音详情 **学习目标:** - 专辑详情业务需求 - 专辑服务 1.专辑信息 2.分类信息 3.统计信息 4,主播信息 - 搜索服务:汇总专辑详情数据 - 专辑包含**声音列表(付费标识动态展示)** - MongoDB文档型数据库应用 - 基于**MongoDB**存储用户对于声音**播放进度** - 基于Redis实现排行榜(将不同分类下包含各个维度热门专辑排行) # 1、专辑详情 ![详情-专辑详情和声音列表](assets/详情-专辑详情和声音列表.gif) 专辑详情页面渲染需要以下四项数据: - **albumInfo**:当前专辑信息 - **albumStatVo**:专辑统计信息 - **baseCategoryView**:专辑分类信息 - **announcer**:专辑主播信息 因此接下来,我们需要在**专辑微服务**、**用户微服务**中补充RestFul接口实现 并且 提供远程调用Feign API接口给**搜索微服务**来调用获取。 在专辑**搜索微服务**中编写控制器**汇总专辑详情**所需**数据**: 以下是详情需要获取到的数据集 1. 通过专辑Id 获取专辑数据{已存在} 2. 通过专辑Id 获取专辑统计信息**{不存在}** 3. 通过三级分类Id 获取到分类数据{已存在} 4. 通过用户Id 获取到主播信息{存在} ## 1.1 服务提供方提供接口 ### 1.1.1 根据专辑Id 获取专辑数据(已完成) ### 1.1.2 根据三级分类Id获取到分类信息(已完成) ### 1.1.3 根据用户Id 获取主播信息(已完成) ### 1.1.4 根据专辑Id 获取统计信息 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/67 **AlbumInfoApiController** 控制器 java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ @Operation(summary = "根据专辑ID查询专辑统计信息") @GetMapping("/albumInfo/getAlbumStatVo/{albumId}") public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId) { AlbumStatVo albumStatVo = albumInfoService.getAlbumStatVo(albumId); return Result.ok(albumStatVo); } **AlbumInfoService**接口 java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ AlbumStatVo getAlbumStatVo(Long albumId); **AlbumInfoServiceImpl**实现类 java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ @Override public AlbumStatVo getAlbumStatVo(Long albumId) { return albumInfoMapper.getAlbumStatVo(albumId); } **albumInfoMapper.java** java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ AlbumStatVo getAlbumStatVo(@Param("albumId") Long albumId); **albumInfoMapper.xml** sql <select id="getAlbumStatVo" resultType="com.atguigu.tingshu.vo.album.AlbumStatVo"> select stat.album_id, max(if(stat.stat_type='0401', stat.stat_num, 0)) playStatNum, max(if(stat.stat_type='0402', stat.stat_num, 0)) subscribeStatNum, max(if(stat.stat_type='0403', stat.stat_num, 0)) buyStatNum, max(if(stat.stat_type='0404', stat.stat_num, 0)) commentStatNum from album_stat stat where stat.album_id = #{albumId} and stat.is_deleted = 0 group by stat.album_id </select> service-album-client模块**AlbumFeignClient** 接口中添加 java /** * 根据专辑ID查询专辑统计信息 * * @param albumId * @return */ @GetMapping("/albumInfo/getAlbumStatVo/{albumId}") public Result<AlbumStatVo> getAlbumStatVo(@PathVariable Long albumId); **AlbumDegradeFeignClient**熔断类: java @Override public Result<AlbumStatVo> getAlbumStatVo(Long albumId) { log.error("[专辑模块]提供远程调用方法getAlbumStatVo服务降级"); return null; } ## 1.2 服务调用方汇总数据 回显时,后台需要提供将数据封装到map集合中; java result.put("albumInfo", albumInfo); 获取专辑信息 result.put("albumStatVo", albumStatVo); 获取专辑统计信息 result.put("baseCategoryView", baseCategoryView); 获取分类信息 result.put("announcer", userInfoVo); 获取主播信息 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/69 在service-search 微服务**itemApiController** 控制器中添加 java package com.atguigu.tingshu.search.api; import com.atguigu.tingshu.common.result.Result; import com.atguigu.tingshu.search.service.ItemService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @Tag(name = "专辑详情管理") @RestController @RequestMapping("api/search") @SuppressWarnings({"all"}) public class itemApiController { @Autowired private ItemService itemService; /** * 根据专辑ID查询专辑详情相关数据 * * @param albumId * @return */ @Operation(summary = "根据专辑ID查询专辑详情相关数据") @GetMapping("/albumInfo/{albumId}") public Result<Map<String, Object>> getItemInfo(@PathVariable Long albumId) { Map<String, Object> mapResult = itemService.getItemInfo(albumId); return Result.ok(mapResult); } } 接口与实现 java package com.atguigu.tingshu.search.service; import java.util.Map; public interface ItemService { /** * 根据专辑ID查询专辑详情相关数据 * * @param albumId * @return */ Map<String, Object> getItemInfo(Long albumId); } java package com.atguigu.tingshu.search.service.impl; import cn.hutool.core.lang.Assert; import com.atguigu.tingshu.album.AlbumFeignClient; import com.atguigu.tingshu.model.album.AlbumInfo; import com.atguigu.tingshu.model.album.BaseCategoryView; import com.atguigu.tingshu.search.service.ItemService; import com.atguigu.tingshu.user.client.UserFeignClient; import com.atguigu.tingshu.vo.album.AlbumStatVo; import com.atguigu.tingshu.vo.user.UserInfoVo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadPoolExecutor; @Slf4j @Service @SuppressWarnings({"all"}) public class ItemServiceImpl implements ItemService { @Autowired private AlbumFeignClient albumFeignClient; @Autowired private UserFeignClient userFeignClient; @Autowired private ThreadPoolExecutor threadPoolExecutor; /** * 根据专辑ID查询专辑详情相关数据 * 1.albumInfo:当前专辑信息 * 2.albumStatVo:专辑统计信息 * 3.baseCategoryView:专辑分类信息 * 4.announcer:专辑主播信息 * * @param albumId * @return */ @Override public Map<String, Object> getItemInfo(Long albumId) { //1.创建响应结果Map对象 HashMap在多线程环境下并发读写线程不安全:导致key覆盖;导致死循环 //采用线程安全:ConcurrentHashMap Map<String, Object> mapResult = new ConcurrentHashMap<>(); //2.远程调用专辑服务获取专辑基本信息-封装albumInfo属性 CompletableFuture<AlbumInfo> albumInfoCompletableFuture = CompletableFuture.supplyAsync(() -> { AlbumInfo albumInfo = albumFeignClient.getAlbumInfo(albumId).getData(); Assert.notNull(albumInfo, "专辑:{}不存在", albumId); mapResult.put("albumInfo", albumInfo); return albumInfo; }, threadPoolExecutor); //3.远程调用专辑服务获取专辑统计信息-封装albumStatVo属性 CompletableFuture<Void> albumStatCompletableFuture = CompletableFuture.runAsync(() -> { AlbumStatVo albumStatVo = albumFeignClient.getAlbumStatVo(albumId).getData(); Assert.notNull(albumStatVo, "专辑统计信息:{}不存在", albumId); mapResult.put("albumStatVo", albumStatVo); }, threadPoolExecutor); //4.远程调用专辑服务获取专辑分类信息-封装baseCategoryView属性 CompletableFuture<Void> baseCategoryViewCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> { BaseCategoryView categoryView = albumFeignClient.getCategoryView(albumInfo.getCategory3Id()).getData(); Assert.notNull(categoryView, "分类:{}不存在", albumInfo.getCategory3Id()); mapResult.put("baseCategoryView", categoryView); }, threadPoolExecutor); //5.远程调用用户服务获取主播信息-封装announcer属性 CompletableFuture<Void> announcerCompletableFuture = albumInfoCompletableFuture.thenAcceptAsync(albumInfo -> { UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(albumInfo.getUserId()).getData(); Assert.notNull(userInfoVo, "用户:{}不存在", albumInfo.getUserId()); mapResult.put("announcer", userInfoVo); }, threadPoolExecutor); //6.组合异步任务,阻塞等待所有异步任务执行完毕 CompletableFuture.allOf( albumInfoCompletableFuture, albumStatCompletableFuture, baseCategoryViewCompletableFuture, announcerCompletableFuture ).join(); return mapResult; } } ## 1.3 获取专辑声音列表 查询时,评论数关键字属性:commentStatNum ![](assets/image-20231005140148572.png) 需求:根据专辑ID分页查询声音列表,返回当前页10条记录,对每条声音付费标识处理。**关键点:哪个声音需要展示付费标识。** **默认每个声音付费标识为:false** 判断专辑付费类型:0101-免费、**0102-vip免费、0103-付费** - 用户未登录 - 专辑类型不是免费,将除了免费可以试听声音外,将本页中其余声音付费标识设置:true - 用户登录(获取是否为VIP) - 不是VIP,或者VIP过期(除了免费以外声音全部设置为付费) - 是VIP,专辑类型为付费 需要进行处理 - 统一处理需要付费情况 - 获取用户购买情况(专辑购买,或者声音购买)得到每个声音购买状态 - 判断根据用户购买情况设置声音付费标识 ### 1.3.1 获取用户声音列表付费情况 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/87 user_paid_album 这张表记录了用户购买过的专辑 user_paid_track 这张表记录了用户购买过的声音 如果购买过,则在map 中存储数据 key=trackId value = 1 未购买value则返回0 例如: - 某专辑第一页,除了试听的声音(前五)从6-10个声音需要在用户微服务中判断5个声音是否购买过 - 用户翻到第二页,从11-20个声音同样需要判断用户购买情况 **UserInfoApiController** 控制器: java /** * 该接口提供给给专辑服务,展示声音列表动态判断付费标识 * 判断当前用户某一页中声音列表购买情况 * * @param userId 用户ID * @param albumId 专辑ID * @param needChackTrackIdList 待检查购买情况声音列表 * @return data:{声音ID:购买结果} 结果:1(已购)0(未购买) */ @Operation(summary = "判断当前用户某一页中声音列表购买情况") @PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}") public Result<Map<Long, Integer>> userIsPaidTrack( @PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> needChackTrackIdList) { Map<Long, Integer> mapResult = userInfoService.userIsPaidTrack(userId, albumId, needChackTrackIdList); return Result.ok(mapResult); } **UserInfoService接口**: java /** * 判断当前用户某一页中声音列表购买情况 * * @param userId 用户ID * @param albumId 专辑ID * @param needChackTrackIdList 待检查购买情况声音列表 * @return data:{声音ID:购买结果} 结果:1(已购)0(未购买) */ Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needChackTrackIdList); **UserInfoServiceImpl实现类**: java @Autowired private UserPaidAlbumMapper userPaidAlbumMapper; @Autowired private UserPaidTrackMapper userPaidTrackMapper; /** * 判断当前用户某一页中声音列表购买情况 * * @param userId 用户ID * @param albumId 专辑ID * @param needChackTrackIdList 待检查购买情况声音列表 * @return data:{声音ID:购买结果} 结果:1(已购)0(未购买) */ @Override public Map<Long, Integer> userIsPaidTrack(Long userId, Long albumId, List<Long> needChackTrackIdList) { //1.根据用户ID+专辑ID查询已购专辑表 LambdaQueryWrapper<UserPaidAlbum> userPaidAlbumLambdaQueryWrapper = new LambdaQueryWrapper<>(); userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getAlbumId, albumId); userPaidAlbumLambdaQueryWrapper.eq(UserPaidAlbum::getUserId, userId); Long count = userPaidAlbumMapper.selectCount(userPaidAlbumLambdaQueryWrapper); if (count > 0) { //1.1 存在专辑购买记录-用户购买过该专辑,将待检查声音列表购买情况设置为:1 Map<Long, Integer> mapResult = new HashMap<>(); for (Long trackId : needChackTrackIdList) { mapResult.put(trackId, 1); } return mapResult; } //2. 不存在专辑购买记录-根据用户ID+声音列表查询已购声音表 LambdaQueryWrapper<UserPaidTrack> userPaidTrackLambdaQueryWrapper = new LambdaQueryWrapper<>(); userPaidTrackLambdaQueryWrapper.eq(UserPaidTrack::getUserId, userId); userPaidTrackLambdaQueryWrapper.in(UserPaidTrack::getTrackId, needChackTrackIdList); //获取本页中已购买声音列表 List<UserPaidTrack> userPaidTrackList = userPaidTrackMapper.selectList(userPaidTrackLambdaQueryWrapper); //2.1 不存在声音购买记录-将待检查声音列表购买情况设置为:0 if (CollectionUtil.isEmpty(userPaidTrackList)) { Map<Long, Integer> mapResult = new HashMap<>(); for (Long trackId : needChackTrackIdList) { mapResult.put(trackId, 0); } return mapResult; } //2.2 存在声音购买记录-循环判断待检查声音ID找出哪些是已购,哪些是未购买 List<Long> userPaidTrackIdList = userPaidTrackList.stream().map(UserPaidTrack::getTrackId).collect(Collectors.toList()); Map<Long, Integer> mapResult = new HashMap<>(); for (Long needCheckTrackId : needChackTrackIdList) { //如果待检查声音ID包含在已购声音Id集合中(已购买) if (userPaidTrackIdList.contains(needCheckTrackId)) { mapResult.put(needCheckTrackId, 1); } else { //反之则未购买声音 mapResult.put(needCheckTrackId, 0); } } return mapResult; } service-user-client模块中**UserFeignClient** 远程调用接口中添加: java /** * 该接口提供给给专辑服务,展示声音列表动态判断付费标识 * 判断当前用户某一页中声音列表购买情况 * * @param userId 用户ID * @param albumId 专辑ID * @param needChackTrackIdList 待检查购买情况声音列表 * @return data:{声音ID:购买结果} 结果:1(已购)0(未购买) */ @PostMapping("/userInfo/userIsPaidTrack/{userId}/{albumId}") public Result<Map<Long, Integer>> userIsPaidTrack( @PathVariable Long userId, @PathVariable Long albumId, @RequestBody List<Long> needChackTrackIdList); **UserDegradeFeignClient熔断类**: java @Override public Result<Map<Long, Integer>> userIsPaidTrack(Long userId, Long albumId, List<Long> needChackTrackIdList) { log.error("[用户服务]提供远程调用方法userIsPaidTrack执行服务降级"); return null; } ### 1.3.2 查询专辑声音列表 在service-album 微服务中添加控制器. 获取专辑声音列表时,我们将数据都统一封装到**AlbumTrackListVo**实体类中 > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/89 **TrackInfoApiController控制器** java /** * 用于小程序端专辑页面展示分页声音列表,动态根据用户展示声音付费标识 * * @param albumId * @param page * @param limit * @return */ @GuiGuLogin(required = false) @Operation(summary = "用于小程序端专辑页面展示分页声音列表,动态根据用户展示声音付费标识") @GetMapping("/trackInfo/findAlbumTrackPage/{albumId}/{page}/{limit}") public Result> getAlbumTrackPage(@PathVariable Long albumId, @PathVariable Integer page, @PathVariable Integer limit) { //1.获取用户ID Long userId = AuthContextHolder.getUserId(); //2.封装分页对象 Page<AlbumTrackListVo> pageInfo = new Page<>(page, limit); //3.调用业务层封装分页对象 pageInfo = trackInfoService.getAlbumTrackPage(pageInfo, albumId, userId); return Result.ok(pageInfo); } **TrackInfoService接口:** java /** * 用于小程序端专辑页面展示分页声音列表,动态根据用户展示声音付费标识 * * @param pageInfo MP分页对象 * @param albumId 专辑ID * @param userId 用户ID * @return */ Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, Long albumId, Long userId); **TrackInfoServiceImpl实现类:** - 根据专辑Id 获取到专辑列表, - 用户为空的时候,然后找出哪些是需要付费的声音并显示付费 isShowPaidMark=true 付费类型: 0101-免费 0102-vip付费 0103-付费 - ​ 用户不为空的时候 - 判断用户的类型 - vip 免费类型 - 如果不是vip 需要付费 - 如果是vip 但是已经过期了 也需要付费 - 需要付费 - 统一处理需要付费业务 ​ 获取到声音Id列表集合 与 用户购买声音Id集合进行比较 将用户购买的声音存储到map中,key=trackId value = 1或0; 1:表示购买过,0:表示没有购买过 如果声音列表不包含,则将显示为付费,否则判断用户是否购买过声音,没有购买过设置为付费 java @Autowired private UserFeignClient userFeignClient; /** * 分页获取专辑下声音列表,动态根据用户情况展示声音付费标识 * * @param userId 用户ID * @param albumId 专辑ID * @param pageInfo 分页对象 * @return */ @Override public Page<AlbumTrackListVo> getAlbumTrackPage(Long userId, Long albumId, Page<AlbumTrackListVo> pageInfo) { //1.根据专辑ID分页获取该专辑下包含声音列表(包含声音统计信息)-默认声音付费标识为false pageInfo = albumInfoMapper.getAlbumTrackPage(pageInfo, albumId); //2.TODO 动态判断当前页中每个声音付费标识 关键点:找出付费情况 //2.根据专辑ID查询专辑信息 AlbumInfo albumInfo = albumInfoMapper.selectById(albumId); Assert.notNull(albumInfo, "专辑:{}不存在", albumId); String payType = albumInfo.getPayType(); //3.处理用户未登录情况 if (userId == null) { //3.1 判断专辑付费类型:VIP免费(0102)或 付费(0103) 除了免费试听外声音都应该设置付费标识 if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(payType) || SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(payType)) { //3.2 获取本页中声音列表,过滤将声音序号大于免费试听集数声音付费标识设置为true pageInfo.getRecords() .stream() .filter(albumTrackVo -> albumTrackVo.getOrderNum() > albumInfo.getTracksForFree()) //过滤获取除免费试听以外声音 .collect(Collectors.toList()) .stream().forEach(albumTrackListVo -> { albumTrackListVo.setIsShowPaidMark(true); }); } } else { //4.处理用户已登录情况 //4.1 远程调用用户服务获取用户信息得到用户身份 UserInfoVo userInfoVo = userFeignClient.getUserInfoVo(userId).getData(); Assert.notNull(userInfoVo, "用户{}不存在", userId); Integer isVip = userInfoVo.getIsVip(); //4.2 默认设置需要进一步确定购买情况标识:默认false Boolean isNeedCheckPayStatus = false; //4.2.1 如果专辑付费类型 VIP免费 if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(payType)) { //当前用户为普通用户或VIP会员过期 if (isVip.intValue() == 0) { isNeedCheckPayStatus = true; } if (isVip.intValue() == 1 && new Date().after(userInfoVo.getVipExpireTime())) { isNeedCheckPayStatus = true; } } //4.2.2 如果专辑付费类型 付费 if (SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(payType)) { //当前用户为普通用户或VIP会员过期 isNeedCheckPayStatus = true; } if (isNeedCheckPayStatus) { //4.3 进一步确定用户是否购买专辑或声音-远程调用用户服务获取本页中专辑或者声音购买情况 //本页中需要检查购买情况声音列表,过滤掉当前页免费试听声音 List<AlbumTrackListVo> needCheckTrackList = pageInfo.getRecords().stream() .filter(albumTrackListVo -> albumTrackListVo.getOrderNum() > albumInfo.getTracksForFree()) .collect(Collectors.toList()); //本页中需要检查购买情况声音ID列表,过滤掉当前页免费试听声音 List<Long> needCheckTrackIdList = needCheckTrackList.stream() .map(albumTrackListVo -> albumTrackListVo.getTrackId()) .collect(Collectors.toList()); Map<Long, Integer> userPayStatusTrackMap = userFeignClient.userIsPaidTrack(userId, albumId, needCheckTrackIdList).getData(); //4.4 循环当前页中声音列表-跟返回用户购买情况声音集合逐一判断 needCheckTrackList.stream().forEach(needCheckTrack -> { Integer payStatus = userPayStatusTrackMap.get(needCheckTrack.getTrackId()); if (payStatus.intValue() == 0) { //4.5 某个声音用户未购买,将设置付费标识 isShowPaidMark:true needCheckTrack.setIsShowPaidMark(true); } }); } } return pageInfo; } **TrackInfoMapper接口**:条件必须是当前已经开放并且是审核通过状态的数据,并且还需要获取到声音的播放量以及评论数量 java /** * 查询指定专辑下包含声音列表 * * @param pageInfo * @param albumId * @return */ Page<AlbumTrackListVo> getAlbumTrackPage(Page<AlbumTrackListVo> pageInfo, @Param("albumId") Long albumId); **TrackInfoMapper.xml** 映射文件 动态SQL sql #分页查询指定专辑下包含声音列表(包含统计信息) select * from track_info where album_id = 307; select * from track_info where album_id = 307 and id = 16289; select * from track_stat where track_id = 16289; select ti.id trackId, ti.track_title trackTitle, ti.media_duration mediaDuration, ti.order_num orderNum, ti.create_time createTime, max(if(ts.stat_type='0701', ts.stat_num, 0)) playStatNum, max(if(ts.stat_type='0702', ts.stat_num, 0)) collectStatNum, max(if(ts.stat_type='0703', ts.stat_num, 0)) praiseStatNum, max(if(ts.stat_type='0704', ts.stat_num, 0)) commentStatNum from track_info ti left join track_stat ts on ts.track_id = ti.id where ti.album_id = 307 and ti.is_deleted = 0 group by ti.id order by ti.order_num sql <select id="getAlbumTrackPage" resultType="com.atguigu.tingshu.vo.album.AlbumTrackListVo"> select ti.id trackId, ti.track_title, ti.media_duration, ti.order_num, ti.create_time, max(if(stat.stat_type='0701', stat.stat_num, 0)) playStatNum, max(if(stat.stat_type='0702', stat.stat_num, 0)) collectStatNum, max(if(stat.stat_type='0703', stat.stat_num, 0)) praiseStatNum, max(if(stat.stat_type='0704', stat.stat_num, 0)) commentStatNum from track_info ti left join track_stat stat on stat.track_id = ti.id where ti.album_id = #{albumId} and ti.is_deleted = 0 group by ti.id order by ti.order_num </select> 测试: - 手动增加用户购买专辑记录:**user_paid_album** - 手动增加用户购买声音记录:**user_paid_track** - 手动修改VIP会员:**user_info** 情况一:未登录情况,专辑付费类型:VIP免费 付费 查看声音列表->试听声音免费+其余都需要展示付费标识 情况二:登录情况 - 普通用户 - 免费 全部免费 - VIP付费 试听声音免费+用户购买过专辑/声音,未购买展示付费标识 - 付费:试听声音免费+用户购买过专辑/声音,未购买展示付费标识 - VIP用户 - 免费 全部免费 - VIP付费 全部免费 - 付费:试听声音免费+用户购买过专辑/声音,未购买展示付费标识 时间处理: java @Data @Schema(description = "UserInfoVo") public class UserInfoVo implements Serializable { @Schema(description = "用户id") private Long id; @Schema(description = "微信openId") private String wxOpenId; @Schema(description = "nickname") private String nickname; @Schema(description = "主播用户头像图片") private String avatarUrl; @Schema(description = "用户是否为VIP会员 0:普通用户 1:VIP会员") private Integer isVip; @Schema(description = "当前VIP到期时间,即失效时间") @DateTimeFormat( pattern = "yyyy-MM-dd" ) @JsonFormat( shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "GMT+8" ) @JsonDeserialize private Date vipExpireTime; } # 2、MongoDB文档型数据库 详情见:**第6章 MongoDB入门.md** **播放进度**对应的实体类: java @Data @Schema(description = "UserListenProcess") @Document public class UserListenProcess { @Schema(description = "id") @Id private String id; @Schema(description = "用户id") private Long userId; @Schema(description = "专辑id") private Long albumId; @Schema(description = "声音id,声音id为0时,浏览的是专辑") private Long trackId; @Schema(description = "相对于音频开始位置的播放跳出位置,单位为秒。比如当前音频总时长60s,本次播放到音频第25s处就退出或者切到下一首,那么break_second就是25") private BigDecimal breakSecond; @Schema(description = "是否显示") private Integer isShow; @Schema(description = "创建时间") private Date createTime; @Schema(description = "更新时间") private Date updateTime; } # 3、声音详情 ![详情-获取播放记录进度](assets/详情-获取播放记录进度.gif) ## 3.1 获取声音播放进度 在播放声音的时候,会有触发一个获取播放进度的控制器!因为页面每隔10s会自动触发一次保存功能,会将数据写入MongoDB中。所以我们直接从MongoDB中获取到上一次声音的播放时间即可! ![](assets/tingshu013.png) > YAPI接口:http://192.168.200.6:3000/project/11/interface/api/71 在 service-user 微服务的 **UserListenProcessApiController** 控制器中添加 java /** * 获取当前用户收听声音播放进 * * @param trackId * @return */ @GuiGuLogin(required = false) @Operation(summary = "获取当前用户收听声音播放进度") @GetMapping("/userListenProcess/getTrackBreakSecond/{trackId}") public Result<BigDecimal> getTrackBreakSecond(@PathVariable Long trackId) { Long userId = AuthContextHolder.getUserId(); if (userId != null) { BigDecimal breakSecond = userListenProcessService.getTrackBreakSecond(userId, trackId); return Result.ok(breakSecond); } return Result.ok(); } **UserListenProcessService接口**: java /** * 获取当前用户收听声音播放进度 * @param userId 用户ID * @param trackId 声音ID * @return */ BigDecimal getTrackBreakSecond(Long userId, Long trackId); **UserListenProcessServiceImpl**实现类: java package com.atguigu.tingshu.user.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; import com.alibaba.fastjson.JSON; import com.atguigu.tingshu.common.constant.KafkaConstant; import com.atguigu.tingshu.common.constant.RedisConstant; import com.atguigu.tingshu.common.constant.SystemConstant; import com.atguigu.tingshu.common.service.KafkaService; import com.atguigu.tingshu.common.util.MongoUtil; import com.atguigu.tingshu.model.user.UserListenProcess; import com.atguigu.tingshu.user.service.UserListenProcessService; import com.atguigu.tingshu.vo.album.TrackStatMqVo; import com.atguigu.tingshu.vo.user.UserListenProcessVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Date; import java.util.concurrent.TimeUnit; @Service @SuppressWarnings({"all"}) public class UserListenProcessServiceImpl implements UserListenProcessService { @Autowired private MongoTemplate mongoTemplate; /** * 获取当前用户收听声音播放进度 * * @param userId 用户ID * @param trackId 声音ID * @return */ @Override public BigDecimal getTrackBreakSecond(Long userId, Long trackId) { //1.构建查询条件 Query query = new Query(); query.addCriteria(Criteria.where("userId").is(userId).and("trackId").is(trackId)); //2.执行查询播放进度 UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); if (userListenProcess != null) { return userListenProcess.getBreakSecond(); } return new BigDecimal("0.00"); } } ![image-20240307200116030](assets/image-20240307200116030.png) ## 3.2 更新播放进度 页面每隔10秒左右更新播放进度. 1. 更新播放进度页面会传递 专辑Id ,秒数,声音Id 。后台会将这个三个属性封装到UserListenProcessVo 对象中。然后利用MongoDB进行存储到UserListenProcess实体类中! 2. 为了提高用户快速访问,将用户信息存储到缓存中。先判断当前用户Id 与 声音Id 是否存在,不存在的话才将数据存储到缓存,并且要发送消息给kafka。 3. kafka 监听消息并消费,更新专辑与声音的统计数据。 ### 3.2.1 更新MongoDB ![](assets/tingshu015.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/73 在 **UserListenProcessApiController** 控制器中添加 java /** * 更新当前用户收听声音播放进度 * @param userListenProcessVo * @return */ @GuiGuLogin(required = false) @Operation(summary = "更新当前用户收听声音播放进度") @PostMapping("/userListenProcess/updateListenProcess") public Result updateListenProcess(@RequestBody UserListenProcessVo userListenProcessVo){ Long userId = AuthContextHolder.getUserId(); if (userId != null) { userListenProcessService.updateListenProcess(userId, userListenProcessVo); } return Result.ok(); } **UserListenProcessService**接口: java /** * 更新当前用户收听声音播放进度 * @param userId 用户ID * @param userListenProcessVo 播放进度信息 * @return */ void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo); **UserListenProcessServiceImpl**实现类: java @Autowired private RedisTemplate redisTemplate; @Autowired private KafkaService kafkaService; /** * 更新当前用户收听声音播放进度 * * @param userId 用户ID * @param userListenProcessVo 播放进度信息 * @return */ @Override public void updateListenProcess(Long userId, UserListenProcessVo userListenProcessVo) { //1.根据用户ID+声音ID获取播放进度 Query query = new Query(); //1.1 设置查询条件 query.addCriteria(Criteria.where("userId").is(userId).and("trackId").is(userListenProcessVo.getTrackId())); //1.2 设置查询第一条记录(避免小程序暂停后恢复播放将积压更新进度请求并发发起,导致新增多条播放进度) query.limit(1); UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); if (userListenProcess == null) { //2.如果播放进度不存在-新增播放进度 userListenProcess = new UserListenProcess(); userListenProcess.setUserId(userId); userListenProcess.setAlbumId(userListenProcessVo.getAlbumId()); userListenProcess.setTrackId(userListenProcessVo.getTrackId()); userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond()); userListenProcess.setIsShow(1); userListenProcess.setCreateTime(new Date()); userListenProcess.setUpdateTime(new Date()); } else { //3.如果播放进度存在-更新进度 userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond()); userListenProcess.setUpdateTime(new Date()); } mongoTemplate.save(userListenProcess, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); //4.采用Redis提供set k v nx ex 确保在规定时间内(24小时/当日内)播放进度统计更新1次 String key = RedisConstant.USER_TRACK_REPEAT_STAT_PREFIX + userId + ":" + userListenProcessVo.getTrackId(); long ttl = DateUtil.endOfDay(new Date()).getTime() - System.currentTimeMillis(); Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, userListenProcess.getTrackId(), ttl, TimeUnit.MILLISECONDS); if (flag) { //5.如果是首次更新播放进度,发送消息到Kafka话题 //5.1 构建更新声音播放进度MQVO对象 TrackStatMqVo mqVo = new TrackStatMqVo(); //生成业务唯一标识,消费者端(专辑服务、搜索服务)用来做幂等性处理,确保一个消息只能只被处理一次 mqVo.setBusinessNo(IdUtil.fastSimpleUUID()); mqVo.setAlbumId(userListenProcessVo.getAlbumId()); mqVo.setTrackId(userListenProcessVo.getTrackId()); mqVo.setStatType(SystemConstant.TRACK_STAT_PLAY); mqVo.setCount(1); //5.2 发送消息到更新声音统计话题中 kafkaService.sendMessage(KafkaConstant.QUEUE_TRACK_STAT_UPDATE, JSON.toJSONString(mqVo)); } } ### 3.2.2 更新MySQL统计信息 在service-album 微服务中添加监听消息: java package com.atguigu.tingshu.album.receiver; import com.alibaba.fastjson.JSON; import com.atguigu.tingshu.album.service.AlbumInfoService; import com.atguigu.tingshu.common.constant.KafkaConstant; import com.atguigu.tingshu.vo.album.TrackStatMqVo; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; /** * @author: atguigu * @create: 2023-12-19 15:31 */ @Slf4j @Component public class AlbumReceiver { @Autowired private AlbumInfoService albumInfoService; /** * 监听到更新声音统计信息 * 1.考虑消息幂等性 2.是否需要事务管理 * * @param record */ @KafkaListener(topics = KafkaConstant.QUEUE_TRACK_STAT_UPDATE) public void updateTrackStat(ConsumerRecord<String, String> record) { String value = record.value(); if (StringUtils.isNotBlank(value)) { log.info("[专辑服务],监听到更新声音统计消息:{}", value); TrackStatMqVo mqVo = JSON.parseObject(value, TrackStatMqVo.class); albumInfoService.updateTrackStat(mqVo); } } } 在**TrackInfoService** 中添加接口 java /** * MQ监听更新声音统计信息 * @param mqVo */ void updateTrackStat(TrackStatMqVo mqVo); 在**TrackInfoServiceImpl** 中添加实现 java @Autowired private AlbumStatMapper albumStatMapper; @Autowired private RedisTemplate redisTemplate; /** * MQ监听更新声音统计信息(包含:播放、收藏、点赞、评论) * * @param mqVo */ @Override @Transactional(rollbackFor = Exception.class) public void updateTrackStat(TrackStatMqVo mqVo) { //1.做幂等性处理,统一个消息只处理一次 采用set k(业务消息唯一标识) v NX EX String key = "mq:" + mqVo.getBusinessNo(); try { Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, mqVo.getBusinessNo(), 1, TimeUnit.HOURS); if (flag) { //2.更新声音统计信息 trackStatMapper.updateStat(mqVo.getTrackId(), mqVo.getStatType(), mqVo.getCount()); //3.更新专辑统计信息(播放量、评论量只要声音+1,对应专辑也得+1) if (SystemConstant.TRACK_STAT_PLAY.equals(mqVo.getStatType())) { albumStatMapper.updateStat(mqVo.getAlbumId(), SystemConstant.ALBUM_STAT_PLAY, mqVo.getCount()); } if (SystemConstant.TRACK_STAT_COMMENT.equals(mqVo.getStatType())) { albumStatMapper.updateStat(mqVo.getAlbumId(), SystemConstant.ALBUM_STAT_COMMENT, mqVo.getCount()); } } } catch (Exception e) { //如果更新数据库发送异常,事务会进行回滚,下次再次投递消息允许继续处理统一个消息 redisTemplate.delete(key); throw new RuntimeException(e); } } **TrackStatMapper**.java 添加方法 java package com.atguigu.tingshu.album.mapper; import com.atguigu.tingshu.model.album.TrackStat; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.*; @Mapper public interface TrackStatMapper extends BaseMapper<TrackStat> { /** * 更新声音统计信息 * @param trackId 声音ID * @param statType 统计类型 * @param count 数量 */ @Update("update track_stat set stat_num = stat_num + #{count} where track_id = #{trackId} and stat_type = #{statType}") void updateStat(@Param("trackId") Long trackId, @Param("statType") String statType, @Param("count") Integer count); } **AlbumStatMapper**.java 接口添加 java package com.atguigu.tingshu.album.mapper; import com.atguigu.tingshu.model.album.AlbumStat; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; @Mapper public interface AlbumStatMapper extends BaseMapper<AlbumStat> { @Update("update album_stat set stat_num = stat_num + #{count} where album_id = #{albumId} and stat_type = #{statType}") void updateStat(@Param("albumId") Long albumId, @Param("statType") String statType, @Param("count") Integer count); } ## 3.3 专辑上次播放专辑声音 ![image-20231012111356796](assets/image-20231012111356796.png) 我们需要根据用户Id 来获取播放记录 ,需要获取到专辑Id 与声音Id 封装到map中然后返回数据即可! > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/83 控制器 **UserListenProcessApiController** java /** * 获取当前用户上次播放专辑声音记录 * * @return */ @GuiGuLogin @GetMapping("/userListenProcess/getLatelyTrack") public Result<Map<String, Long>> getLatelyTrack() { Long userId = AuthContextHolder.getUserId(); return Result.ok(userListenProcessService.getLatelyTrack(userId)); } **UserListenProcessService接口:** java /** * 获取用户最近一次播放记录 * @param userId * @return */ Map<String, Long> getLatelyTrack(Long userId); **UserListenProcessServiceImpl实现类**: java /** * 获取用户最近一次播放记录 * * @param userId * @return */ @Override public Map<String, Long> getLatelyTrack(Long userId) { //根据用户ID查询播放进度集合,按照更新时间倒序,获取第一条记录 //1.构建查询条件对象 Query query = new Query(); //1.1 封装用户ID查询条件 query.addCriteria(Criteria.where("userId").is(userId)); //1.2 按照更新时间排序 query.with(Sort.by(Sort.Direction.DESC, "updateTime")); //1.3 只获取第一条记录 query.limit(1); //2.执行查询 UserListenProcess listenProcess = mongoTemplate.findOne(query, UserListenProcess.class, MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)); if (listenProcess != null) { //封装响应结果 Map<String, Long> mapResult = new HashMap<>(); mapResult.put("albumId", listenProcess.getAlbumId()); mapResult.put("trackId", listenProcess.getTrackId()); return mapResult; } return null; } ## 3.4 获取声音统计信息 ![](assets/tingshu014.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/75 统计声音需要更新的数据如下,我们将数据封装到一个实体类中便于操作 java @Data @Schema(description = "用户声音统计信息") public class TrackStatVo { @Schema(description = "播放量") private Integer playStatNum; @Schema(description = "订阅量") private Integer collectStatNum; @Schema(description = "点赞量") private Integer praiseStatNum; @Schema(description = "评论数") private Integer commentStatNum; //该属性需要修改 } 在**TrackInfoApiController** 控制器中添加 java /** * 根据声音ID,获取声音统计信息 * @param trackId * @return */ @Operation(summary = "根据声音ID,获取声音统计信息") @GetMapping("/trackInfo/getTrackStatVo/{trackId}") public Result<TrackStatVo> getTrackStatVo(@PathVariable Long trackId){ return Result.ok(trackInfoService.getTrackStatVo(trackId)); } **TrackInfoService接口**: java /** * 根据声音ID,查询声音统计信息 * @param trackId * @return */ TrackStatVo getTrackStatVo(Long trackId); **TrackInfoServiceImpl实现类**: java /** * 根据声音ID,查询声音统计信息 * @param trackId * @return */ @Override public TrackStatVo getTrackStatVo(Long trackId) { return trackInfoMapper.getTrackStatVo(trackId); } **TrackInfoMapper**.java java /** * 获取声音统计信息 * @param trackId * @return */ @Select("select\n" + " track_id,\n" + " max(if(stat_type='0701', stat_num, 0)) playStatNum,\n" + " max(if(stat_type='0702', stat_num, 0)) collectStatNum,\n" + " max(if(stat_type='0703', stat_num, 0)) praiseStatNum,\n" + " max(if(stat_type='0704', stat_num, 0)) commentStatNum\n" + " from track_stat where track_id = #{trackId} and is_deleted=0\n" + "group by track_id") TrackStatVo getTrackStatVo(@Param("trackId") Long trackId); **SQL** sql # 根据声音ID查询指定声音统计信息 playStatNum collectStatNum praiseStatNum commentStatNum select track_id, max(if(stat_type='0701', stat_num, 0)) playStatNum, max(if(stat_type='0702', stat_num, 0)) collectStatNum, max(if(stat_type='0703', stat_num, 0)) praiseStatNum, max(if(stat_type='0704', stat_num, 0)) commentStatNum from track_stat where track_id = 49162 and is_deleted=0 group by track_id # 4、更新Redis排行榜 手动调用一次更新,查看排行榜。后续会整合xxl-job 分布式定时任务调度框架做定时调用。 ![详情-排行榜](assets/详情-排行榜.gif) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/77 service-album 微服务中**BaseCategoryApiController**控制器中添加 java /** * 查询所有一级分类列表 * @return */ @Operation(summary = "查询所有一级分类列表") @GetMapping("/category/findAllCategory1") public Result> getAllCategory1() { return Result.ok(baseCategoryService.list()); } **AlbumFeignClient** java /** * 查询所有一级分类列表 * @return */ @GetMapping("/category/findAllCategory1") public Result> getAllCategory1(); **AlbumDegradeFeignClient熔断类**: java @Override public Result> getAllCategory1() { log.error("[专辑模块Feign调用]getAllCategory1异常"); return null; } > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/79 在**SearchApiController** 中添加控制器 java /** * 为定时更新首页排行榜提供调用接口 * @return */ @Operation(summary = "为定时更新首页排行榜提供调用接口") @GetMapping("/albumInfo/updateLatelyAlbumRanking") public Result updateLatelyAlbumRanking(){ searchService.updateLatelyAlbumRanking(); return Result.ok(); } **SearchService**接口: java /** * 获取不同分类下不同排序方式榜单专辑列表 */ void updateLatelyAlbumRanking(); **SearchServiceImpl实现类:** java @Autowired private RedisTemplate redisTemplate; /** * 获取不同分类下不同排序方式榜单专辑列表 */ @Override public void updateLatelyAlbumRanking() { try { //1.远程调用专辑服务获取所有1级分类列表 List<BaseCategory1> category1List = albumFeignClient.getdAllCategory1().getData(); Assert.notNull(category1List, "一级分类为空"); //2.循环遍历1级分类列表,获取该分类下5种不同排序方式榜单专辑 for (BaseCategory1 baseCategory1 : category1List) { Long category1Id = baseCategory1.getId(); //3.在处理当前1级分类中,再次循环5种不同排序方式得到具体榜单数据 //3.1 声明排序方式数组 String[] rankingDimensionArray = new String[]{"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"}; for (String rankingDimension : rankingDimensionArray) { //3.2 调用ES检索接口获取榜单数据 SearchResponse<AlbumInfoIndex> searchResponse = elasticsearchClient.search( s -> s.index(INDEX_NAME) .query(q -> q.term(t -> t.field("category1Id").value(category1Id))) .sort(sort -> sort.field(f -> f.field(rankingDimension).order(SortOrder.Desc))) .size(10) , AlbumInfoIndex.class ); //3.3 获取当前分类下某个排序方式榜单专辑列表 List<Hit<AlbumInfoIndex>> hits = searchResponse.hits().hits(); if (CollectionUtil.isNotEmpty(hits)) { List<AlbumInfoIndex> list = hits.stream().map(hit -> hit.source()).collect(Collectors.toList()); //4.将榜单专辑列表存入Redis-Hash中 //4.1 声明Redis排行榜Hash接口 Key 形式:前缀+1级分类ID field:排序方式 val:榜单列表 String key = RedisConstant.RANKING_KEY_PREFIX + category1Id; //4.2 将当前分类榜单数据放入Redis中 redisTemplate.opsForHash().put(key, rankingDimension, list); } } } } catch (Exception e) { log.error("[搜索服务]更新排行榜异常:{}", e); throw new RuntimeException(e); } } # 5、获取排行榜 ![image-20231012114420751](assets/image-20231012114420751.png) 点击排行榜的时候,能看到获取排行榜的地址 排行榜:key=ranking:category1Id field = hotScore 或 playStatNum 或 subscribeStatNum 或 buyStatNum 或albumCommentStatNum value=List<AlbumInfoIndexVo> ![](assets/tingshu016.png) > YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/81 **SearchApiController** 控制器中添加 java /** * 获取指定1级分类下不同排序方式榜单列表-从Redis中获取 * @param category1Id * @param dimension * @return */ @Operation(summary = "获取指定1级分类下不同排序方式榜单列表") @GetMapping("/albumInfo/findRankingList/{category1Id}/{dimension}") public Result> getRankingList(@PathVariable Long category1Id, @PathVariable String dimension){ List<AlbumInfoIndex> list = searchService.getRankingList(category1Id, dimension); return Result.ok(list); } **SearchService**接口: java /** * 获取指定1级分类下不同排序方式榜单列表-从Redis中获取 * @param category1Id * @param dimension * @return */ List<AlbumInfoIndex> getRankingList(Long category1Id, String dimension); **SearchServiceImpl实现类**: java /** * 获取指定1级分类下不同排序方式榜单列表-从Redis中获取 * * @param category1Id * @param dimension * @return */ @Override public List<AlbumInfoIndex> getRankingList(Long category1Id, String dimension) { //1.构建分类排行榜Hash结构Key String key = RedisConstant.RANKING_KEY_PREFIX + category1Id; //2.获取Redis中hash结构中value Boolean flag = redisTemplate.opsForHash().hasKey(key, dimension); if (flag) { List<AlbumInfoIndex> list = (List) redisTemplate.opsForHash().get(key, dimension); return list; } return null; } 结合上面的内容介绍断点续播的实现思路,不需要给出代码

最新推荐

recommend-type

微软解决方案面向服务的架构.doc

微软解决方案面向服务的架构.doc
recommend-type

Huawei S6780-H-V600R024SPH120

Huawei S6780-H_V600R024SPH120,里面包含补丁说明书和补丁安装指导书,该补丁支持哪些型号,支持哪些版本可以安装当前补丁,请参考补丁说明书和补丁安装指导书。
recommend-type

VC图像编程全面资料及程序汇总

【标题】:"精通VC图像编程资料全览" 【知识点】: VC即Visual C++,是微软公司推出的一个集成开发环境(IDE),专门用于C++语言的开发。VC图像编程涉及到如何在VC++开发环境中处理和操作图像。在VC图像编程中,开发者通常会使用到Windows API中的GDI(图形设备接口)或GDI+来进行图形绘制,以及DirectX中的Direct2D或DirectDraw进行更高级的图形处理。 1. GDI(图形设备接口): - GDI是Windows操作系统提供的一套应用程序接口,它允许应用程序通过设备无关的方式绘制图形。 - 在VC图像编程中,主要使用CDC类(设备上下文类)来调用GDI函数进行绘制,比如绘制线条、填充颜色、显示文本等。 - CDC类提供了很多函数,比如`MoveTo`、`LineTo`、`Rectangle`、`Ellipse`、`Polygon`等,用于绘制基本的图形。 - 对于图像处理,可以使用`StretchBlt`、`BitBlt`、`TransparentBlt`等函数进行图像的位块传输。 2. GDI+: - GDI+是GDI的后继技术,提供了更丰富的图形处理功能。 - GDI+通过使用`Graphics`类来提供图像的绘制、文本的渲染、图像的处理和颜色管理等功能。 - GDI+引入了对矢量图形、渐变色、复杂的文本格式和坐标空间等更高级的图形处理功能。 - `Image`类是GDI+中用于图像操作的基础类,通过它可以进行图像的加载、保存、旋转、缩放等操作。 3. DirectX: - DirectX是微软推出的一系列API集合,用于在Windows平台上进行高性能多媒体编程。 - DirectX中的Direct2D是用于硬件加速的二维图形API,专门用于UI元素和简单的图形渲染。 - DirectDraw主要用于硬件加速的位图操作,比如全屏游戏开发中的画面渲染。 4. 位图操作: - 在VC图像编程中,位图操作是一个重要的部分。需要了解如何加载、保存和处理位图(BMP)文件。 - 可以使用位图文件格式的解析,来访问位图的像素数据,进行像素级别的图像处理和修改。 5. 高级图像处理技术: - 包括图像滤镜、图像转换、图像压缩和解压缩技术。 - 需要掌握一些图像处理算法,比如卷积、FFT(快速傅里叶变换)、DCT(离散余弦变换)等。 - 了解图像的色彩空间转换,比如RGB到YUV的转换,这在视频处理中非常重要。 6. 图像库的使用: - 除了直接使用API进行图像处理之外,还可以使用开源的图像处理库,如OpenCV。 - OpenCV是一个跨平台的计算机视觉和机器学习软件库,它提供了很多高级的图像处理功能。 【压缩包子文件的文件名称列表】: VC++ 文件名称列表显示为"VC++",这表明文件包中可能包含了Visual C++的项目、源代码文件、动态链接库(DLLs)、编译器设置、链接器设置等。如果要使用这些文件进行VC图像编程,需要确保Visual C++开发环境已经安装并且配置好,同时需要安装好所有依赖的库和工具。 要开始VC图像编程,开发者需要具备C++编程基础,熟悉Windows编程概念,并且对图形学有一定的了解。掌握VC图像编程技巧,对于进行桌面应用程序开发、图像处理软件开发以及游戏开发等都是至关重要的。
recommend-type

Pokemmo响应速度翻倍:多线程处理的高级技巧

# 摘要 多线程处理是提高软件并发性能和响应速度的关键技术,本文从基础概念与优势出发,深入探讨了多线程的实现机制,包括线程模型、调度算法、编程关键技术、线程安全性最佳实践、死锁预防与处理等方面。通
recommend-type

人名列表滚动抽奖

<think>嗯,用户想实现一个滚动抽奖功能,需要前端动画效果和随机选择逻辑。首先,我得考虑前端怎么实现滚动效果。可能用CSS动画或者JavaScript的requestAnimationFrame?比如,使用CSS的@keyframes来控制位移,或者用JS动态更新样式。然后,随机选择算法,可能需要确保公平性,比如用Fisher-Yates洗牌算法,或者用Math.random()来生成随机索引。然后,用户可能需要平滑的滚动动画,比如先快速滚动,然后逐渐减速,最后停在选中的人名上。这可能需要设置定时器,逐步改变位置,或者使用CSS过渡效果。另外,还要考虑性能,避免页面卡顿,可能需要使用硬件加
recommend-type

一站式JSF开发环境:即解压即用JAR包

标题:“jsf开发完整JAR包”所指的知识点: 1. JSF全称JavaServer Faces,是Java EE(现EE4J)规范之一,用于简化Java Web应用中基于组件的用户界面构建。JSF提供了一种模型-视图-控制器(MVC)架构的实现,使得开发者可以将业务逻辑与页面表示分离。 2. “开发完整包”意味着这个JAR包包含了JSF开发所需的所有类库和资源文件。通常来说,一个完整的JSF包会包含核心的JSF库,以及一些可选的扩展库,例如PrimeFaces、RichFaces等,这些扩展库提供了额外的用户界面组件。 3. 在一个项目中使用JSF,开发者无需单独添加每个必要的JAR文件到项目的构建路径中。因为打包成一个完整的JAR包后,所有这些依赖都被整合在一起,极大地方便了开发者的部署工作。 4. “解压之后就可以直接导入工程中使用”表明这个JAR包是一个可执行的归档文件,可能是一个EAR包或者一个可直接部署的Java应用包。解压后,开发者只需将其内容导入到他们的IDE(如Eclipse或IntelliJ IDEA)中,或者将其放置在Web应用服务器的正确目录下,就可以立即进行开发。 描述中所指的知识点: 1. “解压之后就可以直接导入工程中使用”说明这个JAR包是预先配置好的,它可能包含了所有必要的配置文件,例如web.xml、faces-config.xml等,这些文件是JSF项目运行所必需的。 2. 直接使用意味着减少了开发者配置环境和处理依赖的时间,有助于提高开发效率。 标签“jsf jar包”所指的知识点: 1. 标签指明了JAR包的内容是专门针对JSF框架的。因此,这个JAR包包含了JSF规范所定义的API以及可能包含的具体实现,比如Mojarra或MyFaces。 2. “jar包”是一种Java平台的归档文件格式,用于聚合多个文件到一个文件中。在JSF开发中,JAR文件经常被用来打包和分发库或应用程序。 文件名称列表“jsf”所指的知识点: 1. “jsf”文件名可能意味着这是JSF开发的核心库,它应该包含了所有核心的JavaServer Faces类文件以及资源文件。 2. 如果是使用特定版本的JSF,例如“jsf-2.2.jar”,则表明文件内包含了对应版本的JSF实现。这种情况下,开发者必须确认他们所使用的Web服务器或应用程序服务器支持该版本的JSF。 3. 文件名称也可能是“jsf-components.jar”、“jsf-impl.jar”等,表明这个JAR包是JSF的一个子模块或特定功能组件。例如,“jsf-components.jar”可能包含了一系列用于在JSF应用中使用的自定义组件。 4. 对于开发者而言,了解文件名称中所蕴含的信息非常重要,因为这将决定他们需要下载哪些JAR包来满足特定项目的需求。 综合以上信息,开发者在使用JSF进行Java Web应用开发时,会通过一个预先配置好的JAR包来快速地搭建和启动项目。这样做不仅简化了项目初始化的过程,也使得开发者能够更加聚焦于业务逻辑的实现和界面设计,而不必深究底层框架配置的细节。
recommend-type

Pokemmo内存优化揭秘:专家教你如何降低50%资源消耗

# 摘要 本文综述了Pokemmo游戏的内存优化方法,从内存管理基础出发,探讨内存使用效率的影响因素,并介绍了性能监控与分析工具。在内存优化实践技巧章节中,详细讨论了代码层面的优化、数据结构和算法选择对内存效率的影响,并通过案例分析展示了实际的优化过程。针对Pokemmo游戏特点,分析了内存消耗特性并提出了特定优化技术。最后,本文展望了未来内存管理技术的发展方向,以及游戏开发中面临的新挑战,为Pokemmo及类似游戏提供了优化建议。 # 关键字 内存优化;内存管理;性能监控;数据结构;算法效率;游戏开发 参考资源链接:[Pokemmo必备资源包:四种ROM与汉化补丁](https://we
recommend-type

直接访问子路由是吧

<think>我们正在使用Vue2,因此需要参考VueRouterv3的文档(因为Vue2对应VueRouterv3,Vue3对应VueRouterv4)。用户的问题是:如何配置路由以支持直接访问子路由?即,在Vue2中,我们希望直接通过URL访问嵌套的子路由(例如:/parent/child),而不仅仅是先访问父路由再导航到子路由。根据之前的回答和引用,我们已经知道:1.在父路由的配置中,使用`children`数组来定义子路由。2.子路由的`path`不能以斜杠开头(例如:'child'而不是'/child'),这样它就会基于父路由的路径进行拼接。3.在父组件的模板中放置`<router-
recommend-type

C++函数库查询辞典使用指南与功能介绍

标题中提到的“C++函数库查询辞典”指的是一个参考工具书或者是一个软件应用,专门用来查询C++编程语言中提供的标准库中的函数。C++是一种静态类型、编译式、通用编程语言,它支持多种编程范式,包括过程化、面向对象和泛型编程。C++标准库是一组包含函数、类、迭代器和模板的库,它为C++程序员提供标准算法和数据结构。 描述中提供的内容并没有给出实际的知识点,只是重复了标题的内容,并且有一串无关的字符“sdfsdfsdffffffffffffffffff”,因此这部分内容无法提供有价值的信息。 标签“C++ 函数库 查询辞典”强调了该工具的用途,即帮助开发者查询C++的标准库函数。它可能包含每个函数的详细说明、语法、使用方法、参数说明以及示例代码等,是学习和开发过程中不可或缺的参考资源。 文件名称“c++函数库查询辞典.exe”表明这是一个可执行程序。在Windows操作系统中,以“.exe”结尾的文件通常是可执行程序。这意味着用户可以通过双击或者命令行工具来运行这个程序,进而使用其中的查询功能查找C++标准库中各类函数的详细信息。 详细知识点如下: 1. C++标准库的组成: C++标准库由多个组件构成,包括输入输出流(iostream)、算法(algorithm)、容器(container)、迭代器(iterator)、字符串处理(string)、数值计算(numeric)、本地化(locale)等。 2. 输入输出流(iostream)库: 提供输入输出操作的基本功能。使用诸如iostream、fstream、sstream等头文件中的类和对象(如cin, cout, cerr等)来实现基本的输入输出操作。 3. 算法(algorithm)库: 包含对容器进行操作的大量模板函数,如排序(sort)、查找(find)、拷贝(copy)等。 4. 容器(container)库: 提供各种数据结构,如向量(vector)、列表(list)、队列(queue)、映射(map)等。 5. 迭代器(iterator): 迭代器提供了一种方法来访问容器中的元素,同时隐藏了容器的内部结构。 6. 字符串处理(string)库: C++标准库中的字符串类提供了丰富的功能用于处理字符串。 7. 数值计算(numeric)库: 提供数值计算所需的函数和类,比如对复数的支持和数值算法。 8. 本地化(locale)库: 提供本地化相关的功能,比如日期、时间的格式化显示以及字符的本地化比较。 9. 错误处理和异常: C++通过throw、try、catch关键字和标准异常类提供了一套异常处理机制。 10. 智能指针: C++11及其后续版本提供了智能指针(如unique_ptr、shared_ptr、weak_ptr)来自动管理动态分配的内存。 11. lambda表达式: 在C++11中引入,允许临时创建匿名函数对象。 12. C++11新特性: 包括范围for循环、移动语义、类内初始化器、auto类型推导等。 使用C++函数库查询辞典的用户可能需要对C++的基础知识有一定的掌握,例如变量、数据类型、控制结构、函数以及面向对象的概念等。了解C++标准库的结构和内容能够帮助程序员有效地利用库函数进行软件开发,提高编程效率并减少重复造轮子的工作。 总结来说,一个C++函数库查询辞典工具对于C++程序员来说是一个非常有用的资源,它能够提供快速查找标准库函数的能力,帮助程序员更高效地解决问题和进行学习。同时,随着C++标准的不断更新,例如C++11、C++14、C++17和C++20,函数库查询辞典也会不断地更新以包含新的特性,这对于紧跟技术发展的开发者来说尤为重要。
recommend-type

【bat脚本安全最佳实践】:保护你的系统与脚本安全的黄金法则

# 摘要 本文旨在全面阐述BAT脚本的基础知识、安全编写原则、审查与优化方法以及在企业环境中的安全应用。通过深入分析脚本安全基础、常见安全陷阱及脚本的权限管理,文章提出了安全编写的具体实践和预防措施。本文详细介绍了脚本安全审查流程、代码优化、错误处理和安全更新维护策略。在企业应用方面,探讨了企业安全政策制定、脚本审计和版本控制以及外部威胁的防范措施。通过案例分析,总结了脚本