资讯动态

DoraMate架构设计:从产品定位到模块化单体的完整实践

发布时间:2026/10/14 23:21:07 来源:尧图企业网站定制
1. 不做定位就做架构等于没画图纸就开工DoraMate 系列进展到第三篇终于可以聊一个很多人跳过的环节了。市面上绝大多数半途夭折的个人项目问题往往不是代码能力不够而是产品定位模糊、架构设计撑不住后续功能。DoraMate 也不例外早期原型阶段随手写的模块到中期开始频繁打补丁跟橡皮筋似的到处粘。这次干脆停下来补上产品定位和架构设计这两块地基。如果你正在做一个跨平台应用或正在规划自己的独立项目这篇文章值得看一下。我会把 DoraMate 从“想做什么”到“技术骨架怎么搭”的完整思考过程拆给你看包括定位推导、架构选型、模块划分、数据建模以及踩过的几个坑。先说结论产品定位决定架构的上限和下限。定位没想清楚后续所有技术方案都是沙滩上盖楼。1.1 产品定位先搞清楚“用户凭什么用你”DoraMate 的内核是“跨平台智能陪伴助手”。请注意“智能陪伴”这四个字本身非常危险——它过于宽泛一个人一个理解。有人以为要接大模型聊天有人以为是提醒事项有人以为要搞情绪日记。这就是典型的需求发散问题。为了收敛我采用了最朴素也最有效的方法找核心场景写用户故事。不需要敏捷开发那一整套仪式就一个问题——“用户在什么情境下、因为什么痛点、会用 DoraMate?”。最终收敛到三个核心场景碎片化提醒用户记不住喝水、久坐起身、每日复盘这类轻量事项需要无感式提醒。状态记录用户想记录当天的心情、精力值但不想打开正经日记 App心理负担太大。轻量对话用户偶尔需要一个能接话茬的对话对象但不想和一堆人解释“我只是无聊”。这三个场景有一条共同主线低门槛、低负担、高频率。换句话说DoraMate 不是“效率工具”也不是“聊天机器人”而是“一个带点温度的效率伴侣”。这个定位直接影响后面一系列技术取舍——界面交互不能太重、每次打开 App 的操作路径必须短、消息通知必须轻量且可自定义、核心数据模型尽量简单。1.2 从定位推导功能边界砍功能比加功能难十倍明确了核心场景接下来最重要的一步是定义非目标(Non-Goals)。我列了一张单子方向做不做原因提醒轻量任务、习惯打卡复杂项目管理、团队协作复杂工作流需要大量界面与逻辑偏离陪伴主线记录心情、每日评价、精力值完整日记排版、标签体系记录超过 2 步就违背“低负担”承诺AI 对话预设角色、短会话开放式 Agent、长对话长对话对上下文管理、接口开销、数据存储要求高且很难控制体验多端手机、桌面、手表消息预览全平台功能完全一致手表端只做被动查看不做主动输入这个边界不是拍脑袋定的它由定位反推出来的。当一个功能跑出来后不符合“低门槛、高频率、轻量化”主线砍掉不管技术上多有意思。提示定位的关键产出不是功能清单而是“不做什么”。架构最怕的不是少功能而是为不可预测的需求预留过度能力导致每一层都变得抽象又沉重。1.3 从定位推导出的产品功能边界与里程碑明确边界后我按“能不能独立演示”“能不能带出用户价值”两个维度把功能拆成了里程碑M1基础闭环账号体系 多端同步创建轻量任务/提醒快捷记录心情与精力值每日汇总卡片(里程碑演示题)M2伴侣感增强提醒消息可交互(直接标记完成、稍后提醒)本地数据导出基于时间线的状态回顾M3对话能力接入对接语言模型实现短会话对话内容自动提取待办/状态记录角色风格可选里程碑的划分关系到工程排期。M1 和 M2 不依赖大型 AI 能力技术上相对可控M3 才引入模型调用。这样即使后面 M3 延期M1/M2 依然是一个完整可用的产品不会出现“AI 聊不了天App 就是空壳”的尴尬。2. 架构设计的核心权衡单体、模块化还是微服务定位收敛后才轮到“怎么搭骨架”。DoraMate 选择的是模块化单体(Modular Monolith)而不是微服务也不是纯单体。这个决定是反复推演过的。2.1 三种架构的对比分析对中小型个人项目而言微服务是当前最容易被高估、最容易被过度设计的选项没有之一。对照如下维度纯单体模块化单体微服务启动成本最低中高代码失控风险高(依赖随意交叉)低(模块边界强制)低(服务边界强制)独立部署能力无有限(部分模块可拆分)强运维复杂度低低高(服务发现/链路追踪/日志聚合)适合场景简单工具/原型中型应用/长期演化多团队/高并发/复杂组织DoraMate 最终选择模块化单体的理由是:项目处于快速迭代期模块边界需要能随时调整同时单人维护成本必须尽量低不能接受微服务带来的运维负担。模块化单体本质是在一个进程内部强制模块边界模块之间只能通过公开接口交互禁止跨模块直接访问内部数据。把单体架构的简单性和领域建模的清晰度结合起来是这一阶段最合理的 Trade-off。2.2 分层架构设计从路由到存储的职责划分模块化单体只是整体形态模块内部还有分层。DoraMate 参考了经典的分层架构但做了一定简化目的就是不让层数过多影响交付效率。controller(接收 HTTP 请求参数校验) ↓ service(业务逻辑、事务控制) ↓ repository(数据访问) ↓ database(PostgreSQL Redis)每一层单向依赖不允许下层反向调用上层也不允许 controller 直接操作 repository。这个规则看似简单实际执行时特别容易被破坏——图省事直接在 controller 里写查询逻辑是我见过的最多的架构腐化起点。各层的职责约束:Controller 层只做输入解析和输出格式转换不写 if-else 业务分支。Service 层承载真正的业务逻辑包括事务边界、权限判断、领域事件发布。Repository 层只做数据存取不写业务判断。所有复杂查询都收敛到该模块的数据访问接口中。注意很多教程喜欢在分层架构上叠加 DDD(领域驱动设计)但 DDD 的上手成本偏高。DoraMate 的做法是只借鉴 DDD 的模块划分思路不在早期强行引入实体、值对象、领域服务等概念。模块边界清晰领域模型设计保留在模块内部演化不至于因为过度设计拖慢进度。2.3 模块边界划分的思考过程模块边界是怎么划出来的我用了两个原则变更频率和业务归属。变更频率原则很好理解一起变化的代码应该放在一起。DoraMate 的用户提醒逻辑和通知推送逻辑频繁一起改划成一个模块心情记录和时间线数据经常一起读也放一个模块。业务归属原则是一个业务概念只能在一个模块中作为“主人”其他模块要读它的数据必须通过接口。最终 DoraMate 规划了如下模块模块职责对外接口示例user注册登录、设备绑定UserService.Register()task任务/提醒/习惯打卡TaskService.CreateReminder()mood心情/精力值记录与查询MoodService.Record()notify消息推送、行程提醒触发NotifyService.Send()assistantAI 对话、意图识别AssistantService.Chat()report每日汇总/数据导出ReportService.BuildDaily()每个模块内部可有自己的子分层但模块之间只通过接口通信。如果某天assistant模块长到需要独立部署可以把它整个抽出来包一层 API其他模块调它的 HTTP 接口即可——这就是模块化单体向微服务演进的自然路径。3. 关键技术选型不追新只求稳接下来聊技术选型。DoraMate 的技术选型原则出乎意料地保守能不开源的用成熟的能不自己写的尽量不自己写一切以纯个人维护成本为锚点。3.1 技术选型的基本原则我先列了一个选型公式技术选型 生命周期长度 × 社区活跃度 × 个人熟悉度 ÷ 学习成本生命周期短、社区冷清、自己不会用的技术直接淘汰。基于此DoraMate 的选型如下后端编程语言GoWeb 框架标准库 轻量路由数据库PostgreSQL 16缓存Redis 7队列Redis Stream前端移动端跨平台框架解决 Android/iOS 双端问题桌面端Web 套壳方案降低维护成本状态管理轻量级响应式方案部署单台云服务器 Docker Compose免费版对象存储用于文件/导出内容使用现成第三方服务收发推送3.2 为什么后端选 Go PostgreSQL个人项目三要素并发能力、编译时错误捕获、内存占用。Go 在这三项上都达标而且部署产物是静态二进制Docker 镜像能压到 20MB 以内——这对小内存服务器非常友好。PostgreSQL 的选择没什么悬念。JSONB 字段允许在早期不预设复杂表结构加上 PostGIS、全文检索这类扩展能力后面要加任何高级功能都不用换库。补充为什么不用 NoSQLDoraMate 的核心数据——用户、任务、心情记录——天生就是关系型的。NoSQL 的弹性优势在个人项目里根本用不上反而会在多表关联查询时让你头疼。只有 AI 对话产生的历史消息可能用文档型存储但也完全可以用 Postgres 的 JSONB 字段替代。3.3 前端跨平台框架为什么选稳定的那个跨平台框架我选的是生态最稳、人才储备最多、踩坑资料最全的那个不选最新预览版。原因很直接个人项目的繁忙期恰好是框架踩坑最难受的时期。新框架 0.x 版本每个版本都可能破坏既有 API同一个问题在 GitHub issues 里挂几个月没人答。与其赌未来不如用已经稳定多年的方案确保核心功能尽快闭环。桌面端用了 Web 套壳本质就是一个浏览器窗口加载本地 SPA。因为 DoraMate 桌面端的核心场景是“快速看今日安排”和“快捷记录”不需要复杂的系统对接Web 壳完全够用。3.4 基础设施与服务依赖基础设施层DoraMate 使用 Docker Compose 在单机上编排所有组件。为什么不用云原生那一套因为 K8s 不会让你的个人项目幸福感变高只会让你从写业务变成写运维脚本。存储方面对象存储存放用户导出的数据文件、后续可能的语音记录附件。CDN暂不需要图片等静态资源直接由对象存储托管。第三方服务的使用原则是推送、登录、对象存储这类非核心能力的通用服务能用第三方的直接用。自己从头做推送通道既费时又难以处理厂商限制得不偿失。4. 核心数据模型与接口契约设计架构定了模块划了下一步就是数据模型和接口设计。这是 DoraMate 整个设计阶段最容易被低估的环节。4.1 领域模型设计一张表看懂核心实体DoraMate 的核心实体可以浓缩为一张关系图user (用户) └── task (任务) └── mood_record (心情记录) └── device (设备) task ──→ reminder (提醒) task ──→ habit_checkin (打卡记录) conversation (对话) └── message (会话消息)每个实体的核心字段设计如下表实体关键字段说明userid, nickname, timezone, settingstimezone 影响提醒调度老的字段可预留taskid, user_id, title, due_at, repeat_rule, statusrepeat_rule 用 JSONB 存规则如每周一三五mood_recordid, user_id, date, mood_score, energy_score, note日期维度记录天然适合日报聚合reminderid, task_id, remind_at, method, statemethod 区分推送/站内/邮件messageid, conversation_id, role, content, metameta 存 token 数、耗时的查找等不再单独建表conversationid, user_id, type, started_attype 区分闲聊/任务指令/情感陪伴这里细节比较多我只重点说两个容易被忽略的点timezone 必须放用户维度。一个用户在东京和在上海提醒推送的绝对时间完全不同。如果不存用户时区、只按服务器时间算后续换服务区或扩展到海外用户时会非常痛苦。这个字段是在设计阶段就纳入的不是上线后补丁。repeat_rule 用 JSONB 而不是普通序列化字符串。字符串虽然也能存但查询时要做内存解析JSONB 在 PostgreSQL 里可以直接做条件查询未来还会出现“每天早晨 8 点提醒”这类需求扫描JSONB 就派上了用场。4.2 接口风格与统一响应约定API 风格上 DoraMate 选择了RESTful 风格 统一响应结构这是最实用、最容易对接多端的风格。里面包含了几个考量的细节统一响应格式设计为{ code: 0, message: ok, data: {} }这里code为 0 表示成功非 0 表示业务错误码。HTTP 状态码仍然遵循语义如 200 成功、400 参数错误、401 未认证、404 不存在。为什么要搞两层错误表达原因是HTTP 状态码不能覆盖业务语义比如“任务已过期但仍在创建提醒”这种业务错误如果只靠 400 表达前端无法精细判断错误类型有了错误码前端可精确跳转到对应提示。接口执行顺序统一为Controller 层校验权限 → Service 层校验业务规则 → Repository 层执行数据操作 → 统一封装返回结果。这种顺序能在早期就避免大量“调了接口却不知道错在哪里”的排查成本。4.3 数据一致性策略DoraMate 不需要强分布式事务但要保证两个场景的一致性场景一创建任务与设置提醒的原子性。简单做法是使用本地数据库事务同时写task表和reminder表失败则回滚。这个场景在单体应用里用一个事务就解决了不用引入分布式事务框架。场景二提醒状态与任务状态的同步。用户点击“完成任务”后任务状态要改待办的提醒要标记为“已失效”。这就导致task和reminder两个模块之间出现了一次跨模块更新。方案是task模块负责自己的状态变更然后通过模块内事件或直接调用reminder模块的公开接口将关联提醒失效。跨模块的数据交换只能走接口禁止直接改对方的表。提示任何时候跨模块的不一致问题优先通过“先写主数据、再发补偿操作”的方式解决。保证关键数据的最终一致性不要因为一次失败把整个请求变成死锁。5. 实操过程从需求发散到架构定稿的三次迭代说实话上面的架构不是一口气画出来的。我经历了三轮反复调整每一轮都有真实的决策教训。5.1 第一轮需求发散架构过度设计第一版架构是从功能列表“推导”出来的有用户系统、有任务系统、有 AI 对话、有报表……于是画出了一个微服务 消息队列 独立 BFF(Backend for Frontend) 的复杂架构。当时觉得很有把握结果在评审时被自己问到卡壳“BFF 到底在这里解决了什么实际问题”答案是没有。项目只有两三个客户端接口数量总共不超过 30 个BFF 纯粹是画蛇添足。微服务更是无必要 —— 进程内调用的性能优势和低运维成本完全被忽略了。这一轮的最大教训是架构设计必须回到真实业务场景去验证而不是在架构真空中追求“完备”。5.2 第二轮按“业务变更频率”重新切模块第二轮开始我没有再从头想系统架构而是拉出了 DoraMate 未来半年的功能清单标记了每个功能的预期变更频率。结果精确指向“模块化单体”。notify模块独立出来是因为推送通道依赖第三方 SDK其版本迭代频率远高于普通业务代码assistant模块独立出来是因为后续要接入大模型接口不稳定不能和稳定模块耦合太深。第二个成果是放弃了自定义 RPC 传输协议所有跨模块交互统一走进程内函数调用 接口约束。等到哪天真的要把某个模块拆成独立服务再在接口层加一层 HTTP 适配即可。架构的可演进性不是靠一张复杂的拓扑图而是靠在边界设计上留有余地。5.3 第三轮工程规范落地与代码目录结构架构定了还需要把它变成每个开发成员都能自动遵循的代码规范。DoraMate 采用了一目了然的目录结构server/ ├── cmd/ │ └── api/main.go (启动入口) ├── internal/ │ ├── user/ │ │ ├── controller/ │ │ ├── service/ │ │ ├── repository/ │ │ └── model/ │ ├── task/ │ ├── mood/ │ ├── notify/ │ ├── assistant/ │ └── report/ ├── pkg/ │ ├── db/ │ ├── cache/ │ └── config/ └── go.mod关键点在internal/的自包含性。每个模块的 model 不对外暴露其他模块如果需要读数据必须通过该模块的 service 接口。这是模块化单体最重要的纪律。作为约束手段我们设了一条 CI 规则禁止internal下不同模块间出现跨目录的 import 操作。为此专门写了一个简单的静态检查脚本在 CI 阶段跑发现就报错。有了这条红线模块边界就不会在忙碌中被悄悄突破。6. 常见架构设计问题与排查经验最后整理一份避坑清单。这些不是书本上的知识点是我实打实踩过的。问题征兆应对方案模块依赖方向混乱多个模块直接 import 同一个数据表CI 脚本强制检查跨模块 import过度设计接口每个模块都预先定义一堆“可能用到的”接口只定义有明确调用方的接口用错存储把 Redis 当主存储数据丢失才意识到不能持久化明确 Redis 只做缓存/队列/临时状态忽略时区用户提醒时间在跨地区场景出现偏差用户表增加 timezone 字段第三方服务耦合推送 SDK 换了新版本导致整个模块编译失败用接口包裹第三方 SDK适配层隔离再补充两个更“隐性”的坑接口设计不要在一开始就求大求全。比如创建任务接口第一版只需要title和due_at两个必填字段值不值得为“循环规则”“优先级”“标签”预留可选字段我的建议是不需要。YAGNI(你不会需要它)原则比未雨绸缪更实用——字段后面可以在兼容前提下增量添加。数据导出功能必须早期做。这个纯粹是经验教训。很多产品上线几个月了用户还没法备份自己的数据这是隐私层面的重大缺失。DoraMate 在 M2 就规划了数据导出虽然业务代码量不大但涉及设计“导出期间的新数据如何处理”这类细节早期做可以避免后期的结构性返工。如果你正在做个人项目或小团队产品我特别建议你把本文提到的“非目标列表”和“模块边界红线”纳入自己的流程。它们看起来是文档实际上决定了项目未来大半年的推进节奏等代码量上来再回头调架构成本会成倍放大。后面我会继续更新 DoraMate 系列把 auth、提醒调度、AI 接入等具体模块的落地细节一点点放出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