从“死了么”到“我在”:用uniCloud开发一款温暖人心的App

0 阅读6分钟

大家好,我是前端大鱼。

前几个月,“死了么”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的核心优势

  1. 一体化开发:前端直接调用云函数,不用配域名、HTTPS、跨域
  2. 定时任务内置:通过trigger配置,比node-cron更稳定
  3. 推送集成:uni-push 2.0直接可用,支持离线推送
  4. 免费额度够用:阿里云或腾讯云空间,每月有免费调用次数
  5. 自动扩缩容:不用关心服务器压力

四、技术架构

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元


七、写在最后

「我在」这个名字,是我能想到的最温暖的回答。

如果你也想做独立开发,欢迎评论区聊聊:

  1. 你最想要「我在」有什么功能?
  2. 你会为哪些功能付费?
  3. 这个名字,你喜欢吗?

评论区抽三位送终身会员(如果App真做出来的话😂)


关注公众号" 大前端历险记",掌握更多前端开发干货姿势!