Django+UniApp 实现「可能感兴趣的用户」智能推荐功能(基于浏览指数排序)
前言
近期在博客社交类项目中,完成了消息中心 - 可能感兴趣的用户推荐模块开发。该功能根据用户browse_rate(浏览指数)做智能筛选、排序,同时实现头像跳转个人主页、关注 / 取消关注全套交互。 技术栈:Django + MySQL + UniApp,下面完整记录从数据表设计、后端接口开发、前端页面实现、问题踩坑全流程,适合前后端开发者参考学习。
一、功能需求说明
- 数据来源:读取项目
user用户表; - 筛选规则:排除当前登录用户,不把自己推荐给自己;
- 排序规则:根据
browse_rate浏览指数降序展示(数值越高排名越靠前); - 数量限制:仅展示 4~5 个推荐用户,保证页面布局整洁;
- 交互要求
- 前端摒弃静态写死数据,动态接口渲染;
- 点击头像跳转对方个人主页;
- 个人主页支持关注 / 取消关注,自动识别关注状态;
- 容错处理:无数据、接口异常、参数错误等场景做兜底提示。
二、数据库结构
1. 用户表 user
browse_rate 字段为字符串类型,格式为 数字,数字,数字(多维度浏览指数),这也是本次排序的核心字段。
sql
CREATE TABLE `user` (
`user_id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`phone` varchar(255) NULL DEFAULT NULL COMMENT '手机号',
`subscribe` int NOT DEFAULT 0 COMMENT '关注人数',
`fan` int NOT DEFAULT 0 COMMENT '粉丝人数',
`like_and_collect` int NOT DEFAULT 0 COMMENT '获赞收藏数',
`avatar_url` varchar(255) NULL DEFAULT NULL COMMENT '头像地址',
`browse_rate` varchar(255) DEFAULT "0" COMMENT '浏览指数(三值字符串)',
PRIMARY KEY (`user_id`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4;
2. 关注关系表 user_follow
用于存储用户关注关系,实现关注 / 取关、状态查询功能,设置联合唯一约束,防止重复关注。
sql
CREATE TABLE `user_follow` (
`id` int NOT NULL AUTO_INCREMENT,
`self_id` int NOT NULL COMMENT '关注人ID',
`target_id` int NOT NULL COMMENT '被关注人ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '关注时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_self_target` (`self_id`,`target_id`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4;
三、后端实现(Django)
3.1 模型映射 models.py
user 应用模型
python
运行
from django.db import models
class User(models.Model):
user_id = models.AutoField(primary_key=True, verbose_name="用户ID")
username = models.CharField(max_length=50, verbose_name="用户名")
password = models.CharField(max_length=100, verbose_name="密码")
phone = models.CharField(max_length=20, null=True, blank=True, verbose_name="手机号")
subscribe = models.IntegerField(default=0, verbose_name="关注人数")
fan = models.IntegerField(default=0, verbose_name="粉丝人数")
like_and_collect = models.IntegerField(default=0, verbose_name="获赞与收藏总数")
avatar_url = models.CharField(max_length=255, null=True, blank=True, verbose_name="头像地址")
# 三值字符串:兼容 单数字 / 数字,数字,数字 两种格式
browse_rate = models.CharField(max_length=255, default="0", verbose_name="浏览指数")
class Meta:
db_table = "user"
interaction 交互应用模型(关注表)
python
运行
from django.db import models
class UserFollow(models.Model):
self_id = models.IntegerField(verbose_name="关注人ID")
target_id = models.IntegerField(verbose_name="被关注人ID")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="关注时间")
class Meta:
db_table = "user_follow"
unique_together = ("self_id", "target_id") # 联合唯一,禁止重复关注
执行数据迁移:
bash
运行
python manage.py makemigrations
python manage.py migrate
3.2 核心接口开发(interaction/views.py)
所有交互相关接口统一放在 interaction 应用,一共开发 4 个接口:
- 推荐用户接口(核心排序)
- 获取对方用户信息接口
- 查询关注状态接口
- 关注 / 取消关注操作接口
python
运行
import json
from django.views import View
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from user.models import User
from .models import User
# 1. 可能感兴趣的用户推荐接口(按browse_rate排序)
@method_decorator(csrf_exempt, name='dispatch')
class RecommendUserView(View):
def post(self, request):
try:
data = json.loads(request.body)
current_user_id = data.get("user_id")
if not current_user_id:
return JsonResponse({"code": 400, "msg": "用户ID不能为空", "data": []})
# 排除当前登录用户
user_query = User.objects.exclude(user_id=current_user_id)
temp_data = []
# 解析browse_rate三值字符串,取第一个值作为排序依据
for user in user_query:
rate_str = user.browse_rate if user.browse_rate else "0"
rate_arr = rate_str.split(",")
try:
sort_score = int(rate_arr[0])
except (ValueError, IndexError):
sort_score = 0
temp_data.append({"obj": user, "score": sort_score})
# 按浏览指数 降序排序
temp_data.sort(key=lambda x: x["score"], reverse=True)
# 限制只返回前5条(4-5人需求)
temp_data = temp_data[:5]
# 组装返回前端的数据
result = []
for item in temp_data:
u = item["obj"]
result.append({
"user_id": u.user_id,
"name": u.username,
"avatar": u.avatar_url if u.avatar_url else "/static/img/avatar.png",
"desc": "园艺爱好者"
})
return JsonResponse({"code": 200, "msg": "获取成功", "data": result})
except Exception as e:
print("推荐接口异常:", str(e))
return JsonResponse({"code": 500, "msg": "服务器异常", "data": []})
# 2. 获取用户详情接口
@method_decorator(csrf_exempt, name='dispatch')
class GetUserInfoView(View):
def post(self, request):
try:
data = json.loads(request.body)
user_id = data.get("user_id")
user = User.objects.filter(user_id=user_id).first()
if not user:
return JsonResponse({"code": 404, "msg": "用户不存在", "data": {}})
res = {
"user_id": user.user_id,
"username": user.username,
"avatar": user.avatar_url,
"browse_rate": user.browse_rate
}
return JsonResponse({"code": 200, "msg": "成功", "data": res})
except Exception as e:
return JsonResponse({"code": 500, "msg": str(e)})
# 3. 查询是否已关注
@method_decorator(csrf_exempt, name='dispatch')
class CheckFollowView(View):
def post(self, request):
try:
data = json.loads(request.body)
self_id = data.get("self_id")
target_id = data.get("target_id")
# 判断是否存在关注记录
is_follow = UserFollow.objects.filter(self_id=self_id, target_id=target_id).exists()
return JsonResponse({"code": 200, "is_follow": is_follow})
except Exception as e:
return JsonResponse({"code": 500, "msg": str(e)})
# 4. 关注 / 取消关注 操作
@method_decorator(csrf_exempt, name='dispatch')
class FollowActionView(View):
def post(self, request):
try:
data = json.loads(request.body)
self_id = data.get("self_id")
target_id = data.get("target_id")
action = data.get("action") # 1=关注 2=取消关注
if str(self_id) == str(target_id):
return JsonResponse({"code": 400, "msg": "不能关注自己"})
if action == 1:
# 新增关注
UserFollow.objects.get_or_create(self_id=self_id, target_id=target_id)
return JsonResponse({"code": 200, "msg": "关注成功"})
elif action == 2:
# 取消关注
UserFollow.objects.filter(self_id=self_id, target_id).delete()
return JsonResponse({"code": 200, "msg": "取消关注成功"})
else:
return JsonResponse({"code": 400, "msg": "操作类型错误"})
except Exception as e:
return JsonResponse({"code": 500, "msg": str(e)})
3.3 路由配置 interaction/urls.py
python
运行
from django.urls import path
from . import views
urlpatterns = [
path("recommend_user", views.RecommendUserView.as_view()),
path("get_user_info", views.GetUserInfoView.as_view()),
path("check_follow", views.CheckFollowView.as_view()),
path("follow_action", views.FollowActionView.as_view()),
]
3.4 全局路由 & 跨域配置
项目根 urls.py 注册应用:
python
运行
from django.urls import path, include
urlpatterns = [
path('interaction/', include('interaction.urls')),
]
settings.py 开启跨域(前端小程序联调必备):
python
运行
INSTALLED_APPS = [
# 其他应用
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # 放在最顶部
# 其他中间件
]
# 本地开发允许所有跨域
CORS_ALLOW_ALL_ORIGINS = True
安装跨域依赖:
bash
运行
pip install django-cors-headers
四、前端实现(UniApp)
4.1 消息中心页面 message_center.vue
核心功能:页面加载请求推荐接口、动态渲染用户列表、头像点击跳转个人主页。
vue
<template>
<view class="container">
<!-- 顶部功能入口 -->
<view class="top-box">
<view class="item" @click="toLike">
<image src="/static/like.png" class="icon"></image>
<text class="txt">赞和收藏</text>
</view>
<view class="item" @click="toFollow">
<image src="/static/follow.png" class="icon"></image>
<text class="txt">新增关注</text>
</view>
<view class="item" @click="toComment">
<image src="/static/comment.png" class="icon"></image>
<text class="txt">评论</text>
</view>
</view>
<!-- 私信模块 -->
<view class="msg-box">
<text class="title">私信</text>
<ChatList
:msg-list="msgList"
:loading="loading"
:user-id="userId"
/>
</view>
<!-- 可能感兴趣的人(动态渲染) -->
<view class="interest-box" v-if="recommendList.length > 0">
<text class="title">可能感兴趣的人</text>
<view class="user-list">
<view class="user-item" v-for="(user, index) in recommendList" :key="index">
<!-- 头像点击跳转个人主页 -->
<image
:src="user.avatar || '/static/img/avatar.png'"
class="avatar"
mode="aspectFill"
@click.stop="goBlogHost(user)"
></image>
<view class="info">
<text class="name">{{ user.name || '用户昵称' }}</text>
<text class="desc">{{ user.desc || '暂无介绍' }}</text>
</view>
<button class="follow-btn">关注</button>
</view>
</view>
</view>
</view>
</template>
<script>
import global from "@/common/global.js"
import ChatList from "@/component/chat_list.vue"
export default {
components:{ ChatList },
data() {
return {
msgList: [],
loading: false,
userId: "",
recommendList: [] // 推荐用户列表
}
},
onLoad() {
this.getMsgData()
this.getRecommendUser()
},
methods: {
getMsgData() {
this.userId = String(global.getUserId())
const baseUrl = "http://127.0.0.1:8000"
uni.request({
url: `${baseUrl}/getChatList`,
method: "POST",
header: {"content-type": "application/json"},
data: { user_id: this.userId },
success: (res) => {
this.msgList = res.data.data || []
}
})
},
// 请求推荐用户接口
getRecommendUser() {
const currentUid = global.getUserId()
if (!currentUid) return
const baseUrl = "http://127.0.0.1:8000"
uni.request({
url: `${baseUrl}/interaction/recommend_user`,
method: "POST",
header: {"content-type": "application/json"},
data: { user_id: currentUid },
success: (res) => {
if (res.data.code === 200) {
this.recommendList = res.data.data || []
}
}
})
},
// 跳转对方个人主页
goBlogHost(item) {
if (!item.user_id) {
uni.showToast({ title: "用户ID异常", icon: "none" });
return;
}
uni.navigateTo({
url: `/pages/blog/blog_host?host_id=${item.user_id}`
});
},
toLike(){ uni.navigateTo({url:"/pages/message/like_detail"}) },
toFollow(){ uni.navigateTo({url:"/pages/message/follow_detail"}) },
toComment(){ uni.navigateTo({url:"/pages/message/comment_detail"}) }
}
}
</script>
<style scoped>
.container { padding: 20rpx; background-color: #f7f8fa; min-height: 100vh; }
.top-box { display: flex; justify-content: space-around; background: #fff; border-radius: 20rpx; padding: 30rpx 0; margin-bottom: 20rpx; }
.item { display: flex; flex-direction: column; align-items: center; }
.icon { width: 50rpx; height: 50rpx; margin-bottom: 10rpx; }
.txt { font-size: 26rpx; color: #333; }
.msg-box { background: #fff; border-radius: 20rpx; padding: 30rpx; margin-bottom: 20rpx; }
.msg-box .title { font-size: 32rpx; font-weight: bold; color: #2d7d5a; }
.interest-box { background: #fff; border-radius: 20rpx; padding: 30rpx; margin-bottom: 20rpx; }
.interest-box .title { font-size: 32rpx; font-weight: bold; color: #2d7d5a; margin-bottom: 20rpx; }
.user-item { display: flex; align-items: center; }
.avatar { width: 80rpx; height: 80rpx; border-radius: 50%; margin-right: 20rpx; cursor: pointer; }
.info { flex: 1; }
.name { font-size: 30rpx; font-weight: bold; color: #333; margin-bottom: 6rpx; }
.desc { font-size: 24rpx; color: #999; }
.follow-btn { width: 120rpx; height: 50rpx; line-height: 50rpx; border-radius: 50rpx; background-color: #2d7d5a; color: #fff; font-size: 24rpx; border: none; }
</style>
4.2 个人主页 blog_host.vue
接收跳转参数,加载用户信息、判断关注状态、实现关注 / 取关切换。
vue
<template>
<view class="host-page">
<view class="user-card">
<image class="avatar" :src="userInfo.avatar || '/static/img/avatar.png'" mode="aspectFill"></image>
<view class="user-info">
<text class="username">{{ userInfo.username || "匿名用户" }}</text>
<text class="desc">浏览指数:{{ userInfo.browse_rate || "0,0,0" }}</text>
</view>
<button
class="follow-btn"
:class="isFollow ? 'followed' : ''"
@click="toggleFollow"
>
{{ isFollow ? "已关注" : "关注" }}
</button>
</view>
<view class="divider"></view>
<view class="content">
<text>用户博客内容区域</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
hostId: "",
currentUid: "",
userInfo: {},
isFollow: false,
loading: false
}
},
onLoad(option) {
this.hostId = option.host_id
this.currentUid = uni.getStorageSync('user_id')
this.getUserInfo()
this.checkFollowStatus()
},
methods: {
// 获取用户信息
async getUserInfo() {
const baseUrl = "http://127.0.0.1:8000"
const res = await uni.request({
url: `${baseUrl}/interaction/get_user_info`,
method: "POST",
header: { "content-type": "application/json" },
data: { user_id: this.hostId }
})
if (res.data.code === 200) {
this.userInfo = res.data.data
}
},
// 查询关注状态
async checkFollowStatus() {
const baseUrl = "http://127.0.0.1:8000"
const res = await uni.request({
url: `${baseUrl}/interaction/check_follow`,
method: "POST",
header: { "content-type": "application/json" },
data: { self_id: this.currentUid, target_id: this.hostId }
})
if (res.data.code === 200) {
this.isFollow = res.data.is_follow
}
},
// 切换关注/取关
async toggleFollow() {
if (this.loading || this.currentUid == this.hostId) {
uni.showToast({ title: "不能关注自己", icon: "none" })
return
}
this.loading = true
const baseUrl = "http://127.0.0.1"
const action = this.isFollow ? 2 : 1
const res = await uni.request({
url: `${baseUrl}/interaction/follow_action`,
method: "POST",
header: { "content-type": "application/json" },
data: { self_id: this.currentUid, target_id: this.hostId, action }
})
if (res.data.code === 200) {
this.isFollow = !this.isFollow
uni.showToast({ title: this.isFollow ? "关注成功" : "已取消关注", icon: "success" })
}
this.loading = false
}
}
}
</script>
<style scoped>
page { background-color: #f5f5f5; }
.host-page { padding: 20rpx; }
.user-card { display: flex; align-items: center; background: #fff; padding: 30rpx; border-radius: 20rpx; }
.avatar { width: 120rpx; height: 120rpx; border-radius: 50%; margin-right: 30rpx; }
.user-info { flex: 1; }
.username { font-size: 34rpx; font-weight: bold; color: #333; margin-bottom: 12rpx; }
.desc { font-size: 26rpx; color: #666; }
.follow-btn { width: 140rpx; height: 56rpx; line-height: 56rpx; border-radius: 28rpx; background-color: #2d7d5a; color: #fff; border: none; }
.followed { background-color: #cccc; color: #666; }
.divider { height: 1rpx; background: #eee; margin: 30rpx 0; }
.content { background: #fff; padding: 30rpx; border-radius: 20rpx; font-size: 28rpx; }
</style>
五、开发踩坑 & 问题解决(重点)
坑 1:browse_rate 是字符串,直接排序错乱
问题:browse_rate 存储 1000,200,300 这类字符串,数据库 / ORM 直接按字符排序,逻辑错误。 解决:后端手动拆分字符串,截取第一个值转为数字类型再排序,同时兼容旧的单值数据。
坑 2:后端排序临时字段导致接口 500 报错
问题:排序用的 sort_num 临时字段被塞进返回数据,前端解析 + 后端取值双重报错。 解决:拆分「排序临时数据」和「最终返回数据」,临时字段不对外输出。
坑 3:前端 Prop 类型警告(数字 vs 字符串)
问题:本地缓存用户 ID 是数字,子组件要求字符串类型,控制台报类型错误。 解决:使用 String(global.getUserId()) 强制类型转换。
坑 4:接口 URL 地址错误
问题:后端接口迁移到 interaction 应用后,前端地址未同步,请求失败。 解决:统一前端请求路径为 /interaction/xxx。
坑 5:重复关注问题
问题:用户可以多次点击关注,产生多条重复数据。 解决:数据表设置联合唯一索引,后端使用 get_or_create 避免重复插入。
六、功能整体效果
- 数据动态化:彻底抛弃静态写死用户,全部由接口动态拉取;
- 智能推荐:自动排除自己,按浏览指数降序,固定展示 4~5 人;
- 交互统一:头像跳转逻辑和项目其他页面保持一致,用户体验统一;
- 状态联动:个人主页实时识别关注状态,关注 / 取关双向操作,数据持久化;
- 高容错:空数据、参数异常、网络错误均有兜底处理。
七、后续优化方向
- 增加多维度排序(粉丝数、获赞数),丰富推荐规则;
- 推荐列表增加「新动态」小红点提示;
- 增加拉黑、屏蔽推荐用户功能;
- 前端增加加载动画,优化弱网体验。
总结
本次基于 browse_rate 实现的用户推荐功能,核心难点在于字符串字段的数字排序和前后端交互联调。整体采用「接口分层 + 组件化」开发,代码结构清晰、易维护。整套方案可直接复用在博客、社区、社交类项目的用户推荐、好友推荐场景,有需要的小伙伴可以参考借鉴。
更多推荐
所有评论(0)