当前位置: 首页 > news >正文

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

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

1、概念

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

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

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

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

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

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

2、寻找领域事件

        找领域事件。

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

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

二、数据库表设计

(1)支付订单表

  • DATETIME:不受 2038 年限制。
  • DECIMAL :避免浮点数(FLOAT/DOUBLE)带来的精度误差,适合货币、计费等场景。
  • 用户、商品、订单、支付单。
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 密钥

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)对接文档

  • 获取 Access Token:基础接口 / 获取接口调用凭据
  • 生成带参数的二维码:服务号二维码 / 带参二维码 / 生成带参数的二维码
  • 接收消息推送事件(扫描带参数二维码事件):能力接入 / 基础消息与订阅通知 / 接收事件推送
  • 发送模板消息:基础消息与订阅通知 / 模板消息 / 发送模板消息

(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 脚本)、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 的要求,启动失败。

        解决方案:

# 重新挂载 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" ]; thenumask 022
fi# 保存后生效
source ~/.bashrc  # 立即生效,无需重启# 重启
wsl --shutdown  # 完全关闭 WSL 子系统
wsl修改文件权限:
sudo chmod 600 rabbitmq/data/.erlang.cookie
查看文件权限:
ls -la rabbitmq/data/.erlang.cookie

        执行:

docker-compose -f docker-compose-environment.yml up -d

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

# 启动
rabbitmq-plugins enable rabbitmq_management# 访问,也可以直接在 docker 管理页面进入
wsl ip + 外部访问容器的端口

(2)应用

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

(3)内网穿透

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

[[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,会导致不在同一个网络下。)

    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。

http://www.dtcms.com/a/574235.html

相关文章:

  • uni-app开发app移动端使用ucharts自定义标签栏Tooltip
  • 《uni-app跨平台开发完全指南》- 03 - Vue.js基础入门
  • uniapp中的静态资源文件,如图片等文件,h5端设置本地与生产测试环境的区别,本地不加前缀,生产测试添加前缀,h5端的已进行测试可行,非h5的未进行测试
  • uni-app + Vue3 实现折叠文本(超出省略 + 展开收起)
  • 云南微网站搭建wordpress插件安装不
  • 汽车行业网站设计chrome google
  • 好用的云电脑!手机怎么用UU远程云电脑玩电脑游戏?
  • 网站开发安装网站原型图软件
  • 坑#Spring Cloud Gateway#DataBufferLimitException
  • 15年做哪些网站能致富网页升级访问紧急通知狼
  • ping: baidu.com: 域名解析暂时失败
  • 上海网站设计方法有哪些网站上可以做试卷
  • 网站建设项目立项登记 表自己家的电脑宽带50m做网站服务器
  • 宜宾公司做网站建设一个电子文学网站资金多少
  • 效率提升的声音助手——工业物联网中的智能化变革
  • 普罗宇宙发布大白机器人2.0 及灵巧手,携手京东加速全球化落地
  • Java 集合框架:List 体系与实现类深度解析
  • 阿里云 ip 网站哈尔滨行业网站建设策划
  • 注册了网站怎么建设网站视听内容建设
  • 泉州专业做网站网上做网站怎么防止被骗
  • 使用 ECharts + ECharts-GL 生成 3D 环形图
  • 做电影网站视频放在那里南阳做那个网站好
  • 美德的网站建设局网站建设招标
  • 学校网站的建设论文怎么建网站做推广
  • 第四阶段通讯开发-7:TCPListener和TCPClient
  • 中国最权威的网站排名电脑网站安全证书有问题如何解决
  • 网站建设实训小结在线网站流量查询
  • 深圳网站建设自己人做1688网站到哪里找图片
  • C++ —— list
  • xv6 附录A