普通视图

发现新文章,点击刷新页面。
昨天 — 2025年10月29日首页

从 0 到 1,我用小程序 + 云开发打造了一个“记忆瓶子”,记录那些重要的日子!

2025年10月29日 09:26

缘起:为什么需要一个“记忆瓶子”?

市面上有很多纪念日 APP,但它们常常伴随着各种广告弹窗、冗余功能,或者在UI设计上未能满足我个人对于“简洁与美”的追求。我希望能有一个小而美的应用,能够:

  • 纯粹地记录重要的公历或农历纪念日。
  • 能够自由设置重复提醒。
  • 界面美观,甚至可以自定义背景图。
  • 最重要的是,没有多余的干扰,安静地守护那些珍贵的回忆。

于是,“记忆瓶子”的构思便在脑海中逐渐成形。在深入技术细节之前,欢迎扫码快速体验‘记忆瓶子’! gh\_6ba58d08bd84\_344.jpg

技术选型:原生小程序 + 云开发的“双剑合璧”

作为一名开发者,选择合适的技术栈是项目成功的关键。

  • 微信小程序: 无疑是触达用户最便捷的平台之一。其轻量级、无需下载安装的特性,以及微信生态内丰富的接口能力(订阅消息、用户授权等),都让它成为首选。
  • 微信云开发: 这次我选择了“All in 云开发”的模式。云开发提供了数据库、云存储、云函数等一站式服务,大大降低了后端开发和运维的复杂度。对于个人开发者而言,免费额度友好,上手成本极低,能够让我们更专注于前端和业务逻辑的实现。

这套组合让我在短时间内能够快速迭代,将产品想法落地。

核心功能与技术亮点

1. 公农历转换与复杂日期计算的“艺术”

“纪念日”的核心就是日期。但它远非简单的加减法,公历、农历、重复、指定年数,甚至还有时区问题,都让日期计算变得异常复杂。

痛点: 用户可能想记录一个农历生日,每年自动提醒;或者一个固定公历的周年纪念日;又或者是一个只发生一次的特殊事件。如何精准地计算出这些纪念日的“下一个发生日期”,并与当前日期进行比对,是小程序的核心挑战。 解决方案: 我引入了两个强大的日期处理库:

  • solarlunar: 用于精准地进行公历和农历之间的转换。尤其是在处理闰月等复杂情况时,它的表现非常可靠。
  • dayjs 及其 utc/timezone 插件: 这两个插件对于处理跨时区(尤其是中国大陆)的日期计算至关重要。我发现,仅仅使用 new Date() 可能会在服务器和用户手机之间产生时区差异,导致“今天”的判断不准确。通过 dayjs().tz('Asia/Shanghai').startOf('day') 能够确保在云函数和前端都以统一的“上海时间”零点作为基准,从而确保倒计时和提醒的精确性。

核心逻辑片段(getList 云函数中计算 nextOccurrenceTimestamp 的部分简化版):

// 假设 item.timestamp 是事件的UTC时间戳
const eventDayjsUTC = dayjs(item.timestamp);
const eventInTimeZone = eventDayjsUTC.tz(TARGET_TIMEZONE); // TARGET_TIMEZONE = 'Asia/Shanghai'
const todayStartInTimeZone = dayjs().tz(TARGET_TIMEZONE).startOf('day');

let nextSolarYear, nextSolarMonth, nextSolarDay;

