从“死了么”到“我在”:用uniCloud开发一款温暖人心的App
大家好,我是前端大鱼。
前几个月,“死了么”App火了。几个人很短的时间做了一个极简功能——每天签到,两天不签就发邮件给紧急联系人。就这么简单,冲上了付费榜第一。
评论区最高赞的留言我一直记得:“名字太晦气了,为什么不叫‘活着么’?”
我想了很久。活着么?还是有点丧。后来有个读者留言说:“叫‘我在’吧,两个字,既回答了活着,也说出了陪伴。”
就它了。
「我在」——双层含义:我在(活着)、我在这里(守护你)。
今天这篇文章,是一个完整的项目规划书,从产品构想到技术实现,希望能给想做独立开发的朋友一些启发。
一、「我在」是什么?
1.1 产品定位
“死了么”的核心逻辑很简单:每日签到,两天不签就发邮件通知紧急联系人。它的成功在于直面死亡的黑色幽默。
但「我在」不想做第二个“死了么”。
我的产品定位是:从“怕死”到“惜活”,从“被动通知”到“主动记录”,从“孤独一人”到“有人陪伴”。
简单说:
- “死了么”是在你可能死了的时候通知别人
- 「我在」是在你确定活着的时候记录自己,同时告诉在乎你的人:我还在
1.2 核心功能
| 功能模块 | 具体内容 | 免费/付费 |
|---|---|---|
| 每日签到 | 一键打卡“我在”,记录心情和今日小事 | 免费 |
| 守护者机制 | 绑定一位守护者,渐进式提醒,48小时未签发送邮件 | 免费 |
| 心情日记 | 记录每天的情绪和琐事,形成时光相册 | 付费 |
| 时光胶囊 | 写给未来的自己,1/3/5/10年后打开 | 付费(免费限3个) |
| 生命树 | 连续签到养成虚拟树,30天长叶,365天开花 | 付费皮肤 |
| 陪伴地图 | 匿名查看全国用户的“我在”状态 | 免费 |
1.3 渐进式提醒机制
这是「我在」最核心的守护功能:
- 12小时未签到:App推送提醒用户自己:“今天记得说‘我在’哦”
- 24小时未签到:通知守护者:“你守护的人今天还没说‘我在’”
- 36小时未签到:守护者需确认是否联系上你
- 48小时未签到:发送邮件给紧急联系人:“您的亲友已48小时未说‘我在’”
二、为什么叫「我在」?
这两个字,我想了很久。
第一层含义:活着。 当你在App里点击“我在”,就是在告诉世界:今天我也在好好地活着。
第二层含义:陪伴。 当你成为别人的守护者,你的存在本身就是一种承诺——“别怕,我在。”
第三层含义:回响。 在这个孤独的时代,有人问你“在吗”,你可以回一句“我在”。简单,却温暖。
比起“活着么”的质问,「我在」更像是一个回答,一个承诺,一个拥抱。
三、为什么选uniCloud + UniApp?
作为一个独立开发者,我的选型原则是:一次编写,多端运行,免运维,低成本。
3.1 uniCloud的核心优势
- 一体化开发:前端直接调用云函数,不用配域名、HTTPS、跨域
- 定时任务内置:通过trigger配置,比node-cron更稳定
- 推送集成:uni-push 2.0直接可用,支持离线推送
- 免费额度够用:阿里云或腾讯云空间,每月有免费调用次数
- 自动扩缩容:不用关心服务器压力
四、技术架构
4.1 整体架构图
4.2 云函数结构
cloudfunctions/
├── user/ # 用户相关
├── checkin/ # 签到相关
├── capsule/ # 时光胶囊
├── timer/ # 定时任务
└── common/ # 公共模块
五、核心代码实现
5.1 数据库设计(简版)
users集合:
{
"_id": "用户ID",
"nickname": "昵称",
"guardian_id": "守护者ID",
"emergency_email": "紧急联系人邮箱",
"last_checkin": "最后签到时间",
"continuous_days": "连续签到天数"
}
checkins集合:
{
"user_id": "用户ID",
"mood": "心情",
"note": "今日小事",
"create_date": "签到时间"
}
5.2 签到云函数:说“我在”
// cloudfunctions/checkin/create.js
exports.main = async (event, context) => {
const { mood, note } = event;
const { uid } = context.auth;
const db = uniCloud.database();
const dbCmd = db.command;
// 检查今天是否已签到
const today = new Date();
today.setHours(0, 0, 0, 0);
const exist = await db.collection('checkins').where({
user_id: uid,
create_date: dbCmd.gte(today)
}).get();
if (exist.data.length > 0) {
return { code: 400, msg: '今天已经说过“我在”了' };
}
// 获取用户信息
const user = await db.collection('users').doc(uid).get();
const userData = user.data[0];
// 计算连续天数
let continuousDays = 1;
if (userData.last_checkin) {
const last = new Date(userData.last_checkin);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (last >= yesterday) {
continuousDays = (userData.continuous_days || 0) + 1;
}
}
// 开启事务
const transaction = await db.startTransaction();
try {
// 插入签到记录
await transaction.collection('checkins').add({
user_id: uid,
mood,
note,
create_date: new Date(),
continuous_days: continuousDays
});
// 更新用户信息
await transaction.collection('users').doc(uid).update({
last_checkin: new Date(),
continuous_days: continuousDays,
total_checkins: dbCmd.inc(1)
});
await transaction.commit();
// 通知守护者(如果有)
if (userData.guardian_id) {
uniCloud.callFunction({
name: 'sendPush',
data: {
userId: userData.guardian_id,
title: '❤️ 你守护的人说“我在”了',
content: `${userData.nickname}今天打卡了,连续${continuousDays}天`
}
});
}
return { code: 0, msg: '打卡成功', data: { continuous_days: continuousDays } };
} catch (e) {
await transaction.rollback();
throw e;
}
};
5.3 定时任务:检查未签到用户
// cloudfunctions/timer/checkReminder.js
'use strict';
exports.main = async (event, context) => {
const db = uniCloud.database();
const dbCmd = db.command;
const now = new Date();
// 查找48小时未签到的用户
const cutoff48 = new Date(now - 48 * 3600 * 1000);
const users48 = await db.collection('users').where({
last_checkin: dbCmd.lt(cutoff48),
emergency_email: dbCmd.exists(true)
}).get();
for (const user of users48.data) {
// 发送邮件给紧急联系人
await sendEmail({
to: user.emergency_email,
subject: '【紧急提醒】您的亲友可能失联',
html: `${user.nickname}已48小时未打卡,请确认其安全。`
});
}
// 查找24小时未签到的用户
const cutoff24 = new Date(now - 24 * 3600 * 1000);
const users24 = await db.collection('users').where({
last_checkin: dbCmd.lt(cutoff24),
guardian_id: dbCmd.exists(true)
}).get();
for (const user of users24.data) {
// 通知守护者
await sendPushToUser(user.guardian_id,
'你守护的人还没打卡',
`${user.nickname}已24小时未说“我在”`
);
}
return { code: 0 };
};
// 发送推送
async function sendPushToUser(userId, title, content) {
const uniPush = uniCloud.getPushManager();
await uniPush.sendMessage({ user_id: userId, title, content });
}
// 发送邮件(使用nodemailer)
async function sendEmail({ to, subject, html }) {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: 'smtp.qq.com',
port: 465,
secure: true,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
await transporter.sendMail({
from: `"我在" <${process.env.EMAIL_USER}>`,
to, subject, html
});
}
5.4 前端:首页调用云函数
<template>
<view class="container">
<view class="streak-card">
<text class="streak-num">{{ continuousDays }}</text>
<text class="streak-label">连续说“我在” {{ continuousDays }} 天</text>
</view>
<view v-if="!todayChecked">
<button @click="handleCheckin">说「我在」</button>
</view>
<view v-else>
<text>✅ 今天已打卡</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
continuousDays: 0,
todayChecked: false
}
},
onLoad() {
this.checkTodayStatus();
},
methods: {
async checkTodayStatus() {
const res = await uniCloud.callFunction({
name: 'checkin-status'
});
this.continuousDays = res.result.data.continuous_days;
this.todayChecked = res.result.data.today_checked;
},
async handleCheckin() {
uni.showLoading({ title: '打卡中...' });
const res = await uniCloud.callFunction({
name: 'checkin-create',
data: { mood: 'happy', note: '今天很好' }
});
if (res.result.code === 0) {
this.todayChecked = true;
this.continuousDays = res.result.data.continuous_days;
uni.showToast({ title: '打卡成功', icon: 'success' });
}
uni.hideLoading();
}
}
}
</script>
六、成本估算
免费额度(阿里云uniCloud)
| 资源项 | 免费额度 | 说明 |
|---|---|---|
| 云函数调用 | 10万次/月 | 支撑1000日活 |
| 云数据库 | 2GB | 存10万条记录 |
| 云存储 | 5GB | 存放照片 |
| CDN流量 | 5GB/月 | 图片加载 |
1000日活成本:0元
七、写在最后
「我在」这个名字,是我能想到的最温暖的回答。
如果你也想做独立开发,欢迎评论区聊聊:
- 你最想要「我在」有什么功能?
- 你会为哪些功能付费?
- 这个名字,你喜欢吗?
评论区抽三位送终身会员(如果App真做出来的话😂)
关注公众号" 大前端历险记",掌握更多前端开发干货姿势!