如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户

一、前言

  上篇中我们讲述了“把商品卖给用户”中的商品和用户的初步设计。现在把剩余的“卖”这个动作给做了。这里提醒一下,正常情况下,我们的每一步业务设计都需要和领域专家进行沟通,尽可能的符合通用语言的表述。这里的领域专家包括但不限于当前开发团队中对这块业务最了解的开发人员、系统实际的使用人等。

 

二、怎么卖

  如果在没有结合当前上下文的情况下,用通用语言来表述,我们很容易把代码写成下面的这个样子(其中DomainRegistry只是一个简单的工厂,解耦应用层与其他具体实现的依赖,内部也可以使用IOC容器来实现):

 

            var user = DomainRegistry.UserService().GetUser(userId);
            if (user == null)
            {
                return Result.Fail("未找到用户信息");
            }

            var product = DomainRegistry.ProductService().GetProduct(productId);
            if (product == null)
            {
                return Result.Fail("未找到产品信息");
            }

            user.Buy(product, quantity);
            return null;    

  

  初步来看,好像很合理。这里表达出的是“用户购买了商品”这个语义。然后继续往下写,我们会发现购买了之后应该怎么办呢,要把东西放到购物车啊。这里又出现了购物车,我认为购物车是我们销售子域中的一个核心概念,它也是整个用户购买过程中变化最频繁的一个对象。我们来梳理一下,一个最简单的购物车至少包含哪些东西:

  A.一个购物车必须是属于一个用户的。

  B.一个购物车内必然包含购买的商品的相关信息。

  首先我们思考一下如何在我们的购物车中表达出用户的概念,购物车需要知道用户的所有信息吗?答案在大部分场景下应该是否定的,因为在用户挑选商品并加到购物车的这个过程中,整个购物车是不稳定的,那么其实在用户想要进行结算以前,我们只需要知道这个购物车是谁的,仅此而已。那么这里我们已经排除了一种方式是购物车直接持有User的引用。所以说对于购物车来说,在我们排除为性能而进行数据冗余的情况下,我们只需要保持一个用户唯一标识的引用即可。

  购物车明细和商品之间的关系也是一样,每次需要从远程上下中获取到最新的商品信息(如价格等),故也仅需保持一个唯一标识的引用。

  结合上一篇讲的,我们目前已经出现了以下几个对象,见【图1,点击图片查看原图】。

 

                       【图1】

 下面贴上购物车和购物车明细的简单实现。

 

    public class Cart : Infrastructure.DomainCore.Aggregate
    {
        private readonly List<CartItem> _cartItems;

        public Guid CartId { get; private set; }

        public Guid UserId { get; private set; }

        public DateTime LastChangeTime { get; private set; }

        public Cart(Guid cartId, Guid userId, DateTime lastChangeTime)
        {
            if (cartId == default(Guid))
                throw new ArgumentException("cartId 不能为default(Guid)", "cartId");

            if (userId == default(Guid))
                throw new ArgumentException("userId 不能为default(Guid)", "userId");

            if (lastChangeTime == default(DateTime))
                throw new ArgumentException("lastChangeTime 不能为default(DateTime)", "lastChangeTime");

            this.CartId = cartId;
            this.UserId = userId;
            this.LastChangeTime = lastChangeTime;
            this._cartItems = new List<CartItem>();
        }

        public void AddCartItem(CartItem cartItem)
        {
            var existedCartItem = this._cartItems.FirstOrDefault(ent => ent.ProductId == cartItem.ProductId);
            if (existedCartItem == null)
            {
                this._cartItems.Add(cartItem);
            }
            else
            {
                existedCartItem.ModifyQuantity(existedCartItem.Quantity + cartItem.Quantity);
            }
        }
    }

 

   public class CartItem : Infrastructure.DomainCore.Entity
    {
        public Guid ProductId { get; private set; }

        public int Quantity { get; private set; }

        public decimal Price { get; private set; }

        public CartItem(Guid productId, int quantity, decimal price)
        {
            if (productId == default(Guid))
                throw new ArgumentException("productId 不能为default(Guid)", "productId");

            if (quantity <= 0)
                throw new ArgumentException("quantity不能小于等于0", "quantity");

            if (quantity < 0)
                throw new ArgumentException("price不能小于0", "price");

            this.ProductId = productId;
            this.Quantity = quantity;
            this.Price = price;
        }

        public void ModifyQuantity(int quantity)
        {
            this.Quantity = quantity;
        }
    }

 

  回到我们最上面的代码中的“user.Buy(product, quantity);” 的问题。在DDD中主张的是清晰的业务边界,在这里,我们目前的定义导致的结果是User与Cart产生了强依赖,让User内部需要知道过多的Cart的细节,而这些是User不应该知道的。这里还有一个问题是在领域对象内部去访问仓储(或者调用远程上下文的接口)来获取数据并不是一种提倡的方式,他会导致事务管理的混乱。当然有人会说,把Cart作为一个参数传进来,这看上去是个好主意,解决了在领域对象内部访问仓储的问题,然而看一下接口的定义,用户购买商品和购物车?还是用户购买商品并且放入到购物车?这样来看这个方法做的事情似乎过多了,违背了单一职责原则。

  其实在大部分语义中使用“用户”作为一个主体对象,看上去也都还挺合理的,然而细细的去思考当前上下文(系统)的核心价值,会发现“用户”有时并不是核心,当然比如是一个CRM系统的话核心即是“用户”。

  总结一下这种方式的缺点:

  A.领域对象之间的耦合过高,项目中的对象容易形成蜘蛛网结构的引用关系。

  B.需要在领域对象内部调用仓储,不利于最小化事务管理。

  C.无法清晰的表达出通用语言的概念。

  重新思考这个方法。“购买”这个概念更合理的描述是在销售过程中所发生的一个操作过程。在我们电商行业下,可以表述为“用户购买了商品”和“商品被加入购物车”。这时候需要领域服务出场了,由它来表达出“用户购买商品”这个概念最为合适不过了。其实就是把应用层的代码搬过来了,以下是对应的代码: 

 

    public class UserBuyProductDomainService
    {
        public CartItem UserBuyProduct(Guid userId, Guid productId, int quantity)
        {
            var user = DomainRegistry.UserService().GetUser(userId);
            if (user == null)
            {
                throw new ApplicationException("未能获取用户信息!");
            }

            var product = DomainRegistry.ProductService().GetProduct(productId);
            if (product == null)
            {
                throw new ApplicationException("未能获取产品信息!");
            }

            return new CartItem(productId, quantity, product.SalePrice);
        }
    }