if (item.dateType === 'lunar' && item.isRepeat) {
    // 农历重复纪念日的复杂计算...
    let nextSolar = solarlunar.lunar2solar(todayStartInTimeZone.year(), item.lunarMonth, item.lunarDay, item.isLeapMonth || false);
    // ... 如果今年已过,则计算明年的 ...
    if (dayjs.tz(`${nextSolar.year}-${nextSolar.month}-${nextSolar.day}`, TARGET_TIMEZONE).valueOf() < todayStartInTimeZone.valueOf()) {
        nextSolar = solarlunar.lunar2solar(todayStartInTimeZone.year() + 1, item.lunarMonth, item.lunarDay, item.isLeapMonth || false);
    }
    nextSolarYear = nextSolar.year;
    nextSolarMonth = nextSolar.month;
    nextSolarDay = nextSolar.day;

} else if (item.dateType === 'solar' && item.isRepeat) {
    // 公历重复纪念日的计算...
    nextSolarYear = todayStartInTimeZone.year();
    nextSolarMonth = eventInTimeZone.month() + 1;
    nextSolarDay = eventInTimeZone.date();
    // ... 如果今年已过,则计算明年的 ...
    if (dayjs.tz(`${nextSolarYear}-${nextSolarMonth}-${nextSolarDay}`, TARGET_TIMEZONE).valueOf() < todayStartInTimeZone.valueOf()) {
        nextSolarYear++;
    }
} else {
    // 一次性纪念日的计算...
    nextSolarYear = eventInTimeZone.year();
    nextSolarMonth = eventInTimeZone.month() + 1;
    nextSolarDay = eventInTimeZone.date();
}

// 最终构建下一个事件的 Day.js 对象
const nextEventDateStr = `${nextSolarYear}-${String(nextSolarMonth).padStart(2,'0')}-${String(nextSolarDay).padStart(2,'0')}`;
const nextEventStartInTimeZone = dayjs.tz(nextEventDateStr, TARGET_TIMEZONE);
const nextOccurrenceTimestamp = nextEventStartInTimeZone.valueOf();

// ... 剩余的倒计时计算 ...

订阅消息:不错过每一个重要提醒

纪念日如果没有提醒,就失去了意义。“记忆瓶子”集成了微信小程序的订阅消息功能。

实现:

  • 用户在小程序内通过 wx.requestSubscribeMessage 授权接收提醒。
  • 我部署了一个定时触发的云函数 checkReminders。该云函数会每天运行,遍历所有用户的纪念日,计算每个纪念日的目标提醒日期(即“下一个发生日期”减去“提前提醒天数”)。
  • 如果目标提醒日期恰好是今天,云函数便会调用 cloud.openapi.subscribeMessage.send 发送订阅消息。
  • 踩坑提示: 在发送订阅消息时,有一个关键参数 miniprogramState初期为了调试方便,我将其设为 'developer',导致用户点击消息后跳转到开发版小程序! 正式上线后,务必将其改为 'formal',确保用户跳转到正式发布版。

checkReminders 云函数核心片段:

// 假设 nextOccurrenceTimestamp 已经计算好
const targetReminderTimestamp = nextOccurrenceTimestamp - (reminderDaysBefore * ONE_DAY_MS);

if (targetReminderTimestamp === todayTimestamp) { // todayTimestamp 是目标时区今天的0点时间戳
    // 构建消息数据
    const messageData = { /* ... */ };
    remindersToSend.push({
        touser: item._openid,
        templateId: REMINDER_TEMPLATE_ID,
        page: `pages/detail/index?id=${item._id}`,
        data: messageData,
        miniprogramState: 'formal' // 【关键】确保跳转到正式版!
    });
}

云存储与 Base64 图片上传:打造个性化背景

为了让每个纪念日卡片都能拥有独特的视觉效果,我引入了自定义背景图功能。然而,云开发免费版存储空间有限,直接上传大量图片并不是最优解。

巧妙的解决方案:

  • 用户选择图片后,小程序端会先对图片进行压缩处理(减少数据量)。
  • 接着,将压缩后的图片数据转为 Base64 编码字符串
  • 这个 Base64 字符串会被直接作为纪念日记录的一个字段,存储在云数据库中(而非云存储)。
  • 前端渲染时,直接将这个 Base64 字符串赋值给 <img> 标签的 src 属性,图片便能直接显示。

【好处】 : 这种方式巧妙地绕过了云存储的容量限制,对于图片数量不多的个人应用来说,大大节约了成本,并且部署简单,加载速度也很快。

UI/UX 优化与样式攻坚战:Vant Weapp 的“爱恨交织”

