我过去常常花很长时间来决定该穿什么,即使我的衣柜里已经装满了衣服。

这种挫败感让我清楚地意识到问题的根源:问题并不在于拥有的衣服太少,而在于如何更好地整理衣物、更清晰地了解自己的衣橱情况,并在挑选穿搭时获得更有用的建议。

因此,我开发了一款时尚应用程序,帮助用户整理衣柜、获取穿搭建议、评估购物决策,并通过用户的反馈不断优化这些推荐功能。

在这篇文章中,我将详细介绍这款应用的功能、开发过程、我在开发过程中所做的决定,以及那些影响最终成果的挑战。

目录

应用的功能

从整体来看,这款应用具备六项核心功能:

  1. 衣柜管理

  2. 穿搭推荐

  3. 购物建议

  4. 衣物处理建议

  5. 用户反馈与使用情况追踪

  6. 安全的多用户账户系统

用户可以上传自己的衣物信息,查看推荐的穿搭方案,并对这些建议进行评价(表示有用或无用)。他们还可以为这些穿搭打分,同时记录某件衣物是已被穿着、保留还是被丢弃。

这些反馈会转化为结构化数据,用于进一步提升后续推荐的质量。

我为什么要开发这款应用

我想打造一款既实用又让人有亲切感的工具。很多时尚应用程序看起来很精致,但它们并不总能帮助用户解决日常的穿搭问题。我的目标是开发出这样一款应用:它能够简化衣柜管理,让挑选穿搭变得不那么令人困扰。为了实现这一目标,这款应用需要具备以下三个关键功能:

  • 存储每位用户的衣物信息

  • 提供个性化的推荐方案

  • 根据用户反馈不断优化推荐系统

这种反馈机制对我来说非常重要,因为它能让应用程序显得更加生动、更具交互性,而不会显得死板乏味。

技术架构

以下是我在开发这款应用时使用的一些工具:

  • 前端框架:React + Vite

  • 后端框架:FastAPI

  • 数据库:SQLite(用于本地开发)

  • 后台任务处理工具:Celery + Redis

  • 认证系统:JWT(访问令牌与刷新令牌机制)

    部署支持工具:Docker与GitHub Codespaces

这种设计最终让我拥有了一套相当模块化的系统架构,当功能逐渐增多时,这套架构发挥了巨大的作用:它使得前端功能的迭代速度大大加快,API的边界也更加清晰明确,同时还能让推荐系统与用户界面独立发展、共同进化。

产品使用流程介绍(用户实际看到的界面)

1. 注册与账户设置

要开始使用该应用程序,用户需要先进行注册,验证电子邮件地址,并填写一些基本的个人资料信息。

注册页面,包含账户创建、电子邮件验证以及关于体型、身高、体重和风格偏好的个人资料填写选项。

每个账户都是独立的,因此用户的衣橱记录和推荐内容也会专门对应该用户。

在上面的注册页面中,你可以看到账户创建、电子邮件验证以及关于体型、身高、体重和风格偏好的个人资料填写选项。

2. 上传衣物照片

用户可以上传自己的衣物图片。

上传衣物照片的界面,会显示图片分析结果,包括所属类别、主要颜色、次要颜色以及图案细节。

图片分析功能会为每件衣物添加标签,从而便于后续进行推荐系统的相关操作。上传衣物照片的界面会显示图片分析的结果,包括类别、主要颜色、次要颜色以及图案细节等信息。

3. 服装搭配推荐

用户可以请求获得搭配建议,之后还可以对收到的推荐结果进行评分。

在上面这个界面中,你可以看到按排名顺序排列的推荐方案,同时还可以查看用户给出的反馈意见和评分结果。这些推荐结果是根据加权评分模型计算得出的。

4. 购物辅助与衣物处理建议

该应用程序会将新购买的物品与用户现有的衣橱资料进行对比,从而识别出那些可能不太有价值的衣物,提示用户是否需要将其删除。

购物辅助与衣物处理分析界面,会显示每件物品的推荐评分、书面说明以及搭配建议。

在上面这个界面中,你可以看到每件物品的推荐评分、书面说明(而不仅仅是简单的“是否保留”判断),以及搭配建议。如果用户仍然决定保留某件衣物,该界面还会提供具体的穿着建议。

我是如何构建这个应用的

1. 前端开发环境配置(React + Vite)

我选择使用 React + Vite,是因为这样能够实现快速迭代,并保持组件结构的整洁性。

前端功能被划分为几个独立的模块,包括新手引导、衣橱管理、穿搭推荐、购物功能以及废弃物品处理建议等。此外,所有的 API 请求都被集中到服务层中处理,这样用户界面组件就可以专注于渲染和交互逻辑。

下面的代码片段展示了该应用中使用的 API 服务模式简化示例。这个例子并不适合直接复制使用,但它能说明前端在与后端进行通信时所采用的结构。