三、领域服务的使用

  领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。

1.列举几个领域服务适用场景

    A.执行一个显著的业务操作过程。

    B.对领域对象进行转换。

    C.以多个领域对象作为输入进行计算,结果产生一个值对象。

  D.隐藏技术细节,如持久化与缓存之间的依存关系。

2.不要把领域服务作为“银弹”。过多的非必要的领域服务会使项目从面向对象变成面向过程,导致贫血模型的产生。

3.可以不给领域服务创建接口,如果需要创建则需要放到相关聚合、实体、值对象的同一个包(文件夹)中。服务的实现可以不仅限于存在单个项目中。

 

四、回到现实

  按照这样设计之后我们的应用层代码变为:

 

1             var cartItem = _userBuyProductDomainService.UserBuyProduct(userId, productId, quantity);
2             var cart = DomainRegistry.CartRepository().GetOfUserId(userId);
3             if (cart == null)
4             {
5                 cart = new Cart(DomainRegistry.CartRepository().NextIdentity(), userId, DateTime.Now);
6             }
7             cart.AddCartItem(cartItem);
8             DomainRegistry.CartRepository().Save(cart);    

 

  这里的第5行用到了一个仓储(资源库)CartRepository,仓储算是DDD中比较好理解的概念。在DDD中仓储的基本思想是用面向集合的方式来体现,也就是相当于你在和一个List做操作,所以切记不能把任何的业务信息泄露到仓储层去,它仅用于数据的存储。仓储的普遍使用方式如下:

  A.包含保存、删除、指定条件的查询(当然在大型项目中可以考虑采用CQSR来做,把查询和数据操作分离)。

  B.只为聚合创建资源库

  C.通常资源库与聚合式 1对1的关系,然而有时,当2个或者多个聚合位于同一个对象层级中时,它们可以共享同一个资源库。 

  D.资源库的接口定义和聚合放在相同的模块中,实现类放在另外的包中(为了隐藏对象存储的细节)。

  回到代码中来,标红的那部分也可以用一个领域服务来实现,隐藏“如果一个用户没有购物车的情况下新建一个购物车”的业务细节。

 

    public class GetUserCartDomainService
    {
        public Cart GetUserCart(Guid userId)
        {
            var cart = DomainRegistry.CartRepository().GetOfUserId(userId);
            if (cart == null)
            {
                cart = new Cart(DomainRegistry.CartRepository().NextIdentity(), userId, DateTime.Now);
                DomainRegistry.CartRepository().Save(cart);
            }

            return cart;
        }
    }

  这样应用层就真正变成了一个讲故事的人,清晰的表达出了“用户购买商品的整个过程”,把商品购物车的商品转换成购物车明细 --> 获取用户的购物车 --> 添加购物车明细到购物车中 --> 保存购物车。 

        public Result Buy(Guid userId, Guid productId, int quantity)
        {
            var cartItem = _userBuyProductDomainService.UserBuyProduct(userId, productId, quantity);
            var cart = _getUserCartDomainService.GetUserCart(userId);
            cart.AddCartItem(cartItem);
            DomainRegistry.CartRepository().Save(cart);
            return Result.Success();
        }

 

