【项目】小型支付商城 MVC/DDD

一、领域拆分(四色建模)

1、概念

  • 蓝色 - 决策命令:用户发起的行为动作。

  • 黄色 - 领域事件,事件完成态。

  • 粉色 - 外部系统,外部接口。

  • 红色 - 业务流程,串联决策命令到领域事件。

  • 绿色 - 只读模型,读取数据的动作,没有写库的操作。

  • 棕色 - 领域对象,启动决策命令的发起。

2、寻找领域事件

找领域事件。

3、识别领域角色和对象并划分领域边界

找决策命令、领域对象、执行用户。圈出领域边界。

二、数据库表设计

(1)支付订单表

  • DATETIME:不受 2038 年限制。
  • DECIMAL :避免浮点数(FLOAT/DOUBLE)带来的精度误差,适合货币、计费等场景。
  • 用户、商品、订单、支付单。
sql 复制代码
SET NAMES utf8mb4;

CREATE database if NOT EXISTS `s-pay-mall` default character set utf8mb4 ;
use `s-pay-mall`;

DROP TABLE IF EXISTS `pay_order`;

CREATE TABLE `pay_order` (
  `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` varchar(32) CHARACTER SET utf8mb4  NOT NULL COMMENT '用户ID',
  `product_id` varchar(16) NOT NULL COMMENT '商品ID',
  `product_name` varchar(64) NOT NULL COMMENT '商品名称',
  `order_id` varchar(16) CHARACTER SET utf8mb4  NOT NULL COMMENT '订单ID',
  `order_time` datetime NOT NULL COMMENT '下单时间',
  `total_amount` decimal(8,2) unsigned DEFAULT NULL COMMENT '订单金额',
  `status` varchar(32) CHARACTER SET utf8mb4  NOT NULL COMMENT '订单状态;create-创建完成、pay_wait-等待支付、pay_success-支付成功、deal_done-交易完成、close-订单关单',
  `pay_url` varchar(2014) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '支付信息',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_order_id` (`order_id`),
  KEY `idx_user_id_product_id` (`user_id`,`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;

三、创建工程

1、创建初始多模块工程

创建一个多模块工程(将 mvc 每一层放到不同模块,各层间解耦,互不影响;别人要复用哪个模块,直接下对应模块,不用整个项目打包)。把父工程的源代码src 都删了。每个层级分别创个 module。controller 是对外的接口,只保留它的启动文件、yml 配置(启动时加载)、静态资源和测试文件,其他都删掉。

配置文件 pom、yml(各种版本的)、日志配置xml。

2、上传到远程仓库

Git 有两种常用传输协议,HTTPS 和 SSH。使用 SSH 方式通信需要创建 SSH 密钥

bash 复制代码
ssh-keygen -t rsa -C "你的邮箱,用于备注这个密钥是哪个设备的"

一路回车,用户主目录生成了.ssh 文件:

右上角头像》设置》ssh 公钥:

创建远程 Git 仓库,注意不要生成 readme 等文件,因为这样会同时生成一个 .git 文件,这就与本地 init 的 .git 文件冲突了。保持这个界面:

这个日志我们可以忽略掉,不上传:

add:上传到本地暂存区。

ctr+K:commit

ctrl+shift+K:推送到远程仓库,先配置一下远程仓库 url,结束。

四、准备工作

1、内网穿透

我是在云服务器和本地搭建了一个内网穿透服务,docker 运行了 frp 技术镜像的容器,参考我的另一篇文章:【工具】内网穿透服务搭建。主要目的是让项目在本地的 linux 环境运行,消耗的是本地资源,这样就不用买贵的云服务了,买最低档的云服务器也能提供公网 IP 访问内网 IP 项目支持。

再一个就是需要用到微信、支付宝服务,开发阶段第三方服务得调我们本地的接口,那肯定要搭建内网穿透了(否则你每次测试还得部署到云服务器)。

2、第三方接口对接

见我的博客:1. 【工具】微信公众号测试平台的使用 2. 【工具】支付宝沙箱的使用

五、MVC 架构业务实现

1、微信公众号扫码登陆

(1)需求描述

用户在 web 前端点击登录,web 前端展示微信二维码。用户用微信扫码后,通知登录成功模板到微信公众号对话框,同时前端轮询校验用户是否登录成功,后续可跳转到下单页面。

(2)时序图

  • access_token(微信公众号 api 调用的全局凭证):用于验证应用身份并授权接口访问权限,有过期时间,因此需及时更新并缓存(避免频繁调用获取接口,并且微信也有每天获取次数的限制)。
  • 先获取二维码 ticket(ticket 是二维码的唯一凭证,关联了二维码状态等信息,便于在数据库中查找二维码相关数据;同时会返回二维码 url,开发者可个性化生成二维码图;将 ticket 与二维码解耦,缓解了微信服务的生成压力),请求参数中的场景值可用于识别用户的扫描入口,便于开发者统计推广效果数据。
  • 后续前端可用 ticket 换取二维码
  • 用户用微信扫码后:若未关注公众号,关注后会推送带场景值 的关注事件;若关注了,推送带场景值的扫码事件。推送 xml 数据包中,包含 MsgType(消息类型)、Event(事件类型),根据类型可以判断消息是否是 "event",事件是否是 SCAN/subscribe,已关注用户扫码/未关注用户关注,将登录成功。缓存用户的 openId,可用 ticket 作为 key 查询,以此判断用户是否登录。
  • 后续web前端轮询校验是否登录(查询 ticket 对应的 openId,也可以给前端返回 jwt token)。

(3)对接文档

(4)接口测试

获取 ticket:

凭 ticket 换取二维码图片:

已关注用户扫码后,发送登陆成功通知模板:

校验登录,返回 openId:

2、商品下单

(1)需求描述

用户下单后,后端创建订单+支付单,保存订单信息到数据库,返回支付宝支付的表单页面。用户支付后,更新数据库中订单的状态为已支付。

(2)时序图

  • 创建订单:入参:用户id+商品id。

查询该订单:

① 若创建了,但没有创建支付单(CREATE 状态)(掉单),创建支付单后,更新订单状态为 WAIT_PAY、更新支付表单,并返回。

② 若创建了订单+支付单,直接返回。

③ 若没有创建该订单,创建订单+支付单,插入数据库后返回。

  • 支付宝回调通知:用户支付后,支付宝回调通知用户已支付,我们需要更新订单状态为已支付,并将订单支付信息放入消息队列mq,以便异步解耦后续的业务,如:发货、充值、积分等。
  • 如果回调通知处理订单状态失败(抛出异常),支付宝会在有限次数内重试,若依旧失败,我们需要进行补救处理:定时任务,每隔 3 秒钟扫描 WAIT_PAY 状态+超时 1 分钟的订单,向支付宝查询订单,若订单的交易状态为"交易成功",则更新订单状态为已支付。
  • 超时订单关闭:同样需要定时任务,每隔 10 分钟扫描 WAIT_PAY 状态+超时 30 分钟的订单,更新它们的状态为 CLOSE。

(3)对接文档

(4)接口测试

创建订单:

未正确处理支付回调的定时任务:

3、前端

(1)扫码登录

页面加载完后》访问后端获取 ticket》若成功,访问微信服务换取二维码》轮询校验用户是否扫码登录》登录成功,停止轮询、保存 token 到本地、跳转到下单首页。

(2)下单

页面加载完后》用户点击下单按钮》查看本地是否保存 token,若没有,跳转到扫码登录页;若有,访问后端创建订单》嵌入支付表单,并提交表单,跳转到支付宝支付页。

(3)功能测试

4、Docker 部署

(1)环境

制作 mysql(需要挂载 .sql 脚本,容器对外的端口最好不要用 3306,容易与 windows 本机的 mysql 冲突)、redis、rabbitmq、以及它们的管理页面的 docker-compose,在同一自定义网络中。

修改项目 配置文件的 mysql、redis、rabbitmq 连接信息(IP 使用容器名)。

遇到的 wsl 环境问题:(rabbitmq 一直重启不成功 )Cookie file /var/lib/rabbitmq/.erlang.cookie must be accessible by owner only。解析:.erlang.cookie 是 RabbitMQ 用于节点间通信的安全凭证(类似 "密码"),为了防止泄露,RabbitMQ 强制要求其权限必须为 600(即 -rw------- ,含义是:仅文件所有者(user)有读写权限。我的项目存储在 Windows 的 D: 盘(WSL 中挂载为 /mnt/d),而 Windows 文件系统(NTFS)与 Linux 文件系统的权限机制不同:

  • 默认情况下 ,WSL 对 /mnt/d 等 Windows 分区使用 DrvFs 文件系统,不会真正存储 Linux 权限信息,导致所有文件 / 目录默认显示为 777(全权限),且 chmod 命令无效。
  • 这直接导致 .erlang.cookie 无论怎么修改,权限始终不符合 RabbitMQ 的要求,启动失败。

解决方案:

XML 复制代码
# 重新挂载 Windows 分区并添加 metadata 选项:让 WSL 能在 Windows 文件上存储 Linux 权限信息(如 600),使 chmod 命令生效。
# 配置 wsl.conf 中的权限掩码:通过 umask 和 fmask 统一设置新建文件 / 目录的默认权限,避免默认 777。

# 编辑 WSL 配置文件
sudo vi /etc/wsl.conf

# 添加以下内容
[automount]
options = "metadata,umask=022,fmask=111"  # 核心:启用 metadata 并设置默认权限掩码

# 未来版本支持后可添加(当前版本可能无效,提前配置无副作用)
[filesystem]
umask = 022


# 修正 Shell 的 umask:确保新建文件的权限不会因 WSL 版本问题被重置为 000(过松)。
vi ~/.bashrc
# 文件尾添加
# 修复默认权限为 000 的问题(强制设置为 022)
if [ "$(umask)" = "000" ]; then
  umask 022
fi

# 保存后生效
source ~/.bashrc  # 立即生效,无需重启

# 重启
wsl --shutdown  # 完全关闭 WSL 子系统
wsl

修改文件权限:
sudo chmod 600 rabbitmq/data/.erlang.cookie
查看文件权限:
ls -la rabbitmq/data/.erlang.cookie

执行:

XML 复制代码
docker-compose -f docker-compose-environment.yml up -d

进入 rabbitmq 容器,启动管理页面(其它管理页面直接到 docker 管理页面点):

XML 复制代码
# 启动
rabbitmq-plugins enable rabbitmq_management

# 访问,也可以直接在 docker 管理页面进入
wsl ip + 外部访问容器的端口

(2)应用

制作后端服务镜像 dockerfile,制作前后端 docker-compose,一键部署。

(3)内网穿透

配置:新增代理的客户端,IP 和 端口用 容器名和容器端口。

sql 复制代码
[[proxies]]
# 代理应用名称,根据自己需要进行配置
name = "small-pay-mall-prod"
# 代理类型 有tcp\udp\stcp\p2p
type = "tcp"
# 客户端代理应用IP
localIP = "small-pay-mall"
# 客户端代理应用端口
localPort = 8080
# 服务端反向代理端口;提供给外部访问
remotePort = 8082

[[proxies]]
# 代理应用名称,根据自己需要进行配置
name = "small-pay-mall-nginx"
# 代理类型 有tcp\udp\stcp\p2p
type = "tcp"
# 客户端代理应用IP
localIP = "small-pay-mall"
# 客户端代理应用端口
localPort = 80
# 服务端反向代理端口;提供给外部访问
remotePort = 8083

docker-compose:跟应用使用同一个自定义网络。(因为 frpc 跟应用的 docker-compose 不在同一个文件夹下,所以指定 my-network 实际上是创建了 fpc_my-network,会导致不在同一个网络下。)

XML 复制代码
    networks: # 网络名会自动加一个前缀--docker-compose.yaml 所在目录名
      my-network:

networks: # 指定已存在的网络
  my-network:
    external: true  # 声明这是一个外部已存在的网络
    name: docs_my-network  # 明确指定网络的实际名称(不带前缀)

在云服务器禁用应用端口号 8082、8083 的防火墙。

一键部署。

六、DDD 架构业务实现

1、DDD 架构对比 MVC 分析

(1)web 层的拆分

问题:mvc 中,把项目启动(项目启动配置、启动类)+各种外部调用我们的方式(监听器、定时任务、controller http 调用等),都放到 web 层中,非常杂乱臃肿,职责不清晰

拆分:app 层 只负责项目的启动(配置+启动类);trigger 层只负责各种调用我们的方式(前端 http 通信调用 controller、定时任务、mq 监听、rpc 通信等)

再拆分:在微服务架构中,我们需要用到 rpc 让独立运行的不同服务相互通信。把 controller、dto 都放到 trigger 包只适合 http 通信(前端只需要知道 URL 和 DTO 格式,不需要关心 controller 的代码),但是rpc 不适应。因为把整个 controller 都打包给别人是没必要的(我们也不想暴露具体实现),所以需要把 controller 接口定义、dto (其它服务调用我们接口的规则)放到api 层,这样只需要打包 api 即可(让契约和实现解耦)。

(2)让 service 和 domain 充血

问题:mvc 把所有实体类都放在 domain 层下,所有人混着用,导致实体类被不断地修改填充各种各样的属性(后面的人觉得前人写的这个实体类跟我需要的差不多,就加点东西再用),一个实体混杂太多参数,文档及项目难以维护、测试困难低效(一个小改动就要测试各种情况),这都是职责不清晰带来的困扰。service 和 dao 层也是同理,把所有业务以及对第三方、数据库、中间件的调用,都放在同一个包下,随着业务的堆积,难以知道是否有前人写过某个服务,自己又冗余创建一个,导致代码混乱且难以维护。

充血模型 :不同业务放到不同的领域模型之下,设施配备齐全(订单需要的业务、对第三方接口的调用、数据库、中间件的调用,按需配置,不受其它领域的影响)。

充血对象 :让实体类不仅有属性,还配备各种需要的工具,比如:把某个属性进行类型转换、校验订单状态是否匹配等。

基础设施层 专门用于实现我们的项目对其它接口的调用(第三方、数据库、中间件等),各个领域只需要定义基础设施的接口,基础设施层实现适配器和接口(基础设施引用领域,领域注入基础设施的实现,倒置引用),目的是对领域隐藏具体的实现,防止对基础设施层进行混乱地 "按需" 修改。还有个好处:因为基础设施实现了接口适配器 ,无论缓存换了什么中间件,我只需要换适配器就好,根本不会影响到领域层

(3)model 的划分

实体类(用于改变持久化值)、值对象(用于定义枚举、只读对象)、聚合对象(多个实体对象组合,聚合内规范事务一致性的范围)。

最后,types 层放公共部分(mvc 中的 common 层)。

2、脚手架创建初始化工程

把数据库连接配置好,注释掉 mybatis 启动一下试试。

因为 trigger 要实现 api 的接口,所以需要引用 api 的包(倒置引用,避免把具体实现暴露给 api):

因为 基础设施层 需要实现 domain 层的接口,所以需要引用 domain 的包(倒置引用):

3、微信扫码登录重构

(1)微信公众号普通消息推送

公共微信 sdk 工具包 》 types;controller 》trigger

(2)扫码登录及校验

登录 controller 》trigger;api 定义 controller 接口和公共返回类(也可以不用,只要不用 rpc 对外提供接口,就不是必须的);controller config 》 app config

登陆服务,将业务的实现与第三方微信接口对接的实现分开,可以分组协同开发,职责清晰,效率更高。adapter:对两个接口(业务层定义的接口 和 微信接口)进行对接。

4、下单支付宝支付重构

(1)创建订单

订单创建,把原订单服务的实现,拆成实现抽象类(订单服务共有的创建操作 AbstractOrderService)、更具体的实现(继承抽象类,具体实现了保存订单操作 OrderServiceImpl,因为普通、秒杀、预售订单的保存细节有可能不同)、rpc 调用商品服务和操作数据库的服务。并且所有需要的实体类都放在订单领域,不会使用其它领域、层级的实体类。基础层的 adapter 实现了订单服务调用基础服务的接口 和 商品服务接口、数据库操作接口的对接,订单服务可以直接把 ShopCar 传入基础设施调用接口,基础设施的 adapter 在调用具体实现前,再把 ShopCar 转换为其需要的入参(如数据库 PayOrder)。

(2)创建支付单

核心:把订单服务中,创建支付单的部分(调用阿里支付宝接口)的具体实现,移到基础设施层、支付单数据库的更新也在基础设施层。

(3)支付回调

核心:把数据库操作、mq 操作放到基础设施层;mq 消费者、定时补偿任务、定时关单任务放到 trigger。

相关推荐
知兀2 小时前
【Spring/SpringBoot】SSM(Spring+Spring MVC+Mybatis)方案、各部分职责、与Springboot关系
java·spring boot·spring
向葭奔赴♡2 小时前
Spring IOC/DI 与 MVC 从入门到实战
java·开发语言
早退的程序员2 小时前
记一次 Maven 3.8.3 无法下载 HTTP 仓库依赖的排查历程
java·http·maven
向阳而生,一路生花2 小时前
redis离线安装
java·数据库·redis
Tigshop开源商城系统2 小时前
Tigshop 开源商城系统 php v5.1.9.1版本正式发布
java·大数据·开源·php·开源软件
·云扬·2 小时前
使用pt-archiver实现MySQL数据归档与清理的完整实践
数据库·mysql
黄焖鸡能干四碗2 小时前
信息安全管理制度(Word)
大数据·数据库·人工智能·智慧城市·规格说明书
zhangyifang_0092 小时前
PostgreSQL一些概念特性
数据库·postgresql