API 客户端示例代码:

export async function getOutfitRecommendations(userId, params = {}) {
  const query = new URLSearchParams(params).toString();
  const url = `/users/\({userId}/outfits/recommend\){query ? `?${query}` : ""}`;

  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${localStorage.getItem("access_token")}`,
    },
  });

  if (!response.ok) {
    throw new Error("获取穿搭推荐信息失败");
  }

  return response.json();
}

这段代码中发生了以下几件事情:

  • URLSearchParams用于构建包含诸如occasionseasonlimit等参数的查询字符串。

  • 请求路径是用户级别的,这样就能确保每个用户的推荐结果相互独立。

  • Authorization头部用于传递访问令牌,以便后端能够验证用户的身份。

  • 在解析响应之前会先检查其状态码,如果请求失败,前端就可以及时显示相应的错误信息。

这种设计模式使得随着 API 请求次数的增加,前端代码依然保持简洁且易于维护。

2. 基于 FastAPI 的后端架构

后端的逻辑是根据不同的路由组来组织的:

  • 用于处理注册、登录、会话管理等功能的路由
  • 用户数据分析相关的路由
  • 衣橱相关数据的创建、读取、更新和删除操作路由
  • 穿搭推荐、购物建议以及废弃物品分析相关的路由
  • 用于接收用户评价和建议的路由

其中最重要的设计决策之一就是对用户级别的资源实施访问权限控制,这样就能防止某个用户擅自访问其他用户的衣橱数据或反馈信息。

下面的代码片段再次展示了该应用后端路由层的一些简化示例。它展示了请求验证和逻辑协调的过程,而实际的评分计算工作则由推荐服务模块来完成。

@app.get("/users/{user_id}/outfits/recommend")
def recommend_outfits(user_id: int, occasion: str | None = None, season: str | None = None, limit: int = 10):
    user = get_user_or_404(user_id)
    wardrobe_items = get_user_wardrobe(user_id)

    if len(wardrobe_items) < 2:
        raise HTTPException(status_code=400, detail="衣橱物品数量不足")

    recommendations = outfit_generator.generate_outfit_recommendations(
        wardrobe_items=wardrobe_items,
        body_shape=user.body_shape,
        undertone=user.undertone,
        occasion=occasion,
        season=season,
        top_k=limit,
    )

    return {"user_id": user_id, "recommendations": recommendations}

以下是解读该代码的方法:

  • get_user_or_404用于加载进行个性化处理所需的用户资料数据。

  • get_user_wardrobe仅获取当前用户的物品信息。

  • 进行最低限度的数据检查可以确保推荐逻辑不会在数据不完整的情况下运行。

  • generate_outfit_recommendations专门负责评分逻辑的处理,这样可以使相关代码结构更加简洁,也便于测试。

  • 系统返回的结果格式是前端可以直接处理的。

这种分离的设计使得API层更易于理解,同时推荐逻辑也被独立地封装在单独的服务中。

3. 推荐逻辑

在引入复杂的机器学习模型之前,我首先采用了基于确定性规则的推荐机制。这样可以让系统行为更便于调试和解释。

这套推荐系统会利用多种权重因子来计算组合服装的评分:

$$\text{服装评分} = 0.4 \cdot \text{色彩协调性} + 0.4 \cdot \text>身材适配度} + 0.2 \cdot \text>肤色匹配度$$

下面的代码片段展示了推荐系统是如何将多种评估因素综合成最终得分的:

def score_outfit(combo, user_context):
    color_score = color_harmony.score(combo)
    shape_score = body_shape_rules.score(combo, user_context.body_shape)
    undertone_score = undertone_rules.score(combo, user_context.undertone)

    total = 0.4 * color_score + 0.4 * shape_score + 0.2 * undertone_score
    return round(total, 3)

这种评分机制的逻辑非常直观:

  • 色彩协调性有助于提升服装的整体视觉效果。

  • 身材适配度能确保服装更加合身。

  • 肤色匹配度能让颜色更好地与用户的肤质相协调。

对于“不推荐”的建议以及购物提示,我也采用了类似的结构,但会使用不同的评估因素和阈值。

4. 认证机制与安全的多用户设计

安全性是本次开发过程中最重要的考虑因素之一。

我采取了以下措施来保障系统安全:

  • 使用有效期较短的访问令牌。

  • 通过JTI跟踪机制更新令牌。

  • 在令牌更新时进行轮换处理。

  • 能够撤销单个会话或所有会话的权限。

  • 提供邮件验证和密码重置功能。

下面的代码片段展示了应用程序中令牌更新的生命周期流程,重点介绍了关键的控制点,而非所有的辅助函数:

def refresh_access_token(refresh_token: str):
    payload = decode_jwt(refresh_token)
    jti = payload["jti"]

    token_record = db.get_refresh_token(jti)
    if not token_record or token_record.revoked:
        raise AuthError("无效的刷新令牌")

    newrefresh, new_jti = issue_refresh_token(payload["sub"])
    token_record.revoked = True
    token_record.replaced_by_jti = new_jti

    new_access = issue_access_token(payload["sub"])
    return {"access_token": new_access, "refresh_token": new_refresh}

这段代码的作用是:

  • 它会对刷新令牌进行解码,并在数据库中查找与之对应的JTI信息。

  • 它会拒绝使用被重复利用或已被撤销的会话,从而帮助防止重放攻击。

  • 它会定期更换刷新令牌,而不会重复使用同一个令牌。

  • 它会生成新的访问令牌,这样会话就能保持有效状态,而无需用户再次登录。

这种设计使得多设备会话更加安全,同时也让我能够在服务器端控制用户的退出行为。

5. 长时间运行的后台任务

图像分析往往需要耗费大量资源,尤其是当应用程序需要对衣物进行分类、分析颜色或评估与体型相关的信息时。为了保证请求处理的响应速度,我为后台任务添加了对Celery和Redis的支持。

这样一来,我们的应用程序就有了两种运行模式:

  • 对于简单的本地开发环境,采用同步处理方式。

  • 对于那些耗时较长或处理速度较慢的任务,则采用队列处理机制。

  • 这种设计非常重要,因为它既保证了开发者的使用体验,又不会在处理复杂任务时影响应用程序的整体性能。

    6. 数据模型与反馈收集机制

    只有当推荐系统能够准确收集到所需的反馈信息时,它才能真正发挥出作用。

    因此,我为用户提供了专门的反馈表格,用于记录以下内容:

    • 服装搭配的评分(1-5分,可附加评论)

    • 对推荐结果的有用性或无用性的反馈

    • 用户对各项物品的使用情况(是否穿着、是否保留、是否丢弃)

    以下是其中一个数据模型的具体结构:

    class RecommendationFeedback(Base):
        __tablename__ = "recommendation_feedback"
    
        id = Column(Integer, primary_key=True)
        user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
        recommendation_type = Column(String(50), nullable=False)
        recommendation_id = Column Integer, nullable=False)
        helpful = Column(Boolean, nullable=False)
        created_at = Column(DateTime, default.datetime.utcnow)
    

    如何理解这个数据模型:

    • user_id用于将反馈信息与提供反馈的用户关联起来。

    • recommendation_type用来区分反馈是关于服装搭配、购物建议,还是丢弃建议。

    • recommendation_id用于标识具体的推荐内容。

    • helpful用于记录用户对推荐结果的具体评价。

    • created_at有助于分析反馈信息随时间的变化趋势。

    尽管将用户的反馈信息及时更新到数据库中仍属于未来的改进方向,但这一机制确实为我们的应用程序提供了强大的学习能力。

    我遇到的挑战

    这一部分的内容让我学到了很多东西。

    1. 图像处理相关的接口响应速度低于我的预期

    <分析流程以及上传到数据库的步骤需要同时完成多项任务:图像验证、分类、颜色提取、数据存储以及将处理结果写入数据库。>

    起初,这种设计使得请求处理流程的效率明显低于应有的水平。

    我所做的修改包括:

    • 我对同时进行的图像处理任务设置了上限,以防止应用程序一次性尝试处理过多任务。

    • 对于那些处理速度较慢的任务,我会将它们安排在后台进行处理。

    • 通过负载测试的结果,我确定了哪些接口实际上会消耗较多的资源。

    实际效果是:那些耗时较长的图像处理请求不再会互相干扰。我不会让许多耗时的任务在同一请求周期内同时执行,而是会限制正在进行的操作数量,并在需要时将速度较慢的任务放入队列中等待处理。

    为什么这种修改能够解决问题:

    • 对并发任务设置上限可以防止系统因某些任务占用过多CPU资源而陷入瘫痪。

    • 将耗时较多的任务放在异步处理流程中,可以让主要的请求/响应流程保持较高的响应速度。

    • 负载测试为我提供了实际的数据依据,而不是靠猜测来调整系统配置,因此我能够根据系统的真实运行表现来进行优化。

    换句话说,我并不是仅仅在理论上“优化”这些接口,而是彻底改变了系统的执行机制,从而确保那些耗时较多的操作不会影响到其他请求的处理。

    2. JWT会话需要服务器端的有效管理

    简单的JWT配置确实很容易实现,但如果无法及时撤销会话或无法妥善管理多设备用户的信息,这种机制就会变得不够实用。

    我所做的修改包括:

    • 我将刷新令牌存储在数据库中。

    • 我会跟踪这些令牌的相关信息。

    • 当用户刷新会话时,我会更新他们的刷新令牌。

    • 我添加了用于注销单个会话或所有会话的接口。

    关键在于:我们需要从“只要令牌存在,会话就有效”这种思路,转变为“令牌必须存在于数据库中,并且未被撤销或替换”,这样服务器才能有权立即使过期的会话失效。

    为什么这种修改能够解决问题:

    • 通过服务器端对令牌进行管理,就可以实现会话的及时撤销。

    • 定期更新令牌可以有效降低令牌被重复使用的风险。

    • 用户能够清楚地看到自己的会话状态,这使得应用程序显得更加可靠。

      正是这些措施,使得“注销所有会话”以及“多设备管理”功能真正发挥了作用,而不仅仅是一些外观上的设计而已。

      3. 用户数据隔离必须明确到位

      由于这是一个多用户应用程序,所以我必须确保某个账户绝不会无意中看到其他账户的数据。

      我所做的修改包括:

      • 我在针对特定用户的路由中加入了权限检查机制。

      • 所有与用户数据相关的查询都会根据user_id来进行过滤。

      • 我采用了加密存储图像数据的方案,而不是直接暴露原始文件路径。

      在实际开发中,这意味着每一个路由都需要验证这样一个问题:“尝试访问该资源的用户是否拥有这个资源?”如果答案是否定的,请求就会立即被终止。

      为什么这些措施能够解决问题:

      • 通过对数据访问规则进行明确界定,使得相关机制更加清晰明了。

      • 通过用户过滤功能,可以有效防止意外发生的跨账户数据读取行为。

      • 采用加密存储方式,既提升了数据的隐私性,也降低了图像数据被直接泄露的风险。

      正是这些措施的共同作用,才使得不同账户之间的衣橱数据、反馈记录以及图片能够得到正确区分。

      4. Docker让项目更易于共享,但前提是必须先整理好技术架构

      该应用程序包含了前端、后端、Redis、Celery工作进程以及Celery Beat组件,因此首要面临的挑战就是确保整个系统的配置过程具有可重复性,而不是容易出现错误。

      我采取了以下措施:

      • 在Docker Compose中明确了整个技术架构的组成。

      • 详细记录了所需的环境变量信息。

      • 确保开发环境与应用程序实际运行时的配置保持一致。

      这些措施有效消除了许多配置上的不确定性。我不再需要让别人去手动理解前端、后端、Redis以及工作进程之间的配合关系,而是让技术架构本身就能清楚地说明这些问题。

      为什么这些措施能够解决问题:

      • Docker使得贡献者可以以更少的手动操作步骤来开始项目开发。

      • 明确的环境配置规范大大减少了配置错误的发生概率。

      • 使技术架构与应用程序的实际运行方式相匹配,从而提高了代码的可理解性和可测试性。

      这一点非常重要,因为该应用程序包含了许多相互关联的组件,而让项目的启动过程变得可预测,才是使其更易于被他人使用的关键所在。

      我所学到的东西

      这个项目让我学到了几条非常重要的经验:

      • 当各个功能模块能够协同工作时,它们的价值会显著提升。

      • 用户反馈数据是优化推荐系统效果的重要依据之一。

      • 当系统中涉及多个用户时,清晰的数据建模机制至关重要。

      • Docker以及详细的配置指南能够大大降低他人尝试使用该项目的难度。

      我还意识到,一个项目并不一定要规模庞大才能发挥作用。一个专注于解决某一具体问题的小型应用程序,同样可以产生重要的价值。

      接下来我想改进的地方

      我接下来的发展计划如下:

      1. 将用户反馈直接纳入排名系统的更新流程中。

      2. 添加用于分析推荐系统质量变化趋势的可视化工具。

      3. 提升移动端用户体验的一致性。

      4. 采用持久的云存储方案,并配置生产环境所需的数据库参数。

      5. 提供公共演示模式,以便于他人进行评估测试。

      未来的改进方向

      我还有一些想法打算在未来添加到项目中:

      • 开发更先进的推荐系统。

      • 为用户反馈数据提供可视化分析工具。

      • 进一步提升移动端的支持能力。

      • 实现基于持久化云存储的实时部署功能。

      • 继续完善公共演示模式,以便于更便捷地进行测试。

      结论

      这个项目最初只是出于个人的需求而开始的,但最终发展成了一款功能完备的网络应用——它具备身份认证机制、衣物存储功能、推荐系统以及用户反馈收集机制。

      最令我感到欣慰的是,那些实用的软件设计决策,而非仅仅炫目的用户界面,才能真正帮助人们更快地做出日常选择。

      如果你想要深入了解或尝试使用这个项目,请访问代码仓库。你可以亲自测试各项功能并分享你的反馈意见。我特别希望听到你们对于推荐系统质量、用户界面清晰度,以及哪些功能能让这个应用在日常生活中真正发挥作用的意见。

Comments are closed.