五、结语

  这是最简单的购买流程,后续我们会慢慢充实整个购买的业务,包括会员价、促销等等。我还是保持每一篇内容的简短,这样可以最大限度地保证不被其他日常琐事影响每周的更新计划。希望大家谅解:)

 

 

 

本文的源码地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo4

 


 

作者:Zachary_Fan
出处:http://www.cnblogs.com/Zachary-Fan/p/6041374.html

 

 

▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

 

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

内容概要:本文介绍了ENVI Deep Learning V1.0的操作教程,重点讲解了如何利用ENVI软件进行深度学习模型的训练与应用,以实现遥感图像中特定目标(如集装箱)的自动提取。教程涵盖了从数据准备、标签图像创建、模型初始化与训练,到执行分类及结果优化的完整流程,并介绍了精度评价与通过ENVI Modeler实现一键化建模的方法。系统基于TensorFlow框架,采用ENVINet5(U-Net变体)架构,支持通过点、线、面ROI或分类图生成标签数据,适用于多/高光谱影像的单一类别特征提取。; 适合人群:具备遥感图像处理基础,熟悉ENVI软件操作,从事地理信息、测绘、环境监测等相关领域的技术人员或研究人员,尤其是希望将深度学习技术应用于遥感目标识别的初学者与实践者。; 使用场景及目标:①在遥感影像中自动识别和提取特定地物目标(如车辆、建筑、道路、集装箱等);②掌握ENVI环境下深度学习模型的训练流程与关键参数设置(如Patch Size、Epochs、Class Weight等);③通过模型调优与结果反馈提升分类精度,实现高效自动化信息提取。; 阅读建议:建议结合实际遥感项目边学边练,重点关注标签数据制作、模型参数配置与结果后处理环节,充分利用ENVI Modeler进行自动化建模与参数优化,同时注意软硬件环境(特别是NVIDIA GPU)的配置要求以保障训练效率。
普通文本型 根据你的电商项目结构(Spring Boot + Thymeleaf + HTML),你希望使用 **纯 HTML 文件**(通过 Thymeleaf 渲染)替代 JSP。以下是基于你提供的实体类和功能需求,为“简单电商平台”编写的 **完整前端 HTML 页面代码**,全部放在 `src/main/resources/templates/` 目录下。 > ✅ 使用技术:Thymeleaf + HTML + 内联 CSS/JS(可后续拆分) > ✅ 支持:登录、注册、商品列表、购物车、订单、评论 > ✅ 不使用 JSP,完全静态 HTML 模板 --- ### ✅ 1. `login.html` —— 用户登录页 ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>用户登录</title> <style> body { font-family: Arial, sans-serif; text-align: center; margin-top: 100px; } .form { display: inline-block; padding: 20px; border: 1px solid #ccc; border-radius: 8px; } input { margin: 10px 0; padding: 8px; width: 200px; } button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; } </style> </head> <body> <h2>用户登录</h2> <div class="form"> <form th:action="@{/login}" method="post"> <input type="text" name="username" placeholder="用户名" required/> <input type="password" name="password" placeholder="密码" required/> <button type="submit">登录</button> </form> <p th:if="${error}" style="color:red;" th:text="${error}"></p> <p><a href="/register">没有账号?去注册</a></p> </div> </body> </html> ``` --- ### ✅ 2. `register.html` —— 用户注册页 ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>用户注册</title> <style> body { font-family: Arial, sans-serif; text-align: center; margin-top: 100px; } .form { display: inline-block; padding: 20px; border: 1px solid #ccc; border-radius: 8px; } input { margin: 10px 0; padding: 8px; width: 200px; } button { padding: 10px 20px; background: #28a745; color: white; border: none; cursor: pointer; } </style> </head> <body> <h2>用户注册</h2> <div class="form"> <form th:action="@{/register}" method="post"> <input type="text" name="username" placeholder="用户名" required/> <input type="password" name="password" placeholder="密码" required/> <input type="email" name="email" placeholder="邮箱" required/> <input type="text" name="phone" placeholder="电话"/> <select name="role"> <option value="BUYER">买家</option> <option value="SELLER">卖家</option> </select> <button type="submit">注册</button> </form> <p><a href="/login">已有账号?去登录</a></p> </div> </body> </html> ``` --- ### ✅ 3. `product_list.html` —— 商品列表页 ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>商品列表</title> <style> body { font-family: Arial, sans-serif; padding: 20px; } .product { border: 1px solid #ddd; margin: 10px; padding: 10px; display: inline-block; width: 200px; } .product img { width: 100%; height: 150px; object-fit: cover; } .comment-section { margin-top: 20px; } </style> </head> <body> <h2>商品列表</h2> <p th:if="${session.user}" th:text="'欢迎,' + ${session.user.username} + '!'"></p> <a href="/logout">退出登录</a> <div th:each="product : ${products}"> <div class="product"> <img th:src="${product.imageUrl ?: '/images/default.jpg'}" alt="Product Image"/> <h3 th:text="${product.name}"></h3> <p th:text="'¥' + ${#numbers.formatDecimal(product.price, 1, 'COMMA', 2)}"></p> <p th:text="${product.stock > 0 ? '库存: ' + product.stock : '已售罄'}" th:class="${product.stock > 0 ? 'text-success' : 'text-danger'}"></p> <form th:action="@{/add_to_cart}" method="post"> <input type="hidden" name="productId" th:value="${product.id}"/> <button type="submit" th:disabled="${product.stock == 0}">加入购物车</button> </form> <details> <summary>查看评论</summary> <div class="comment-section"> <th:block th:each="comment : ${product.comments}"> <p><strong th:text="${comment.username}"></strong> <span th:each="i : ${#numbers.sequence(1, comment.rating)}">⭐</span> (<span th:text="${comment.rating}"></span>/5) </p> <p th:text="${comment.content}"></p> <small th:text="${comment.createTime}"></small> <hr/> </th:block> <a th:href="'/comment?productId=' + ${product.id}">我要评论</a> </div> </details> </div> </div> <a href="/cart">查看购物车</a> | <a href="/orders">我的订单</a> </body> </html> ``` --- ### ✅ 4. `cart.html` —— 购物车页 ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>购物车</title> <style> table { width: 100%; border-collapse: collapse; margin: 20px 0; } td, th { border: 1px solid #ddd; padding: 8px; text-align: center; } img { width: 50px; height: 50px; object-fit: cover; } </style> </head> <body> <h2>我的购物车</h2> <table> <thead> <tr> <th>商品图片</th> <th>名称</th> <th>单价</th> <th>数量</th> <th>小计</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="item : ${cartItems}"> <td><img th:src="${item.product.imageUrl}" alt=""/></td> <td th:text="${item.product.name}"></td> <td th:text="'¥' + ${#numbers.formatDecimal(item.product.price, 1, 'COMMA', 2)}"></td> <td th:text="${item.quantity}"></td> <td th:text="'¥' + ${#numbers.formatDecimal(item.subtotal, 1, 'COMMA', 2)}"></td> <td> <a th:href="'/remove_from_cart?id=' + ${item.id}">移除</a> </td> </tr> </tbody> </table> <h3>总计:<span th:text="'¥' + ${#numbers.formatDecimal(totalPrice, 1, 'COMMA', 2)}"></span></h3> <a href="/product_list"><button>继续购物</button></a> <a href="/checkout"><button>去结算</button></a> </body> </html> ``` --- ### ✅ 5. `order_list.html` —— 订单列表页 ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>我的订单</title> <style> table { width: 100%; border-collapse: collapse; margin: 20px 0; } td, th { border: 1px solid #ddd; padding: 8px; text-align: center; } </style> </head> <body> <h2>我的订单</h2> <table> <thead> <tr> <th>订单ID</th> <th>商品</th> <th>数量</th> <th>总价</th> <th>状态</th> <th>下单时间</th> </tr> </thead> <tbody> <tr th:each="order : ${orders}"> <td th:text="${order.id}"></td> <td th:text="${order.productName}"></td> <td th:text="${order.quantity}"></td> <td th:text="'¥' + ${#numbers.formatDecimal(order.totalPrice, 1, 'COMMA', 2)}"></td> <td th:text="${order.status}" th:class="${order.status}"></td> <td th:text="${#temporals.format(order.createTime, 'yyyy-MM-dd HH:mm')}"></td> </tr> </tbody> </table> <a href="/product_list">返回商品列表</a> </body> </html> ``` --- ### ✅ 6. `comment.html` —— 评论提交页 ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>发表评论</title> <style> body { padding: 20px; } textarea { width: 100%; height: 100px; margin: 10px 0; } input[type="range"] { width: 100%; } </style> </head> <body> <h2>为商品评分并评论</h2> <form th:action="@{/submit_comment}" method="post"> <input type="hidden" name="productId" th:value="${productId}"/> <label>评分:</label> <input type="range" name="rating" min="1" max="5" value="5" step="1" oninput="this.nextElementSibling.value = this.value"/> <output>5</output>/5<br/> <label>评论内容:</label><br/> <textarea name="content" placeholder="写下你的评价..." required></textarea><br/> <button type="submit">提交评论</button> </form> <a href="/product_list">返回商品列表</a> </body> </html> ``` --- ### ✅ 文件目录结构 ``` src/main/resources/templates/ ├── login.html ├── register.html ├── product_list.html ├── cart.html ├── order_list.html └── comment.html ``` --- ### ✅ 配合后端使用的提示 | 功能 | Controller 应提供 | |------|------------------| | `/product_list` | `Model.addAttribute("products", ...)` | | `/cart` | `Model.addAttribute("cartItems", ...)`, `"totalPrice"` | | `/orders` | `Model.addAttribute("orders", ...)` | | `${session.user}` | 登录成功后 `session.setAttribute("user", user)` | --- ✅ 这些 HTML 页面可以直接运行在 Spring Boot + Thymeleaf 环境中,无需 JSP,也不依赖 JavaScript 框架,适合教学或基础项目。 如果你需要我为你生成对应的 **Controller 示例代码** 来配合这些页面,请告诉我!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值