为了快速构建美观的界面,我选择了 Vant Weapp 组件库。它提供了丰富的组件和完善的文档,确实提升了开发效率。然而,在详情页的底部操作按钮布局上,我遭遇了一场漫长而痛苦的“攻坚战”!

问题描述: 在详情页,我希望“编辑”和“删除”两个按钮能够并排显示,且平分底部空间,左右留出相等的内边距。在开发者工具模拟器上,通过 display: flex; gap: 24rpx; 配合 <van-button custom-class="action-button" />.action-button { flex: 1; min-width: 0; box-sizing: border-box; } 似乎完美解决了。然而,在真机上,按钮的布局却始终是“左边有空白,右边没有”,呈现出不均衡甚至轻微溢出的情况!

排查过程:

  • 检查 flex 容器和子项的 padding, margin, box-sizing
  • 尝试使用 calc() 函数精确计算宽度。
  • 利用真机调试功能,我发现罪魁祸首竟然是微信小程序底层对原生 button 注入的默认样式wx-button:not([size=mini]) { width: 184px; ...; margin-left: auto; margin-right: auto; }。这个固定宽度和 auto 外边距在 Vant Weapp 复杂的组件结构内部,以某种难以覆盖的方式生效,破坏了我的 flex: 1 布局!

最终解决方案: 在尝试了各种 !important 覆盖、多层选择器、甚至修改 Vant 内部样式(失败告终)之后,我做出了一个决定:放弃使用 van-button 来实现底部操作按钮

我选择用两个原生的 <view> 标签来模拟按钮:

<view class="action-button-wrapper">
  <view class="custom-action-button custom-edit-button" hover-class="button-hover" bindtap="onEdit">
    <text>编 辑</text>
  </view>
  <view class="custom-action-button custom-delete-button" hover-class="button-hover" bindtap="onDelete">
    <text>删 除</text>
  </view>
</view>

并结合以下 CSS 样式:

