想直接看成品演示的可以直接划到文章底部
背景
故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个小时。虽然这个时间不多,
但这个时间黑神话悟空足矣让我打完虎先锋
回家我就灵光一闪,是不是可以搞一个AI智能穿搭,只需要上传自己的照片和对应的衣服图片就能实现在线试衣服呢?
说干就干,我就开始构思方案,画原型。
俗话说万事开头难,事实上这个构思到动工就耗费了我一个礼拜,因为一直在构思怎么样的交互场景会让用户使用起来比较丝滑,并且容易上手。
目前实现的功能有:
- ✅ 用户信息展示
- ✅ AI 生成穿搭
- ✅ 风格大厅
待完成:
经过
1. 画产品原型
起初第一个版本的产品原型由于是自己构思没有任何参考,直接上手撸代码的,想到啥就画啥,所以布局非常传统,配色也非常普通(蚂蚁蓝),所以感觉没有太多的时尚气息(个人觉得丑的一逼,不像是互联网的产物)。因为重构掉了,老的现在没有了,我懒就不重新找回来截图了,直接画个当时的样子,大概长成下面这样:
丑的我忍不了,我就去设计师专门用的网站参(chao)考(xi)了一下,找来找去,终于有了下面的最终版原型图
2. 配色选择
大家知道,所有的UI设计,都离不开主题色的选择,比如:淘宝橙、飞猪橙、果粒橙…,目的一方面是为了打造品牌形象,另一方面也是为了提升品牌辨识度,让你看到这个颜色就会想起它
那我必须也得跟上时代的潮流,选了 $#c1a57b$ 这款低调而又不失奢华的色值作为主题色,英雄不问出处,问就是借鉴。
3. 技术选型
我对技术的定义是:技术永远服务于产品,能高效全面帮助我开发出一款应用,并且能保证后续的稳定性和可维护性,啥技术我都行。当然如果这门技术我优先会从我属性的板块去找。
经过各种权衡和比较,最后敲定下来了技术选型方案:
- 前端:taro (为了后续可能会有小程序端做准备)
- 后端:koajs (实际使用的是midway,基于koajs,主要是比较喜欢koa的轻量化架构)
- 数据库:mongodb (别问,问就是简单易上手)
- 代码仓库:gitea
- CI:gitea-runner
- 部署工具:pm2
- 静态文件托管:阿里云OSS
4. 撸代码
这里我只挑一些个人感觉相对需要注意的地方展开讲讲
4.1 图片转存
由于我生成图片的API图片链接会在一天之后失效,所以我需要在调用任务详情的时候,把这个文件转存到我自己的oss服务器,这里我总结出来的思路是:【1. 保存在本地暂存文件夹】-【2. 调用node流式读取接口】-【3. 保存到oss】-【4. 返回替换原来的链接】
具体代码参考如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const tempDir = path.join(tmpdir(), 'temp-upload-files') const link = url.parse(src); const fileName = path.basename(link.pathname) const localPath = path.join(tempDir, `/${fileName}`); let request if (link.protocol === 'https:') { request = https } else { request = http } request.get(src, async (response) => { const fileStream = await fs.createWriteStream(localPath); await response.pipe(fileStream); fileStream.on("error", (error) => { console.error("保存图片出错:", error); reject(error) }); fileStream.on('finish', async res => { console.log('暂存完成,开始上传:', res) let result = await this.ossService.put(`/${params.saveDir || 'tmp'}/${fileName}`, localPath); if (!result) return resolve(result) }); });
|
这里的request因为我不想引入其它的库所以这样写,如果有更好的方案,可以在评论区告知一下。
这里需要注意的一个地方是,上传的这个 localPath 最好是自己做一下处理,我这边没有处理,因为可能两个用户同时上传,他们的文件名称相同的时候,可能会出现覆盖的情况,包括后面的oss保存也是。
4.2 文件流式上传中间件
因为默认的接口处理是不处理流式调用的,所以需要自己创建一个中间件来拦截处理一下,下面给出我的参考代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| class SSE { ctx: Context constructor(ctx: Context) { ctx.status = 200; ctx.set('Content-Type', 'text/event-stream'); ctx.set('Cache-Control', 'no-cache'); ctx.set('Connection', 'keep-alive'); ctx.res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Transfer-Encoding': 'chunked' }); ctx.res.flushHeaders(); this.ctx = ctx; } send(data: any) { if (typeof data === "string") { this.push(data); } else if (data.id) { this.push(`id: ${data.id}\n`); } else if (data.event) { this.push(`event: ${data.event}\n`); } else { const text = JSON.stringify(data) this.push(`data: ${text}\n\n`); } } push(data: any) { this.ctx.res.write(data); this.ctx.res.flushHeaders(); } close() { this.ctx.res.end(); } }
@Middleware() export class StreamMiddleware implements IMiddleware<Context, NextFunction> { resolve() { return async (ctx: Context, next: NextFunction) => {
if (ctx.res.headersSent) { if (!ctx.sse) { console.error('[sse]: response headers already sent, unable to create sse stream'); } return await next(); }
const sse = new SSE(ctx); ctx.sse = sse; await next();
if (!ctx.body) { ctx.body = ctx.sse; } else { ctx.sse.send(ctx.body); ctx.body = sse; } }; }
public match(ctx: Context): boolean { if (ctx.path.indexOf('stream') < 0) return false }
static getName(): string { return 'stream'; } }
|
4.3 mongodb 数据库的权限
这里尽量不要使用root权限的数据库角色,可以创建一个只有当前数据库权限的角色,具体可以网上找相关文档,怎么为某个collection创建账户。
实机演示
1. 提交素材,创建任务
2. 获取生成图片
3. 展示大厅(待完善)
结语
当然现在目前这个还是内测版本,功能还不够健全,还有很多地方需要打磨,包括用户信息页面的展示是否合理,UI的排版,数据库表的设计等等
通过观察生活用现有的技术创造一些价值,对我来说就是一种幸福且有意义的事儿。
如果想要体验的可以后台私信我。如果你也有很棒的想法想交流一下,也可以私我。
我是dev,下期见(太懒了我,更新频率太低)
个人博客
灵感中心