我要在Mongodb中创建一个成就系统。但是我不确定如何将其格式化/存储在数据库中。
从用户的角度来看,应该有一个进步(每完成一个成就,他们就会存储一些progress value
),我真的感到困惑,这是执行此操作的最佳方法,并且没有性能问题。
我该怎么办?,因为我不知道,我的想法也许是这样的:
我是否应该将每个成就存储在Achievement集合的唯一行中,并在该行中存储一个包含用户ID和成就进度的对象的用户数组?
然后我会在1000多个成就中遇到一个性能问题,那就是经常被检查的仙女吗?
还是我应该做些其他事情?
上述选项的示例架构:
{
name:{
type:String,
default:'Achievement name'
},
users:[
{
userid:{
type:String,
default:' users id here'
},
progress:{
type:Number,
default:0
}
}
]
}
答案 0 :(得分:2)
即使问题是专门针对数据库设计的,我也会为跟踪/奖励逻辑提供解决方案,以便为数据库设计建立更准确的上下文。
我会将成就进度与已授予的成就分开存储,以便进行更清晰的跟踪和发现。
整个逻辑是基于事件的,并有多层事件处理。这为您在跟踪数据的方式上提供了大量的灵活性,并为您提供了一个非常好的跟踪历史的机制。基本上,您可以将其视为一种日志记录形式。
当然,您的系统设计和合同高度依赖于您要跟踪的信息及其复杂性。一个简单的 progress
字段可能无法满足每种情况(您可能想要跟踪更复杂的东西,而不是 X 和 Y 之间的简单数字)。还有一种情况是跟踪数据更新非常频繁(例如,在游戏中移动的距离)。您没有提供有关成就系统主题的任何背景信息,因此我们将坚持使用通用解决方案。这只是您应该注意的几件事,因为它会影响设计。
好的,那么,让我们从顶部开始,跟踪被跟踪数据的整个流程及其最终成就进度。假设我们正在跟踪用户登录的连续天数,当他达到 [10] 时,我们将授予他一项成就。
请注意,下面的所有内容都只是一个伪代码。
那么,假设今天是 [2017 年 7 月 8 日]。目前,我们的 User
实体如下所示:
User: {
id: 7;
trackingData: {
lastLogin: 7 of July, 2017 (should be full DateTime object, but using this for brevity),
consecutiveDays: 9
},
achievementProgress: [
{
achievementID: 10,
progress: 9
}
],
achievements: []
}
我们的成就集合包含以下实体:
Achievement: {
id: 10,
name: '10 Consecutive Days',
rewardValue: 10
}
用户尝试登录(或访问网站)。应用程序处理程序注意到这一点,并在处理登录逻辑后触发 ACTION
类型的事件:
ACTION_EVENT = {
type: ACTION,
name: USER_LOGIN,
payload: {
userID: 7,
date: 8 of July, 2017 (should be full DateTime object, but using this for brevity)
}
}
我们有一个 ActionHandler
来监听 ACTION
类型的事件:
ActionHandler.handleEvent(actionEvent) {
subscribersMap = Map<eventName, handlers>;
subscribersMap[actionEvent.name].forEach(subscriber => subscriber.execute(actionEvent.payload));
}
subscribersMap
为我们提供了一组处理程序,这些处理程序应该响应每个特定操作(这应该为我们解析为 USER_LOGIN
)。在我们的例子中,我们可以有 1 或 2 个关注更新 lastLogin
实体中 consecutiveDays
和 user
跟踪属性的用户跟踪信息。我们案例中的处理程序将更新跟踪信息并进一步触发新事件。
再次,为简洁起见,我们将两者合二为一:
updateLoginHandler: function(payload) {
user = db.getUser(payload.userID);
let eventType;
let eventValue;
if (date - user.trackingData.lastLogin > 1 day) {
user.trackingData = 1;
eventType = 'PROGRESS_RESET';
eventValue = 1;
}
else {
const newValue = user.trackingData.consecutiveDays + 1;
user.trackingData.consecutiveDays = newValue;
eventType = 'PROGRESS_INCREASE';
eventValue = newValue;
}
user.trackingData.lastLogin = payload.date;
/* DISPATCH NEW EVENT OF TYPE ACHIEVEMENT_PROGRESS */
AchievementProgressHandler.dispatch({
type: ACHIEVEMENT_PROGRESS
name: eventType,
payload: {
userID: payload.userID,
achievmentID: 10,
value: eventValue
}
});
}
此处,PROGRESS_RESET
与 PROGRESS_INCREASE
具有相同的约定,但具有不同的语义含义,为了历史/跟踪目的,我会将它们分开。如果您愿意,可以将它们合并为一个 PROGRESS_UPDATE
事件。
基本上,我们更新依赖于 lastLogin
日期的跟踪字段并触发一个新的 ACHIEVEMENT_PROGRESS
事件,该事件应该由具有相同模式 (AchievementProgressHandler
) 的单独处理程序处理。在我们的例子中:
ACHIEVEMENT_PROGRESS_EVENT = {
type: ACHIEVEMENT_PROGRESS,
name: PROGRESS_INCREASE
payload: {
userID: 7,
achievementID: 10,
value: 10
}
}
然后,在 AchievementProgressHandler
中,我们遵循相同的模式:
AchievementProgressHandler: function(event) {
achievementCheckers = Map<achievementID, achievementChecker>;
/* update user.achievementProgress code */
switch(event.name): {
case 'PROGRESS_INCREASE':
achievementCheckers[event.payload.achievementID].execute(event.payload);
break;
case 'PROGRESS_RESET':
...
}
}
achievementCheckers
包含每个特定成就的检查器函数,用于决定成就是否达到其期望值(进度为 100%)并应授予。这使我们能够处理各种复杂的情况。如果您只跟踪 Y 场景中的一个 X,您可以在所有成就之间共享该功能。
处理程序基本上是这样做的:
achievementChecker: function(payload) {
achievementAwardHandler;
achievement = db.getAchievement(payload.achievementID);
if (payload.value >= achievement.rewardValue) {
achievementAwardHandler.dispatch({
type: ACHIEVEMENT_AWARD,
name: ACHIEVEMENT_AWARD,
payload: {
userID: payload.userID,
achievementID: achievementID,
awardedAt: [current date]
}
});
/* Here you can clear the entry from user.achievementProgress as you no longer need it. You can also move this inside the achievementAwardHandler. */
}
}
我们再次调度事件并使用事件处理程序 - achievementAwardHandler
。您可以跳过事件创建步骤,直接将成就奖励给用户,但我们会使其与整个历史记录流程保持一致。
这里的一个额外好处是,您可以使用处理程序将成就授予推迟到特定的稍后时间,从而有效地为多个用户批量授予奖励,这有几个目的,包括性能增强。
基本上,这个伪代码处理从[用户操作]到[成就奖励]的流程,包括所有中间步骤。它不是一成不变的,您可以随心所欲地修改它,但总而言之,它为您提供了清晰的关注点分离,更清洁的实体,它的性能,让您添加复杂的检查和处理程序,这些检查和处理程序在相同的情况下易于推理time 提供了用户整体进度的重要历史记录。
关于数据库架构实体,我建议如下:
User: {
id: any;
trackingData: {},
achievementProgress: {} || [],
achievements: []
}
地点:
trackingData
是一个包含你想要的一切的对象
跟踪用户。美妙之处在于这里的属性是
独立于成就数据。您可以跟踪任何内容并最终将其用于成就目的。achievementProgress
:<key: achievementID, value: data>
的地图或
一个包含每个成就的当前进度的数组。achievements
:一系列获奖成就。和Achievement
:
Achievement: {
id: any,
name: any,
rewardValue: any (or any other field/fields. You have complete freedom to introduce any kind of tracking with the approach above),
users?: [
{
userID: any,
awardedAt: date
}
]
}
users
是获得给定成就奖励的用户的集合。这是可选的,仅当您使用它并经常查询此数据时才出现在这里。
答案 1 :(得分:0)
您可能正在寻找的是徽章风格的实现。就像 Stack Overflow 用徽章奖励特定成就的用户一样。
方法 1:您可以在用户个人资料中为每个徽章设置标记。由于您是在 NoSQL 数据库中执行此操作,因此您只需为每个徽章设置一个标志。
const badgeSchema = new mongoose.Schema({
badgeName: {
type: String,
required: true,
},
badgeDescription: {
type: String,
required: true,
}
});
const userSchema = new mongoose.Schema({
userName: {
type: String,
required: true,
},
badges: {
type: [Object],
required: true,
}
});
如果您的应用架构是基于事件的,您可以触发向用户授予徽章。该操作只是在 User badges
数组中插入带有进度的 Badge 对象。
{
badgeId: ObjectId("602797c8242d59d42715ba2c"),
progress: 10
}
更新操作是查找并更新带有进度百分比数字的徽章数组
在用户界面上显示用户成就时,您可以循环遍历 badges
数组以显示该用户已获得的徽章及其取得的进展。
方法 2: 有一个单独的 mongo 集合用于徽章和用户映射。每当用户获得徽章时,您都会在该集合中插入一条记录。它将是用户 _id
和徽章 _id
以及进度值的一对一映射。但随着表的增长,您将需要建立索引以有效查询用户和徽章映射。
您必须根据您的特定用例对最佳方法进行分析。
答案 2 :(得分:0)
MongoDB 足够灵活,可以让团队快速开发应用程序,并在应用程序需要时让他们的模型产生摩擦。如果您从一开始就需要一个强大的模型,他们的方法是一种灵活的方法,可以指导您完成数据建模过程。
methodology 由以下组成:
您可以通过以下方式获得:
关系:确定数据中不同实体之间的关系,量化这些关系并应用嵌入或链接。一般来说,默认情况下您应该更喜欢嵌入,但请记住,数组不应无限制地增长 (6 Rules of Thumb for MongoDB Schema Design: Part 3)。
模式:应用架构设计模式。看看 Building with Patterns: A Summary,它提供了一个矩阵,突出显示了可能对给定用例有用的模式。
最后,此方法论的目标是帮助您创建一个模型,该模型可以在压力下扩展并表现良好。
答案 3 :(得分:0)
如果你像这样设计成就架构:
{
name: {
type: String,
default: "Achievement name",
},
userid: {
type: String,
default: " users id here",
},
progress: {
type: Number,
default: 0,
},
}
}
获得成就后,您只需添加另一个条目
为了获得成就 Map-Reduce 是在数据库上运行 map reduce 的好选择。您可以不定期运行它们,将它们用于离线计算您想要的数据。
基于 documentation 你可以像下面的照片一样