.action-button-wrapper {
  display: flex;
  gap: 24rpx;
  padding: 40rpx 24rpx;
  box-sizing: border-box;
  width: 100%;
}
.custom-action-button {
  flex: 1;
  min-width: 0;
  box-sizing: border-box;
  height: 80rpx;
  border-radius: 40rpx;
  font-size: 28rpx;
  font-weight: bold;
  color: #FFFFFF;
  text-align: center;
  line-height: 80rpx;
}
.custom-edit-button { background-color: #C8B6B6; }
.custom-delete-button { background-color: #ee0a24; }
.button-hover { opacity: 0.85; }

这套方案简单、直接,让我对样式有了 100% 的掌控力,最终在真机上完美实现了预期布局。有时候,回归原生是最可靠的解决方案!

版本更新机制:确保用户始终使用最新功能

为了避免用户因为小程序缓存而无法体验最新功能或修复的 Bug,我在 app.js 中集成了 wx.getUpdateManager()。它能检测到小程序新版本下载完成后,弹窗提示用户重启。

关键点:

  • 监听器 onUpdateReady 必须在 onLaunch 时就设置好。
  • 【非常重要】 每次上传新版本前,务必手动修改项目根目录下 project.config.json 文件中的 "version" 字段(如从 1.0.0 改为 1.1.0),否则微信后台无法识别为新版本,更新机制也无法触发。

产品未来展望

“记忆瓶子”才刚刚起步,未来还有很多有趣的功能等待探索:

  • 共享空间: 允许多人(如情侣、家人)共同创建和维护纪念日,分享彼此的珍贵时刻。
  • 更多主题与精美分享: 提供更丰富的 UI 主题、背景素材,并支持一键生成精美的分享图片。
  • 年度总结与历史上的今天: 增加趣味性功能,回顾一年的点滴,或发现“历史上的今天”发生了什么。

总结

开发“记忆瓶子”的过程,是一个不断学习、不断解决问题的过程。它让我对小程序和云开发有了更深的理解,也再次体会到作为一名开发者,能够将自己的想法变为现实的乐趣。

如果你也对这个小程序感兴趣,欢迎扫码体验,并留下宝贵的反馈和建议!你的每一次使用和反馈,都将是“记忆瓶子”继续成长的动力。再次邀请体验和反馈

gh\_6ba58d08bd84\_344.jpg

感谢阅读!

昨天以前首页

AI 友好的云开发 MySQL SDK 它来了!微信小程序能直连关系型数据库了

作者 蛋先生DX
2025年10月27日 10:08

文章封面.png

写在最前

腾讯云开发最近上新了个能力,能让微信小程序(当然不止小程序啦~)直接访问 MySQL 云数据库,这给咱们开发者又多了一种直接操作数据库的方式。具体是咋肥事呢?且听下文慢慢道来~

前奏铺垫

丹尼尔:蛋兄,听说腾讯云开发近期刚推出一个新功能,现在微信小程序可以直接访问 MySQL 数据库了?

蛋先生:哟,消息挺灵通的嘛!其实以前也是可以访问 MySQL 数据库的,只是需要通过数据模型来间接操作

丹尼尔:数据模型?

蛋先生:可以理解为数据库之上的一层 ORM 层

丹尼尔:那既然都可以访问,有啥不同?

蛋先生:越上层,使用起来会更简单一些,但限制也会多一些,毕竟是在数据库之上的一层黑盒封装。而越接近底层,则会越灵活,开发者能发挥的空间也就越大

丹尼尔:哦,越接近底层,是不是越难了

蛋先生:倒不会啦,毕竟只是一些数据库使用的浅层知识,这点小菜对开发者们来说不在话下~

JS SDK - 前端开发者的福音

丹尼尔:那具体怎么用呢?

蛋先生:如果你对前端技术栈比较熟悉,那 js sdk 就是你的最佳选择了!无论是在 web 应用、服务端 nodejs 应用还是微信小程序里,都提供了相应的 js sdk 来直接访问 mysql 数据库

丹尼尔:举个栗子?

蛋先生:那我就举两个比较有代表性的例子吧:一个是简单的单表查询,另一个是稍复杂的多对多查询。至于其他场景,你可以自己去翻翻官方的文档,写得还是挺全面的

丹尼尔:好咧!

 ✎ 初始化

蛋先生:一切的开始,还是得从初始化说起。通过下面这段代码,我们就能在微信小程序中创建一个 MySQL 数据库连接实例。当然,对于 Web 应用或服务端的 Node.js 应用,也可以用类似的方式进行初始化

const { init } = require("@cloudbase/wx-cloud-client-sdk");

...

wx.cloud.init({
  env"{{这里填上你的云开发环境 ID 哦}}",
});

const db = init(wx.cloud).mysql();

丹尼尔:OK,代码灰常的简单

蛋先生:接下来,我就用下面这三个简单的表来进行演示

 ✎ 简单的单表查询

蛋先生:先来演示一下最基础的单表查询

// 从 `articles` 表中查询所有 `title` 包含 "云开发" 的记录
const { data, error } = await db
  .from("articles")
  .select("*")
  .like("title", "%云开发%")
  
// 返回 data 的示例值
[
  {
    id: 1,
    title: "云开发MySQL新功能详解",
  }
];

丹尼尔:单表查询确实挺简单的,一看就明白

 ✎ 复杂的多对多

蛋先生:那我们再来看一下多对多查询,其实也同样很直观

// 从 `articles` 表中查询所有记录,并只返回每条记录的 id、title 字段,以及关联的 tags 中的 name 字段
const { data, error } = await db
  .from("articles")
  .select("id, title, tags(name)")
  
// 返回 data 的示例值
[
  {
    id: 1,
    tags: [
      {
        name: "云开发",
      },
      {
        name: "MySQL",
      },
    ],
    title: "云开发MySQL新功能详解",
  },
  {
    id: 2,
    tags: [
      {
        name: "微信小程序",
      },
    ],
    title: "微信小程序性能优化指南",
  }
]

丹尼尔:哇塞,太酷了!我之前用过 Supabase 的 supabase-js SDK,感觉两者很像啊!

蛋先生:你眼光真准!云开发的 MySQL JS SDK 在设计上确实和 supabase-js SDK 几乎一模一样

丹尼尔:为什么要这么设计呢?

蛋先生:我觉得啊,时代不同了。以前完全可以随意设计自己的 SDK,但现在是 AI 时代,要让 AI 熟悉一种新的 SDK 使用方式,是需要时间成本的。保持与 supabase-js SDK 一致,可以提高 AI 的适应性,毕竟现在的开发者几乎都离不开 AI 编程助手。

HTTP API - 非 JS 编程语言开发者的利器

丹尼尔:嗯,确实挺好用的。不过这样就只限于 JS 了。如果我想用 Go 来访问 MySQL 云数据库,该怎么办呢?

蛋先生:虽然目前还没有提供 Go SDK,但云开发贴心地提供了 HTTP API,所以你可以用任何编程语言写一个 HTTP 客户端来访问 MySQL 数据库

丹尼尔:这个 HTTP API 是什么样子的?

蛋先生:同样为了提高 AI 的适应性,HTTP API 在设计上选择遵循 PostgREST 标准协议

丹尼尔:哦,那也来用两个例子演示一下吧

蛋先生:好,那就满足一下你的好奇心吧

 ✎ 简单的单表查询

蛋先生:一样的,先来演示一个简单的单表查询

# 从 `articles` 表中查询所有 `title` 包含 "云开发" 的记录

curl -i -X GET 'http://{{host}}/v1/rdb/rest/v1/articles?select=*&title=like.%云开发%' \
-H 'Authorization: Bearer <This is a fake token, please use yours>' \
-H 'Prefer: return=representation'

丹尼尔:不错,依然简单明了

 ✎ 复杂的多对多

蛋先生:最后,我们也来演示一个多对多的查询

# 从 `articles` 表中查询所有记录,并只返回每条记录的 id、title 字段,以及关联的 tags 中的 name 字段

curl -i -X 'GET http://{{host}}/v1/rdb/rest/articles?select=id,title,tags(name)' \
-H 'Authorization: Bearer <This is a fake token, please use yours>'

丹尼尔:Cool~

控制台 - 数据库管理新体验

蛋先生:这次还配套上线了在线 MySQL 数据库管理端。功能虽然算不上花哨,但我觉得关联表功能比以前的数据模型要好一些

丹尼尔:怎么说?

蛋先生:举个例子,user_article 模型里有个字段 user_id 要关联 user 模型,在配置关联时,需要声明在 user 模型中的字段,一开始我还真不适应

丹尼尔:确实,居然可以在 user_article 模型的地盘直接给 user 模型加字段,都不知道 user 模型同意不?

蛋先生:呵呵!还有一个问题,如果 user 模型被多个模型关联,那简直就是灾难。因为在 user 模型里会出现多个代表其他模型的字段,如果这里有 10 个,你可以想象字段在 user 模型中泛滥成灾的景象

丹尼尔:嗯,确实如此。那新的 MySQL 数据库管理端是什么样的?

蛋先生:跟你预期的一样,user_article 表的 user_id 关联 user 表的 _id,一切都是那么自然

丹尼尔:哦,那 user 表的字段也不会被污染了

蛋先生:是的,看起来是不是很清爽

丹尼尔:那 user_article 能查看具体关联的 user 信息吗?这应该是 user_article 表关心的内容

蛋先生:当然可以

不足与期待

丹尼尔:看起来很不错啊。哦,对了,我记得以前数据模型提供的 SDK 是没有事务支持的

蛋先生:是的,这确实是个小硬伤。遗憾的是,这次 MySQL JS SDK 依旧没带上。不过嘛,事务本来就主要用在服务端,对小程序等前端调用影响不大。听说后面是有计划推出的,然后还有实时推送等这些刚需功能,还是可以小期待一下的~So,在这一块上,和数据模型算是五五开吧 😅

丹尼尔:嗯,那我先去了解了解,同时期待这些实用的功能尽快上线

写在最后

“亲们,都到这了,要不,点赞或收藏或关注支持下我呗 o( ̄▽ ̄)d”

❌
❌