一直想做一个真正贴合小众人群需求的项目,而不是又一个泛泛的社交应用。认识几个做皮具、陶瓷和木工的朋友之后,我发现他们有个共同的烦恼:作品做出来了,过程却永远停留在手机相册的某个角落,材料买过什么、还剩多少,全靠脑子记。于是就有了artcraft,一个面向独立手作人与小型工作室的作品管理与展示平台。这是个 Web 应用,核心就干三件事:帮手作人整理作品集、记录创作过程、管理材料库存。项目选用 Vue 3、Node.js 和 PostgreSQL 这套组合,从立项到上线花了两周多。这篇文会把完整的设计思路、技术选型、关键实现和踩坑记录都摊开来讲,如果你正准备做类似的创作工具类项目,可以参考,也可以直接拿来抄作业。
1. 从灵感到立项:artcraft要解决什么问题
1.1 灵感来源:一次手作朋友的吐槽
有一次周末,一个做手工皮具的朋友拿手机给我翻他做过的作品,翻了大概五分钟还没找全。他说自己一年做几十件东西,每件都拍了不少照片,平时发朋友圈用掉一部分,剩下的全都堆在相册里,想回看某件作品的制作过程,得反复翻聊天记录、翻朋友圈、翻网盘,非常痛苦。
材料那边更乱。植鞣革、五金件、边油、染料,零零散散买了一大堆,有的只用过一次就再没动过,有的用到一半才发现已经空了。每次开工前都要先把柜子翻一遍,确认材料够不够,有时候忘了检查,做到一半缺料,活儿就卡住了。
这听起来是小事,但对创作型工作来说,思路一旦被打断,再捡起来就很难。我当时就想,能不能做一个工具,让这些手作人把作品和材料管起来?不需要很复杂,但要贴合手作人的使用习惯。
后来我在某个手作爱好者群聊里随口问了一句,结果响应的人比预想多很多。有人想要一个能展示多图作品集的页面,有人想记录每个步骤的图文过程,有人想要简单的材料入库和用量统计,还有人希望能生成一个作品页,方便发到社交平台上。需求非常分散,但核心是稳定的:作品、过程、材料。
1.2 目标用户与核心需求提炼
把零散反馈梳理之后,我锁定了三类典型用户:
- 独立手作人:全职做手工定制生意,需要定期展示作品,需要按作品归档过程资料。
- 小型手作工作室:两三个人一起干活,需要一个共享的记录和管理工具。
- 手作爱好者:不靠这个赚钱,但作品量不小,也想留个整洁的记录。
这三类人有个共同点:不是技术用户,甚至对“系统”“数据库”这类词很排斥。所以产品形态必须极度简单,打开就是“我的作品”“我的材料”,所有操作都在两个页面里完成。
我给自己定了几条功能取舍原则:
- 不做社交。 不搞关注、点赞、评论,手作人没精力运营,也没必要。
- 不做电商。 售卖走私域更直接,平台里加购物车反而多余。
- 不做会员体系。 不搞VIP、积分,保持工具属性。
- 专注三件事: 作品集、过程记录、材料库存。
这些原则让项目边界非常清楚,对独立开发者来说,能少做一半功能,就能省一半的维护成本。
1.3 为什么叫artcraft
名字是 art 和 craft 的合成词,想表达的就是“艺术化的手作工艺”。手作不是简单复制生产,每件作品都有自己的思考和痕迹,我希望这个工具的界面和体验能配得上“手作”这件事的质感。同时这个名字也顺带确定了整体视觉基调:偏文艺、偏留白,不要大红大紫,不要花哨动效。
2. 技术方案与架构设计:为什么这么选
2.1 技术栈选择的考量
早期原型我用过纯前端方案,数据存在浏览器 LocalStorage 里,做了两天就推翻了。问题是手作人通常有多个设备,手机拍完照要在电脑上整理,LocalStorage 完全没法跨设备。后来决定做前后端分离的全栈应用,技术选型如下表:
| 层面 | 选型 | 理由 |
|---|---|---|
| 前端框架 | Vue 3 + Vite | 上手平缓、中文文档好、组合式 API 适合中小型项目快速迭代 |
| 状态管理 | Pinia | Vue 3 官方推荐,比 Vuex 类型体验好,模板代码更少 |
| UI 组件 | Naive UI | 组件风格清爽,适合工具型应用,不重 |
| 后端框架 | Node.js + Express | 和前端同语言,减少上下文切换,生态成熟 |
| 数据库 | PostgreSQL 15 | 支持事务、行级锁,材料扣减需要强一致性 |
| ORM | Prisma | Schema 清晰、迁移方便、类型安全 |
| 部署 | Docker + Nginx | 环境统一,迁移容易 |
不用 Python(后端原本考虑过 FastAPI)的原因是团队里对 JavaScript 全栈更熟。坦率讲,FastAPI 在接口开发上效率不低,但一个人做全栈项目,能减少语言切换才是最重要的。前端写完接口直接就能接,后端用到类型定义还能共享一部分。
数据库选 PostgreSQL 而不是 MySQL,核心原因是材料库存扣减涉及并发写。PostgreSQL 的行级锁和事务隔离机制比 MySQL 默认配置更省心,虽然 MySQL 也能做,但我不太想花时间去处理那些细枝末节的配置差异。SQLite 只在开发阶段用来快速跑通原型。
2.2 整体架构与数据流设计
架构上走了经典的三层:浏览器(Vue)→ 接口层(Express)→ 数据层(PostgreSQL)。图片文件不直接入库,上传后存在服务器的某个目录里,数据库只存路径和元信息。
整体请求流程是这样:
- 前端登录后拿到 JWT,存到内存和 LocalStorage。
- 每次请求带上 Token,后端中间件校验。
- 所有业务接口遵循 REST 风格,资源名用复数,比如
/api/works/api/materials。 - 图片上传走单独的上传接口,返回文件的相对路径,前端再把路径和其他字段一起提交。
数据模型上,核心关系是用户拥有多件作品,一件作品拥有多个步骤记录,一个作品可以关联多种材料,材料与作品是多对多关系,关联表里带用量字段。这个关系决定了下单扣减库存的基本逻辑,后面讲到材料模块时会细说。
2.3 目录结构与项目初始化
我习惯用一个仓库管理前后端,目录结构如下:
artcraft/ ├── client/ # Vue 3 前端 │ ├── src/ │ │ ├── api/ # 接口封装 │ │ ├── components/# 通用组件 │ │ ├── stores/ # Pinia 状态 │ │ ├── views/ # 页面 │ │ └── utils/ # 工具函数 ├── server/ # Express 后端 │ ├── src/ │ │ ├── routes/ # 路由 │ │ ├── controllers/ # 控制器 │ │ ├── services/ # 业务逻辑 │ │ ├── middlewares/ # 中间件 │ │ └── prisma/ # 数据模型与迁移 │ └── uploads/ # 图片存储目录 ├── docker-compose.yml └── README.md前后端放同一个仓库最大好处是改接口和改页面不用在两个仓库之间来回切换,反正项目不涉及多人协作。等以后规模上去了,再把前后端拆成两个仓库也不迟。
3. 三个核心功能模块的实现细节
3.1 作品集模块:展示是门面,但后台才是重点
作品集模块外表上看就是一个瀑布流页面,点进详情能看到一组图片和一段文字描述。真正花力气的地方在上传环节。
手作人拍照通常用手机,一张图动不动好几 MB,直接传服务器很亏:带宽费不说,页面加载也慢。所以我在前端接入了图片压缩逻辑,选完图片后在浏览器里用 Canvas 把图片压缩到最长边不超过 1600 像素,质量控制在 0.8,一般 3MB 的图能压到 300KB 左右,肉眼基本看不出差别。
压完之后再把图片传到一个独立的上传接口,服务端会校验文件类型和大小,然后按日期分目录存盘。文件名我用时间戳加随机字符串生成,避免中文名乱码和重名覆盖。
作品的数据表结构大概如下:
CREATE TABLE works ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), title VARCHAR(120) NOT NULL, description TEXT, cover_image VARCHAR(255), status VARCHAR(20) DEFAULT 'draft', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE work_images ( id SERIAL PRIMARY KEY, work_id INTEGER NOT NULL REFERENCES works(id), image_url VARCHAR(255) NOT NULL, sort_order INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() );作品表只存基本信息,图片单独建表,一张作品可以对应多张图。封面图我允许手作人自己指定,如果不指定,就用第一张上传的图。
这里的细节坑在于,编辑已有作品时,用户可能会删除某张图再重新上传。如果直接删掉数据库记录,文件还留在服务器上,时间一长磁盘就满了。我写了一个定时任务,每晚扫描 uploads 目录里超过 24 小时未被引用的文件并清理掉,简单粗暴但有效。
3.2 创作过程记录模块:用时间线保存手艺的诞生
过程记录是我自己最喜欢的一块。手作圈有个词叫“过程感”,一件手工制品最打动人的地方往往不是最终成品,而是那些带着手温的过程片段:皮具的边缘在打磨棒下慢慢圆润,陶瓷坯体在利坯机上一点点变薄,木料从粗糙到光滑的纹理变化。
这块功能我做成了一种轻量时间线。用户可以在作品详情页里添加步骤节点,每个节点包含一个标题、一段文字、若干张图片。所有节点按时间顺序排列,自动组成一条创作时间线。
步骤表的简化结构:
CREATE TABLE work_steps ( id SERIAL PRIMARY KEY, work_id INTEGER NOT NULL REFERENCES works(id) ON DELETE CASCADE, title VARCHAR(200), content TEXT, sort_order INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE step_images ( id SERIAL PRIMARY KEY, step_id INTEGER NOT NULL REFERENCES work_steps(id) ON DELETE CASCADE, image_url VARCHAR(255) NOT NULL, sort_order INTEGER DEFAULT 0 );为了让记录更顺手,我做了几个小优化:
- 支持给步骤添加标签,比如“打磨”“缝线”“上色”,之后可以按标签筛选步骤。
- 支持拖动排序,调整步骤顺序是拖动而不是改用上下按钮。
- 正文编辑器用了一个轻量的 Markdown 组件,不追求全能,够用就行。
很多手作人习惯写日记式的笔记,不一定按步骤来。所以我没把步骤设计成强制的“1、2、3”,而是允许用户自由添加,哪怕顺序是乱的,只要时间线展示整体美观就行。
3.3 材料库存模块:别让“没材料了”打断创作
材料管理是需求里最不起眼、但实际使用频率最高的一块。手作人对材料数量的敏感度远超我的预期,毕竟缺料意味着停工,停工意味着收入损失。
材料模块的数据模型:
CREATE TABLE materials ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), name VARCHAR(120) NOT NULL, unit VARCHAR(20) NOT NULL DEFAULT '件', total_quantity DECIMAL(10,2) NOT NULL DEFAULT 0, used_quantity DECIMAL(10,2) NOT NULL DEFAULT 0, min_threshold DECIMAL(10,2) NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() );我用了 total 和 used 两个字段,剩余量 = total - used,而不是直接记一个库存量。这样做的原因是,手作人希望看到“我买了多少”“用了多少”“还剩多少”三个信息,而不是只有一个剩余数。
作品关联材料时,会写入一条用量记录:
CREATE TABLE work_materials ( id SERIAL PRIMARY KEY, work_id INTEGER NOT NULL REFERENCES works(id) ON DELETE CASCADE, material_id INTEGER NOT NULL REFERENCES materials(id), quantity DECIMAL(10,2) NOT NULL, note VARCHAR(255) );当用户在某个作品下添加材料用量时,事务会同时更新 materials 表的 used_quantity,这样每个作品的用量都能溯源,哪些材料用在哪件作品上,一查便知。
还在材料列表页做了低库存提醒。当剩余量低于设置的阈值(比如某款皮革还剩不到半尺,某盒边油还剩不到 1/3),列表里就会出现一个黄色徽标。这个功能不大,但实测下来是最先被用户频繁提到的,因为它直接避免了“开工做到一半发现断料”的尴尬。
4. 从零搭建的关键实操记录
4.1 项目脚手架与系统初始化
我习惯用 Vite 创建前端工程,顺手就把 TypeScript 、ESLint 和 Prettier 都配置好。虽然一开始多花了一点时间,但后面写业务代码省了很多心,尤其是 TypeScript 的接口类型定义,前后端联调时根本不需要反复翻接口文档。
后端这边我用 Express 框架,相关依赖包括:
@prisma/client:数据库访问jsonwebtoken:JWT 生成与校验multer:文件上传处理bcryptjs:密码加密helmet:基础安全头cors:跨域处理
初始化的时候还要做一件事:创建.env文件,把数据库连接串、JWT 密钥、端口这些敏感配置放进去。密钥我用openssl rand -hex 32生成,别用固定字符串,这一点特别重要,之前见过不少项目因为 JWT 密钥写死在代码里,被人反推之后伪造管理员令牌。
4.2 数据库设计与迁移
Prisma 的 schema 文件是整个数据层的单一事实来源。早期我犯过一个错,喜欢先在 Navicat 里手动建表再反向生成,结果改字段经常对不上。后来改成先写 schema,再用prisma migrate生成迁移脚本,才彻底理顺。
这里列出三个主要模型的实际写法:
model Work { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id]) title String @db.VarChar(120) description String? cover String? status String @default("draft") images WorkImage[] steps WorkStep[] materials WorkMaterial[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Material { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id]) name String @db.VarChar(120) unit String @db.VarChar(20) totalQty Decimal @db.Decimal(10, 2) usedQty Decimal @default(0) @db.Decimal(10, 2) minThreshold Decimal @default(0) @db.Decimal(10, 2) workMaterials WorkMaterial[] } model WorkMaterial { id Int @id @default(autoincrement()) workId Int materialId Int quantity Decimal @db.Decimal(10, 2) note String? work Work @relation(fields: [workId], references: [id], onDelete: Cascade) material Material @relation(fields: [materialId], references: [id]) }注意 WorkMaterial 表里我加了onDelete: Cascade,这样删掉一件作品时,对应的用量记录会一并删除,不用手动清理,避免脏数据。
Decimal 类型在 Prisma 里映射到 PostgreSQL 的 numeric,我之前用 Float 存数量,结果前端传入像 0.1 这样的数时出现精度问题,材料快用完时显示成 9.999999999999999,非常尴尬。换成 Decimal 之后彻底解决。
4.3 核心接口实现:材料扣减的事务处理
材料扣减是库存模块最关键的接口。场景是:用户新增一件作品,然后在作品里添加某个材料的使用量,此时后端要更新材料表的 usedQty。如果同时有两个请求都在扣同一个材料,处理不当就会出现“库存越扣越多”或“并发扣减覆盖”的情况。
我用了 PostgreSQL 的事务加行级锁:
// server/src/services/materialService.js async function deductMaterial(userId, materialId, quantity, workId) { return prisma.$transaction(async (tx) => { // 锁定该材料行,直到事务结束 const material = await tx.$queryRaw` SELECT * FROM materials WHERE id = ${materialId} AND user_id = ${userId} FOR UPDATE `; if (!material || material.length === 0) { throw new Error('材料不存在'); } const remaining = Number(material[0].total_qty) - Number(material[0].used_qty); if (remaining < Number(quantity)) { throw new Error(`库存不足,当前剩余 ${remaining} ${material[0].unit}`); } // 更新已用量 await tx.material.update({ where: { id: materialId }, data: { usedQty: { increment: quantity } }, }); // 写入作品关联记录 await tx.workMaterial.create({ data: { workId, materialId, quantity }, }); return { remaining: remaining - Number(quantity) }; }); }这里最关键的是SELECT ... FOR UPDATE。它会锁定这一行直到事务提交,另一个事务如果也想扣这个材料的库存,就必须等前一个结束。没有这个锁的话,两个请求同时读到剩余量 10,各自扣掉 3,最后两个都写回 7,实际库存就变成 14 了,这种错误对库存系统是致命的。
4.4 图片上传接口与本地存储
图片上传接口用 multer 处理。配置需要注意几点:
- 限制文件大小,超出直接返回错误,前端提前压缩本来也不会超。
- 限制文件类型,只允许 jpg、png、webp,防止有人上传 PHP 或 exe 文件。
- 按日期分目录存储,避免一个目录下文件数量爆炸。
const storage = multer.diskStorage({ destination(req, file, cb) { const dir = `uploads/${dateDir()}`; fs.mkdirSync(dir, { recursive: true }); cb(null, dir); }, filename(req, file, cb) { const ext = path.extname(file.originalname).toLowerCase(); cb(null, `${Date.now()}_${randomString(6)}${ext}`); }, }); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter(req, file, cb) { const allowed = ['.jpg', '.jpeg', '.png', '.webp']; const ext = path.extname(file.originalname).toLowerCase(); if (!allowed.includes(ext)) { return cb(new Error('不支持的图片格式')); } cb(null, true); }, });上传接口返回相对路径,比如/uploads/2025/06/1719801234567_a1b2c3.jpg。前端拿到这个路径后,再和其他表单字段一起提交给作品创建接口。
请注意:生产环境里不建议把图片存在应用实例的本地磁盘,因为实例扩容、迁移都会导致文件丢失。我当前是单机部署所以问题不大,但之后做多实例部署,必须先接入对象存储。
5. 上线前后踩过的坑与排查实录
5.1 大图上传把服务器带宽打满
第一次内测时,我让三个朋友同时上传作品图片,结果服务器直接没有响应。查日志发现,其中一张手机原图有 7.8MB,虽然服务端限制了 5MB,但前端没有做压缩,用户选完图就直接传了。三个人同时传,上行带宽瞬间被打满。
这个问题修了两层:
- 前端: 用户选择图片后,立刻在浏览器端用 Canvas 压缩,最长边压到 1600px,质量 0.8,传上去的图基本不会超过 400KB。
- 服务端: 同时用 multer 限制 5MB 做兜底,万一有没压缩的客户端,也不至于把带宽吃爆。
还有个细节是,前端压缩会受浏览器内存限制,如果用户一次选了十几张 10MB 的照片,逐个压缩时页面可能被系统提醒“页面无响应”。所以我在压缩循环里加了await new Promise(r => setTimeout(r, 50)),给浏览器一点喘息时间,实测下来一次处理 20 张图也没再崩过。
5.2 库存并发扣减出现数据错乱
内测第二阶段,一个工作室朋友反馈说,同一个材料库存,两个人同时在作品里加用量,最后剩余量居然比实际应该剩余的多。这就是典型的竞态条件。第一次提交时我没有加锁,直接先 SELECT 再 UPDATE,两个请求的数据互相覆盖了。
排查时我用两个终端连续并发调用接口,复现了问题。修复方案就是上文提到的SELECT ... FOR UPDATE事务锁。加了之后,连续压测 50 次并发扣减,结果完全一致。
这个问题的教训是:涉及“先查后改”的接口,不能靠人脑保证顺序,必须交给数据库事务。另外接口层最好也做一次参数校验,比如 quantity 必须为正数且不超过某个上限,防止异常输入把库存改成负数。
5.3 移动端上传体验的兼容性问题
手作人大量使用手机拍照上传,所以移动端适配必须做足。我遇到的具体问题包括:
- iOS 的 Safari 对 WebP 支持不好,上传的图片压缩后有时打不开,后来统一转成 JPEG 格式。
- Android 部分机型上传时文件名没有后缀,或者后缀大写,需要后端做兼容处理。
- 微信内置浏览器里,个别版本的 Canvas 在压缩图片时会旋转方向,导致图像横过来。这个问题最隐蔽,参考网上方案用 EXIF 数据里的 Orientation 字段做了方向修正,才算彻底解决。
移动端还有一个体验问题是上传进度反馈。原来点完上传只有一个 loading 提示,后来改成显示每张图的上传状态:等待中、上传中、已完成、失败。直观很多,用户也不会因为不知道进度而反复点按钮。
5.4 首屏加载速度太慢
上线一段时间后,有用户反映页面打开很慢。我用浏览器开发者工具看了一下,首屏加载要 2.8 秒,主要问题是打包后的 JS 文件太大,Vue、Naive UI、Markdown 编辑器全打包进了一个 bundle。
优化措施:
- 路由级代码分割,用动态 import 替换静态 import。
- Naive UI 按需引入,不再全量引入。
- Markdown 编辑器组件只在编辑页面加载,列表页完全不需要。
- 静态资源走 Nginx 的
gzip_static和浏览器缓存。
优化后首屏时间降到 1.1 秒左右,对工具型应用来说完全可以接受。
6. 部署、性能优化与后续规划
6.1 部署方案与上线记录
我用的是一台 2 核 4G 的某云服务器,部署方案是 Docker Compose 一键启动三个容器:
artcraft-client:Nginx 提供前端静态文件,同时反向代理 API 请求。artcraft-server:Express 服务,跑在 3000 端口,不直接暴露。artcraft-db:PostgreSQL 15,数据卷持久化。
docker-compose.yml核心配置:
version: "3.8" services: db: image: postgres:15-alpine restart: always environment: POSTGRES_USER: artcraft POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: artcraft volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U artcraft"] interval: 5s timeout: 5s retries: 5 server: build: ./server restart: always environment: DATABASE_URL: postgresql://artcraft:${DB_PASSWORD}@db:5432/artcraft JWT_SECRET: ${JWT_SECRET} depends_on: db: condition: service_healthy volumes: - uploads_data:/app/uploads ports: - "3000:3000" client: build: ./client restart: always depends_on: - server ports: - "80:80" - "443:443" volumes: - ./nginx/ssl:/etc/nginx/ssl:ro volumes: db_data: uploads_data:HTTPS 证书使用了某个免费的 ACME 方案,配合定时脚本自动续期。Nginx 配置里做了三件事:静态文件缓存、client_max_body_size设置(不能太小,前端虽然压缩了,但偶尔会有大文件)、扩展名和 Content-Type 映射。
上线第一周我没有直接开公网,而是走了内测阶段。给几个朋友发了临时账号,让他们真实使用,收集来的问题比我自己测试一天找出的还多。比如他们反映“作品步骤里希望支持拖拽换图”,这个是我自己根本不会想到的需求。
6.2 性能优化与成本控制
单机部署就意味着要做减法。我梳理过一份“现阶段不需要做的事情”列表:
- 不需要 Redis。 当前没有复杂的缓存需求,PostgreSQL 本身有良好的行级缓存。
- 不需要消息队列。 图片压缩放在前端,后端不上传队列任务。
- 不需要 CDN。 用户量不大,国内访问临时用服务器带宽就行。
如果后续用户量增长,优先做的事情是:
- 把图片迁移到对象存储,并接 CDN。
- 应用层加 Redis 缓存热门作品列表。
- 数据库读写分离,或者直接换更高配置的实例。
成本控制上顺手做了个统计,4G 内存的小服务器,跑三个容器,内存占用峰值大概 2.2G,磁盘目前用了 8% 左右,主要是图片。每月的成本完全可以接受。
6.3 后续功能规划
artcraft 目前只完成了核心闭环,后面可做的方向还有不少,我按优先级排了序:
- 作品集导出: 一键生成一份 PDF 或静态 HTML 作品集,方便手作人发品牌资料。
- 多标签筛选: 让作品步骤按工艺类型筛选,比如只看所有作品的“上色”步骤。
- 材料购入记录: 增加采购清单,记录购买价格、供应商,方便核算单个作品的材料成本。
- 复盘统计: 统计每个月的完成作品数、使用材料数,给手作人一个简单的工作量概览。
- 团队空间: 让小型工作室多人共享材料和作品库,但这个功能复杂度高,暂时不会动。
我个人的实际体会是,这种工具型产品最忌讳的就是功能越加越重,最后变成一个谁都不好用的大杂烩。所以在规划里刻意把多数想法放在“以后再说”,先保证三大核心模块足够稳、足够快。
现在回头看看这两周的开发过程,最大的感受是,真正有价值的功能从来不是代码堆出来的,而是从真实的使用场景里长出来的。那个朋友的一句“我找一张成品图翻了半小时”,比任何用户画像都来得具体。artcraft 还远谈不上完美,但它已经实实在在帮几个手作人省下了每天翻相册和翻柜子的时间。
如果你也在考虑做类似的小众工具项目,我的建议是先找三个真实用户,把他们的工作流程摸透,再动手写第一行代码。工具不怕小,就怕不贴手。等做到每天被人主动打开的话,这个项目的意义就自然成立了。