diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..4493970 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,138 @@ +# 编写的原因 + +写在前面,很多加入我们群里的人,都会问我们源码在哪里,现在仔细回答一下 + +1. 我们已经声明了,这是一个前后端分离的商城,而且分离的很彻底,java的后台管理系统不是jsp,使用vue写的,所以商城的后台管理在vue这个项目 https://gitee.com/gz-yami/mall4v ,启动完java,不要访问java的端口,启动vue,访问vue的端口页面,就能看到后台管理,就能上商品了!!! + +2. 和上面一样的,很多人问,前端的浏览商品的页面在哪里,其实就在这里 https://gitee.com/gz-yami/mall4m + +3. 有人会问你是不是将上面两个链接藏起来了,上面两个项目的链接都在readme里面写着,可是很多人都不读。 + +4. swagger文档怎么访问不了,其实路径是/doc.html + +5. 我们开源的刚上线直接申请通过了推荐项目,第一天就有100个星星,一个月就有1k的星星,大家都不是傻的,这代码是能用的,拜托...后来没怎么维护(整个流程都是好的,整个功能都是好的,都不知道维护啥),现在又开始维护了,星星也没了(如果你悄悄拿去做外包项目,觉得这个项目对你有用,就给个星星呗) + + + +## 先确定我们下载的项目有几个项目 + +- mall4j:j代表java,java项目,这里面包含了小程序/后台vue连接需要的接口。 +- mall4v:v代表vue项目,是后台管理员界面使用的前端项目,因为前后端分离的 +- mall4m:m代表mini,小程序项目,这里的项目是小程序的项目 +- mall4uni:uni代表uniapp,H5项目,这里的项目是H5的项目 +- jvm:java虚拟机啦~ + + +## 1.java开发环境安装 + + +### 1.1开发环境 + +以下版本是最低要求的!!! 提问问题前请注意开发环境!! + +| 工具 | 版本 | +|---------|-------| +| jdk | 17 | +| mysql | 5.7+ | +| redis | 4.0+ | +| nodejs | 14-16 | +| xxl-job | 2.4.0 | + + +### 1.2 安装jdk + mysql + redis + maven + +如果不了解怎么安装jdk的,可以参考 [菜鸟教程的java相关](https://www.runoob.com/java/java-environment-setup.html) +- 教程展示的是oracle,需要自行搜索openjdk的下载链接,下载jdk17版本 + +如果不了解怎么安装mysql的,可以参考 [菜鸟教程的mysql相关](https://www.runoob.com/mysql/mysql-install.html) + +如果不了解怎么安装maven的,可以参考 [菜鸟教程的maven相关]( https://www.runoob.com/maven/maven-setup.html ) + +如果对于redis的安装并不了解的,可以参考 [菜鸟教程的redis相关](https://www.runoob.com/redis/redis-install.html) + +安装相对简单,网上也有很多教程,这里就不多讲述。安装完按需对redis进行配置,后启动redis服务即可。 + +### 2.启动 + +- 推荐使用idea,安装lombok插件后,使用idea导入maven项目 +- 将yami_shop.sql导入到mysql中,修改`application-dev.yml`更改 datasource.url、user、password +- 通过修改`shop.properties` 修改七牛云、阿里大鱼等信息 +- 修改`api.properties` 修改当前接口所在域名,用于支付回调 +- 启动redis,端口6379 +- 通过`WebApplication`启动项目后台接口,`ApiApplication` 启动项目前端接口 +- xxl-job定时任务,通过github或者gitee下载xxl-job的已经打包好的源码,把`XxlJobConfig.class`这个文件的代码注释打开,配置yml文件中相关xxl-job配置即可使用 + + +## 3.vue开发环境安装 + +这是一套正常的vue启动流程。如果你无法理解,可能要先学习一下vue... + +#### 3.1 安装nodejs + +[NodeJS](https://nodejs.org) 项目要求最低 18.12.0,推荐 20.9.0 + +如果不了解怎么安装nodejs的,可以参考 [菜鸟教程的nodejs相关](https://www.runoob.com/nodejs/nodejs-install-setup.html) + + + +#### 3.2 安装依赖启动项目 + +项目要求使用 [pnpm](https://www.pnpm.cn/) 包管理工具 + +使用编辑器打开项目,在根目录执行以下命令安装依赖 + +```bash +pnpm i +``` + +如果不想使用 pnpm,请删除 `package.json` 文件中 `preinstall` 脚本后再进行安装 + +```json +{ + "scripts" : { + "preinstall": "npx only-allow pnpm" // 使用其他包管理工具(npm、yarn、cnpm等)请删除此命令 + } +} +``` + + +H5端和平台端修改文件`.env.production`(生产环境)/ `.env.development`(开发环境) +里面的`VITE_APP_BASE_API`为api接口请求地址, `VITE_APP_RESOURCES_URL`为静态资源文件url + +```json +// api接口请求地址 +VITE_APP_BASE_API = 'http://127.0.0.1:8085' + +// 静态资源文件url +VITE_APP_RESOURCES_URL = 'https://img.mall4j.com/' + +``` + +mall4m小程序端修改文件`utils\config.js`,里面的`domain`为api接口请求地址 + +注意!!如果启动uni项目或者小程序,默认后台api服务端口号为8086, +如果启动后台项目,默认后台admin服务端口号为8085,请对照仔细填写后再启动,如遇401状态码,仔细检查端口号是否配置正确! +如果后台启动后,图形验证码显示“接口验证失败数过多,请稍后再试”,请F12打开network确定连接的admin服务端口号是否正确,ip或域名是否正确, +如果有配置nginx,还要确认下项目访问路径是否正确,可以通过地址+/doc.html来访问接口文档确定是否正确访问到admin服务 + + + + +运行dev环境: + +```bash +npm run dev +``` + +运行dev环境(H5): + +```bash +npm run dev:h5 +``` + +## 4.文档 + +这代码有没有文档呀? +当然有啦,你已经下载了,在doc这个文件夹上,实在不知道,我就给链接出来咯: + +### [https://gitee.com/gz-yami/mall4j/tree/master/doc](https://gitee.com/gz-yami/mall4j/tree/master/doc) diff --git a/doc/商城表设计/img/%E9%80%80%E6%AC%BE%E6%96%87%E6%A1%A3%5C1566788134396.png b/doc/商城表设计/img/%E9%80%80%E6%AC%BE%E6%96%87%E6%A1%A3%5C1566788134396.png new file mode 100644 index 0000000..417c294 Binary files /dev/null and b/doc/商城表设计/img/%E9%80%80%E6%AC%BE%E6%96%87%E6%A1%A3%5C1566788134396.png differ diff --git a/doc/商城表设计/img/01.png b/doc/商城表设计/img/01.png new file mode 100644 index 0000000..78b15de Binary files /dev/null and b/doc/商城表设计/img/01.png differ diff --git a/doc/商城表设计/img/02.png b/doc/商城表设计/img/02.png new file mode 100644 index 0000000..912c2b2 Binary files /dev/null and b/doc/商城表设计/img/02.png differ diff --git a/doc/商城表设计/img/05.png b/doc/商城表设计/img/05.png new file mode 100644 index 0000000..1bafc84 Binary files /dev/null and b/doc/商城表设计/img/05.png differ diff --git a/doc/商城表设计/img/1008612.png b/doc/商城表设计/img/1008612.png new file mode 100644 index 0000000..1388758 Binary files /dev/null and b/doc/商城表设计/img/1008612.png differ diff --git a/doc/商城表设计/img/1566788134396.png b/doc/商城表设计/img/1566788134396.png new file mode 100644 index 0000000..417c294 Binary files /dev/null and b/doc/商城表设计/img/1566788134396.png differ diff --git a/doc/商城表设计/img/24.png b/doc/商城表设计/img/24.png new file mode 100644 index 0000000..18c0a43 Binary files /dev/null and b/doc/商城表设计/img/24.png differ diff --git a/doc/商城表设计/img/sku.png b/doc/商城表设计/img/sku.png new file mode 100644 index 0000000..200fa9f Binary files /dev/null and b/doc/商城表设计/img/sku.png differ diff --git a/doc/商城表设计/img/tk01.png b/doc/商城表设计/img/tk01.png new file mode 100644 index 0000000..a26a6ad Binary files /dev/null and b/doc/商城表设计/img/tk01.png differ diff --git a/doc/商城表设计/img/tk03.png b/doc/商城表设计/img/tk03.png new file mode 100644 index 0000000..faed88c Binary files /dev/null and b/doc/商城表设计/img/tk03.png differ diff --git a/doc/商城表设计/img/优惠券数据关系设计2.png b/doc/商城表设计/img/优惠券数据关系设计2.png new file mode 100644 index 0000000..978d810 Binary files /dev/null and b/doc/商城表设计/img/优惠券数据关系设计2.png differ diff --git a/doc/商城表设计/img/减免类型.png b/doc/商城表设计/img/减免类型.png new file mode 100644 index 0000000..b18abb4 Binary files /dev/null and b/doc/商城表设计/img/减免类型.png differ diff --git a/doc/商城表设计/img/分组管理数据库设计2.png b/doc/商城表设计/img/分组管理数据库设计2.png new file mode 100644 index 0000000..aee0584 Binary files /dev/null and b/doc/商城表设计/img/分组管理数据库设计2.png differ diff --git a/doc/商城表设计/img/发布商品.png b/doc/商城表设计/img/发布商品.png new file mode 100644 index 0000000..b767bdd Binary files /dev/null and b/doc/商城表设计/img/发布商品.png differ diff --git a/doc/商城表设计/img/后台-商品分组.png b/doc/商城表设计/img/后台-商品分组.png new file mode 100644 index 0000000..165942e Binary files /dev/null and b/doc/商城表设计/img/后台-商品分组.png differ diff --git a/doc/商城表设计/img/后台-添加优惠券活动.png b/doc/商城表设计/img/后台-添加优惠券活动.png new file mode 100644 index 0000000..95cf31a Binary files /dev/null and b/doc/商城表设计/img/后台-添加优惠券活动.png differ diff --git a/doc/商城表设计/img/后台分组位置.png b/doc/商城表设计/img/后台分组位置.png new file mode 100644 index 0000000..7277d74 Binary files /dev/null and b/doc/商城表设计/img/后台分组位置.png differ diff --git a/doc/商城表设计/img/后台商城地址管理.png b/doc/商城表设计/img/后台商城地址管理.png new file mode 100644 index 0000000..eb5968e Binary files /dev/null and b/doc/商城表设计/img/后台商城地址管理.png differ diff --git a/doc/商城表设计/img/后台指定包邮条件.png b/doc/商城表设计/img/后台指定包邮条件.png new file mode 100644 index 0000000..8c65cc1 Binary files /dev/null and b/doc/商城表设计/img/后台指定包邮条件.png differ diff --git a/doc/商城表设计/img/后台指定区域包邮项.png b/doc/商城表设计/img/后台指定区域包邮项.png new file mode 100644 index 0000000..50726b3 Binary files /dev/null and b/doc/商城表设计/img/后台指定区域包邮项.png differ diff --git a/doc/商城表设计/img/后台新增商品运费模板展示.png b/doc/商城表设计/img/后台新增商品运费模板展示.png new file mode 100644 index 0000000..32f4b91 Binary files /dev/null and b/doc/商城表设计/img/后台新增商品运费模板展示.png differ diff --git a/doc/商城表设计/img/后台新增地址.png b/doc/商城表设计/img/后台新增地址.png new file mode 100644 index 0000000..c260789 Binary files /dev/null and b/doc/商城表设计/img/后台新增地址.png differ diff --git a/doc/商城表设计/img/后台新增标签.png b/doc/商城表设计/img/后台新增标签.png new file mode 100644 index 0000000..9a4ea09 Binary files /dev/null and b/doc/商城表设计/img/后台新增标签.png differ diff --git a/doc/商城表设计/img/后台满减瞒折.png b/doc/商城表设计/img/后台满减瞒折.png new file mode 100644 index 0000000..430cd5b Binary files /dev/null and b/doc/商城表设计/img/后台满减瞒折.png differ diff --git a/doc/商城表设计/img/后台运费模板新增.png b/doc/商城表设计/img/后台运费模板新增.png new file mode 100644 index 0000000..baa0988 Binary files /dev/null and b/doc/商城表设计/img/后台运费模板新增.png differ diff --git a/doc/商城表设计/img/后天满减瞒着活动规则.png b/doc/商城表设计/img/后天满减瞒着活动规则.png new file mode 100644 index 0000000..a2e9f16 Binary files /dev/null and b/doc/商城表设计/img/后天满减瞒着活动规则.png differ diff --git a/doc/商城表设计/img/固定时间生效.png b/doc/商城表设计/img/固定时间生效.png new file mode 100644 index 0000000..0c90d27 Binary files /dev/null and b/doc/商城表设计/img/固定时间生效.png differ diff --git a/doc/商城表设计/img/地区信息数据库设计1.png b/doc/商城表设计/img/地区信息数据库设计1.png new file mode 100644 index 0000000..61d89b2 Binary files /dev/null and b/doc/商城表设计/img/地区信息数据库设计1.png differ diff --git a/doc/商城表设计/img/小程序-优惠券使用.png b/doc/商城表设计/img/小程序-优惠券使用.png new file mode 100644 index 0000000..0fff6c9 Binary files /dev/null and b/doc/商城表设计/img/小程序-优惠券使用.png differ diff --git a/doc/商城表设计/img/小程序-分组商品.png b/doc/商城表设计/img/小程序-分组商品.png new file mode 100644 index 0000000..2e8a565 Binary files /dev/null and b/doc/商城表设计/img/小程序-分组商品.png differ diff --git a/doc/商城表设计/img/小程序-收货地址.png b/doc/商城表设计/img/小程序-收货地址.png new file mode 100644 index 0000000..91465d7 Binary files /dev/null and b/doc/商城表设计/img/小程序-收货地址.png differ diff --git a/doc/商城表设计/img/小程序-用户中心-优惠券.png b/doc/商城表设计/img/小程序-用户中心-优惠券.png new file mode 100644 index 0000000..7c88a88 Binary files /dev/null and b/doc/商城表设计/img/小程序-用户中心-优惠券.png differ diff --git a/doc/商城表设计/img/小程序-用户优惠券列表.png b/doc/商城表设计/img/小程序-用户优惠券列表.png new file mode 100644 index 0000000..92d9308 Binary files /dev/null and b/doc/商城表设计/img/小程序-用户优惠券列表.png differ diff --git a/doc/商城表设计/img/小程序-用户优惠券列表2.png b/doc/商城表设计/img/小程序-用户优惠券列表2.png new file mode 100644 index 0000000..722cba8 Binary files /dev/null and b/doc/商城表设计/img/小程序-用户优惠券列表2.png differ diff --git a/doc/商城表设计/img/小程序-首页-领券中心.png b/doc/商城表设计/img/小程序-首页-领券中心.png new file mode 100644 index 0000000..fedb904 Binary files /dev/null and b/doc/商城表设计/img/小程序-首页-领券中心.png differ diff --git a/doc/商城表设计/img/小程序加入购物车.png b/doc/商城表设计/img/小程序加入购物车.png new file mode 100644 index 0000000..228b277 Binary files /dev/null and b/doc/商城表设计/img/小程序加入购物车.png differ diff --git a/doc/商城表设计/img/我的购物车列表.png b/doc/商城表设计/img/我的购物车列表.png new file mode 100644 index 0000000..6ed573f Binary files /dev/null and b/doc/商城表设计/img/我的购物车列表.png differ diff --git a/doc/商城表设计/img/满减优惠项.png b/doc/商城表设计/img/满减优惠项.png new file mode 100644 index 0000000..bccfe91 Binary files /dev/null and b/doc/商城表设计/img/满减优惠项.png differ diff --git a/doc/商城表设计/img/满减优惠项1.png b/doc/商城表设计/img/满减优惠项1.png new file mode 100644 index 0000000..bccfe91 Binary files /dev/null and b/doc/商城表设计/img/满减优惠项1.png differ diff --git a/doc/商城表设计/img/满减满折商品.png b/doc/商城表设计/img/满减满折商品.png new file mode 100644 index 0000000..2aa650a Binary files /dev/null and b/doc/商城表设计/img/满减满折商品.png differ diff --git a/doc/商城表设计/img/满减满折数据库关系.png b/doc/商城表设计/img/满减满折数据库关系.png new file mode 100644 index 0000000..be64199 Binary files /dev/null and b/doc/商城表设计/img/满减满折数据库关系.png differ diff --git a/doc/商城表设计/img/留言.png b/doc/商城表设计/img/留言.png new file mode 100644 index 0000000..d690e93 Binary files /dev/null and b/doc/商城表设计/img/留言.png differ diff --git a/doc/商城表设计/img/规格关联关系.png b/doc/商城表设计/img/规格关联关系.png new file mode 100644 index 0000000..c3b5834 Binary files /dev/null and b/doc/商城表设计/img/规格关联关系.png differ diff --git a/doc/商城表设计/img/规格可以输入.png b/doc/商城表设计/img/规格可以输入.png new file mode 100644 index 0000000..b538558 Binary files /dev/null and b/doc/商城表设计/img/规格可以输入.png differ diff --git a/doc/商城表设计/img/规格属性.png b/doc/商城表设计/img/规格属性.png new file mode 100644 index 0000000..20b1a3d Binary files /dev/null and b/doc/商城表设计/img/规格属性.png differ diff --git a/doc/商城表设计/img/规格选择.png b/doc/商城表设计/img/规格选择.png new file mode 100644 index 0000000..66d51e1 Binary files /dev/null and b/doc/商城表设计/img/规格选择.png differ diff --git a/doc/商城表设计/img/订单管理界面.png b/doc/商城表设计/img/订单管理界面.png new file mode 100644 index 0000000..1a72307 Binary files /dev/null and b/doc/商城表设计/img/订单管理界面.png differ diff --git a/doc/商城表设计/img/订单表设计.png b/doc/商城表设计/img/订单表设计.png new file mode 100644 index 0000000..0fa6fdf Binary files /dev/null and b/doc/商城表设计/img/订单表设计.png differ diff --git a/doc/商城表设计/img/订单详情页面.png b/doc/商城表设计/img/订单详情页面.png new file mode 100644 index 0000000..d2a8cb9 Binary files /dev/null and b/doc/商城表设计/img/订单详情页面.png differ diff --git a/doc/商城表设计/img/购买页.png b/doc/商城表设计/img/购买页.png new file mode 100644 index 0000000..8dd56ee Binary files /dev/null and b/doc/商城表设计/img/购买页.png differ diff --git a/doc/商城表设计/img/购物车关系设计.png b/doc/商城表设计/img/购物车关系设计.png new file mode 100644 index 0000000..23a1048 Binary files /dev/null and b/doc/商城表设计/img/购物车关系设计.png differ diff --git a/doc/商城表设计/img/购物车关系设计1.png b/doc/商城表设计/img/购物车关系设计1.png new file mode 100644 index 0000000..1f34672 Binary files /dev/null and b/doc/商城表设计/img/购物车关系设计1.png differ diff --git a/doc/商城表设计/img/运费模板城市.png b/doc/商城表设计/img/运费模板城市.png new file mode 100644 index 0000000..1226e63 Binary files /dev/null and b/doc/商城表设计/img/运费模板城市.png differ diff --git a/doc/商城表设计/img/运费模板指定包邮城市.png b/doc/商城表设计/img/运费模板指定包邮城市.png new file mode 100644 index 0000000..1ac6ce5 Binary files /dev/null and b/doc/商城表设计/img/运费模板指定包邮城市.png differ diff --git a/doc/商城表设计/img/运费模板数据库关系.png b/doc/商城表设计/img/运费模板数据库关系.png new file mode 100644 index 0000000..924f0f5 Binary files /dev/null and b/doc/商城表设计/img/运费模板数据库关系.png differ diff --git a/doc/商城表设计/img/运费模板运费项.png b/doc/商城表设计/img/运费模板运费项.png new file mode 100644 index 0000000..c10f4ea Binary files /dev/null and b/doc/商城表设计/img/运费模板运费项.png differ diff --git a/doc/商城表设计/img/配送与运费模板.png b/doc/商城表设计/img/配送与运费模板.png new file mode 100644 index 0000000..c9ddbab Binary files /dev/null and b/doc/商城表设计/img/配送与运费模板.png differ diff --git a/doc/商城表设计/img/领取后生效.png b/doc/商城表设计/img/领取后生效.png new file mode 100644 index 0000000..fb368c3 Binary files /dev/null and b/doc/商城表设计/img/领取后生效.png differ diff --git a/doc/商城表设计/商品分组.md b/doc/商城表设计/商品分组.md new file mode 100644 index 0000000..5d12fba --- /dev/null +++ b/doc/商城表设计/商品分组.md @@ -0,0 +1,149 @@ +## 商品分组 + +#### 商城应用 + +在mall4j精选商城首页中,可以看到有`每日上新`、`商城热卖`、`更多商品`等标签栏,在每一栏位中用来展示特定的商品列表,如下图:。 + +![1566266497255](.\img\小程序-分组商品.png) + +在后台中,我们可以对分组标签进行管理 + +![1566358291520](.\img\后台分组位置.png) + + + +![1566358576330](.\img\后台新增标签.png) + +后台指定绑定商品所指定的标签![1566267024882](.\img\后台-商品分组.png) + + + +店铺商品分组有**两种**分组类型: + +- 系统内置 + + 更多宝贝:在新增商品的时候,如果用户没有新增任何的分组标签,系统默认提供了一个默认标签。系统内置的标签不能够被删除。 + +- 商家自定义分组标签 + + 用户可以通过自定义分组标签,在首页根据自定义的分组情况对商品的经行展示。 + +#### 数据库设计 + +整体实体类关系如下图: + +![1566357194240](.\img\分组管理数据库设计2.png) + +model 实体类 + +商品标签类: + +```java +@Data +@TableName("tz_prod_tag") +public class ProdTag implements Serializable { + private static final long serialVersionUID = 1991508792679311621L; + /** + * 分组标签id + */ + @TableId + private Long id; + /** + * 店铺Id + */ + private Long shopId; + /** + * 分组标题 + */ + private String title; + /** + * 状态(1为正常,0为删除) + */ + private Integer status; + /** + * 默认类型(0:商家自定义,1:系统默认类型) + */ + private Integer isDefault; + /** + * 商品数量 + */ + private Long prodCount; + /** + * 排序 + */ + private Integer seq; + /** + * 列表样式(0:一列一个,1:一列两个,2:一列三个) + */ + private Integer style; + /** + * 创建时间 + */ + private Date createTime; + /** + * 修改时间 + */ + private Date updateTime; + /** + * 删除时间 + */ + private Date deleteTime; +} + +``` + +- `id` ,商品分组编号,自增 +- `shopId` ,店铺ID + +​ 用于取分每个店铺,可扩展为B2B2C模式 + +- `status` ,删除时,1为正常,0为删除 +- `title`, 分组标题 +- `isDefault` 是否为默认类型 + - 商家自定义:每日上新,商城热卖等 + - 系统内置:更多宝贝,默认内置的标签不能被删除,在用户 +- `prodCount`,商品数量统计 +- `seq` 排序顺序 +- `style`列表样式(0:一列一个,1:一列两个,2:一列三个) ,用于扩展开发,用户可以根据自己喜欢的排版布局,对商品布局进行排版 + +商品分组引用:商品分组**引用**。一个商品可以有多个商品分组。 + +```java +@Data +@TableName("tz_prod_tag_reference") +public class ProdTagReference implements Serializable{ + private static final long serialVersionUID = 1L; + /** + * 分组引用id + */ + @TableId + private Long referenceId; + /** + * 店铺id + */ + private Long shopId; + /** + * 标签id + */ + private Long tagId; + /** + * 商品id + */ + private Long prodId; + /** + * 状态(1:正常,0:删除) + */ + private Integer status; + /** + * 创建时间 + */ + private Date createTime; +} +``` + +- `referenceId` ,分组引用ID +- `shopId` , 标识所属的店铺,用于取分每个店铺 +- `tagId`, 所指向的标签ID +- `prodId`,所指向的商品ID +- `createTime` 创建时间 + diff --git a/doc/商城表设计/商品表设计.md b/doc/商城表设计/商品表设计.md new file mode 100644 index 0000000..7be3033 --- /dev/null +++ b/doc/商城表设计/商品表设计.md @@ -0,0 +1,411 @@ +# 1. 背景了解 + +在看具体的数据库实体设计之前,我们先一起了解下**电商的名词定义** + +## 1.1 名词定义 + +参考 [《产品 SKU 是什么意思?与之相关的还有哪些?》](https://www.zhihu.com/question/19841574) 整理。 + +**SKU:Stock Keeping Unit** + +中文翻译为库存单位。SKU 从**库存**视角,以库存进出为单位,可以是件、瓶、箱等等。 + +例如,iPhone 手机,按照规格( 颜色 + 内存 )可以组合出如下多个 SKU : + +| SKU | 颜色 | 内存 | +| ---- | ---- | ---- | +| A | 白色 | 16G | +| B | 白色 | 64G | +| C | 黑色 | 16G | +| D | 黑色 | 64G | + +可以看出,颜色(白色、黑色)与内存(16G、64G)组合排列出四种 iPhone SKU。 + +**SPU:Standard Product Unit** + +中文翻译为标准产品单位。SPU 从**产品**视角,是产品信息聚合的**最小单位**,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以被称为一个 SPU 。例如 iPhone 8 就是一个 SPU ,iPhone 8 Plus 也是一个 SPU ,这个与**商家**无关,与颜色、款式、套餐等**规格**无关。 + +**商品**: + +商家出售某个 SPU ,那么这就是一个商品。商品在 SPU 之上,增加了销售价格、促销活动、运费等等信息。另外,**一个商品可以包含多个 SKU**。 + +**总结** + +![img](./img/24.png) + +------ + +现实的场景往往比定义复杂的多,在本文中,**SKU 代表销售的单元**。主要考虑如下两方面: + +- 实际我们看到的商品详情页,购买的是一个**销售组合单元**。例如,很多商家会打包 【iPhone X :银色-64G-套餐三】,其中套餐三为赠送贴膜 + 保护壳等等,当然价格上会更贵。这明显就违背了我们上述提到 SKU 库存的概念,已经变成了多个 SKU 的销售组合单元。![img](./img/01.png) +- 一个商家会在不同平台销售商品,例如三只松鼠,其在天猫、京东等等平台都有官方旗舰店,同时也供货给其他渠道商,那么实际关系会变成如下图所示:![img](./img/02.png) 通过这样的方式,三只松鼠在不同的平台,定义不同的价格,设置不同的促销信息等等个性化的运营。 + +------ + +**那么注意了**!!! +**下文开始,SKU 代表销售的单元** +**下文开始,SKU 代表销售的单元** +**下文开始,SKU 代表销售的单元** + +## 1.2 界面 + +1. 商城端-购买页 + + ![购买页面](./img/购买页.png) + +2. 运营后台-商品发布页 ![发布商品](./img/发布商品.png) + +# 2. 数据库实体 + +整体实体类关系如下图: + +![img](./img/规格关联关系.png) + +全部实体可在数据库中查阅。 + +## 2.1 Product + +Product 字段较多,我们进行简单的切块。 + +### 2.1.1 基础字段 + +```java +@Data +@TableName("tz_prod") +public class Product implements Serializable { + + /** + * 商品ID + */ + @TableId + private Long prodId; + + /** + * 店铺id + */ + private Long shopId; + + /** + * 商品名称 + */ + private String prodName; + + /** + * 简要描述,卖点等 + */ + private String brief; + + /** + * 商品主图 + */ + private String pic; + + /** + * 商品图片 + */ + private String imgs; + + /** + * 默认是1,表示正常状态, -1表示删除, 0下架 + */ + private Integer status; + + /** + * 商品分类 + */ + private Long categoryId; + + /** + * 已经销售数量 + */ + private Integer soldNum; + + /** + * 录入时间 + */ + private Date createTime; + + /** + * 修改时间 + */ + private Date updateTime; + + /** + * 详细描述 + */ + private String content; + + /** + * 上架时间 + */ + private Date putawayTime; + + + @Data + public static class DeliveryModeVO { + + /** + * 用户自提 + */ + private Boolean hasUserPickUp; + + /** + * 店铺配送 + */ + private Boolean hasShopDelivery; + + } +} +``` + +- `prodId` ,商品id,数据库自增。 +- `shopId` ,店铺编号,支持多商户( 店铺 )。 +- `categoryId` ,商品所在分类id,每个商品都有自己所属的分类 + + + +### 2.1.2 价格库存 + +```java + +/** + * 库存量 + * 基于 sku 的库存数量累加 + */ +private Integer totalStocks; + +/** + * 原价 + */ +private Double oriPrice; + +/** + * 现价 + */ +private Double price; + + +``` + +- 在我们的数据库中规定,所有的商品都是具有sku的,就算是只有一种规格的商品,所以`product`里面的库存数量为所有sku库存数量的总和 +- `price` ,商品价格为元,这里使用`Double`而没有使用`BigDecimal `,而数据库中使用`decimal` 进行存储,所以在数据库中是可以进行直接进行运算的,而在java当中需要使用`com.yami.shop.common.util.Arith`进行运算 。 + + +### 2.1.3 运费信息 + +```java + /** + * 配送方式json + */ + private String deliveryMode; + + /** + * 运费模板id + */ + private Long deliveryTemplateId; + + @Data + public static class DeliveryModeVO { + + /** + * 用户自提 + */ + private Boolean hasUserPickUp; + + /** + * 店铺配送 + */ + private Boolean hasShopDelivery; + + } +``` + +- 根据` deliveryMode `标记所含有的配送方式进行配送。 + +- `deliveryTemplateId` 运费模板id,根据不同的运费模板设计不同的配送费 + + ![img](./img/配送与运费模板.png) + + 运费模板的操作见 :运费模板的设计相关文章。 + + +## 2.2 Sku + +商品 SKU 。 + +![img](./img/sku.png) + +```java +@Data +@TableName("tz_sku") +public class Sku implements Serializable { + /** + * 单品ID + */ + @TableId + private Long skuId; + + /** + * 商品ID + */ + private Long prodId; + + /** + * 销售属性组合字符串,格式是p1:v1;p2:v2 + */ + private String properties; + + /** + * 原价 + */ + private Double oriPrice; + + /** + * 价格 + */ + private Double price; + + /** + * 库存 + */ + private Integer stocks; + + /** + * 实际库存 + */ + private Integer actualStocks; + + /** + * 修改时间 + */ + private Date updateTime; + + /** + * 记录时间 + */ + private Date recTime; + + /** + * 商家编码 + */ + private String partyCode; + + /** + * 商品条形码 + */ + private String modelId; + + /** + * sku图片 + */ + private String pic; + + /** + * sku名称 + */ + private String skuName; + + /** + * 商品名称 + */ + private String prodName; + + /** + * 重量 + */ + private Double weight; + + /** + * 体积 + */ + private Double volume; + + /** + * 状态:0禁用 1 启用 + */ + private Integer status; + + /** + * 0 正常 1 已被删除 + */ + private Integer isDelete; + +} +``` + +- `skuId` ,SKU 编号,自增,唯一,参见分销场景。 + +- `prodId` ,商品编号,N:1 指向对应的 Product 。 + +- `status`,SKU 状态。编辑商品时,当禁用该sku时,前端将会将该sku置灰 + +- `stocks` ,库存数量。 + +- `properties`,商品规格,字符串拼接格式。 + + 绝大多数情况下,数据库里的该字段,不存在检索的需求,更多的时候,是查询整体记录,在内存中解析使用。 + + 少部分情况,灵活的检索,使用 Elasticsearch 进行解决。 + + 因为我们的规格是直接保存字符串的,所以可以选择,或直接输入 + + ![img](./img/规格选择.png)![img](./img/规格可以输入.png) + + + +## 2.3 ProdProp + +商品 SKU 规格属性,在数据库中保存的常用数据。不常用的数据可以直接手动输入即可。 + +![img](./img/规格属性.png) + +```java +public class ProdProp implements Serializable { + /** + * 属性id + */ + @TableId + private Long propId; + + /** + * 属性名称 + */ + private String propName; + + private Long shopId; +} + +``` + +- `propId` ,属性编号。 +- `propName` ,属性名称。 + + + +## 2.4 ProdPropValue + +商品 SKU 规格属性,在数据库中保存的常用数据。 + +```java +public class ProdProp implements Serializable { + /** + * 属性值ID + */ + @TableId + private Long valueId; + + /** + * 属性值名称 + */ + private String propValue; + + /** + * 属性ID + */ + private Long propId; +} + +``` + +- `valueId` ,属性值ID。 +- `propValue` ,属性值名称。 diff --git a/doc/基本框架设计/一对多、多对多分页.md b/doc/基本框架设计/一对多、多对多分页.md new file mode 100644 index 0000000..f7b529b --- /dev/null +++ b/doc/基本框架设计/一对多、多对多分页.md @@ -0,0 +1,43 @@ +使用`mybatis plus` 进行分页的时候,是无法进行一对多、多对多的分页的。最主要的原因是因为,该框架无法清楚count的依据是什么,以哪个表算出来的行数为准,但是我们所有的分页格式已经统一好使用`IPage`对象了,那么该如何适配一对多、多对多分页呢? + + + +## PageAdapter + +使用分页时,前端传入的数据统一格式为`current`当前页,`size`每页大小。而我们在数据库中要将这两个数据变更为从第几行到第几行,所以我们需要简单的适配一下: + +```java +@Data +public class PageAdapter{ + + private int begin; + + private int end; + + public PageAdapter(Page page) { + int[] startEnd = PageUtil.transToStartEnd((int) page.getCurrent(), (int) page.getSize()); + this.begin = startEnd[0]; + this.end = startEnd[1]; + } +} +``` + + + +## Count + +在使用`mybatis plus` 进行分页的时候,该工具会自动为我们编写count的sql,而一对多进行分页时如: + +1个订单有5个订单项,在使用`mybatis plus` 生成的`count sql` 会认为每行都是一条数据,导致最后认为会有5条订单信息,实际上应该只有1条订单信息。这个时候我们必须自己手写`count sql`,并区分`records sql`。 + +具体例子可以查看`OrderServiceImpl` + +```java +@Override +public IPage pageOrdersDetialByOrderParam(Page page, OrderParam orderParam) { + page.setRecords(orderMapper.listOrdersDetialByOrderParam(new PageAdapter(page), orderParam)); + page.setTotal(orderMapper.countOrderDetial(orderParam)); + return page; +} +``` + diff --git a/doc/基本框架设计/分布式锁.md b/doc/基本框架设计/分布式锁.md new file mode 100644 index 0000000..2795056 --- /dev/null +++ b/doc/基本框架设计/分布式锁.md @@ -0,0 +1,117 @@ +在小程序登陆的时候,在`MiniAppAuthenticationProvider`中我们看到这样一行代码 + +```java +yamiUserDetailsService.insertUserIfNecessary(appConnect); +``` + +这便是商城用户创建的代码,在`YamiUserServiceImpl#insertUserIfNecessary()`方法中,有一个这样的注解 + +```java +@RedisLock(lockName = "insertUser", key = "#appConnect.appId + ':' + #appConnect.bizUserId") +``` + +这里便用了分布式锁,为什么我们要在这里使用锁?分布式锁又是什么? + +- 由于用户是通过登录直接注册的,如果一个用户在不刻意之间,又或者前端写的东西有点问题,这就会导致整个系统创建了两个相同的用户,这是非常危险的事情,所以创建用户这里必须加锁。 +- 至于为什么使用分布式锁,是因为我们虽然没有用上spring cloud、dubbo之类的东西,实际上我们也是希望我们的商城可以多实例部署的,也就是可以搞分布式的。因此用了分布式锁 + +分布式锁,简单来说就是锁,而且还是适合分布式环境的。分布式说起来也很奇怪,要是有什么不能共享的东西,那就抽出来共享。比如本地数据缓存不能共享,那么就抽出一个如redis之类的东西,进行共享。session不能共享,那么就将session抽出来,丢到redis之类的东西,又能共享了。 + +锁不能共享,同样可以丢一个标记到redis,由于redis是单线程的,所以也不用担心redis的线程安全的问题。这个标记就是一个锁的标记,那样你就实现了分布式锁... + +我们看回`@RedisLock` 该类,里面有个`expire()`方法 + +```java + /** + * 过期毫秒数,默认为5000毫秒 + * + * @return 锁的时间 + */ + int expire() default 5000; +``` + +由于网络稳定、宕机等各种原因,分布式锁,必须要有过期时间,否则锁无法释放的话,会阻塞一片的实例。 + +## 实现一个简单的分布式锁注解 + +由于自己去实现redis的分布式锁,是比较困难的问题,还要考虑redis复制,宕机之类的问题,所以我们使用一个比较优秀的开源项目 **redisson**来实现我们的分布式锁 + +被`@RedisLock`所注解的方法,会被 `RedisLockAspect` 进行切面管理,代码如下: + +```java + @Around("@annotation(redisLock)") + public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable { + String spel = redisLock.key(); + String lockName = redisLock.lockName(); + // redissonClient 也就是通过redisson 进行对锁管理 + RLock rLock = redissonClient.getLock(getRedisKey(joinPoint,lockName,spel)); + + rLock.lock(redisLock.expire(),redisLock.timeUnit()); + + Object result = null; + try { + //执行方法 + result = joinPoint.proceed(); + + } finally { + rLock.unlock(); + } + return result; + } +``` + +## 识别spel表达式 + +在`@RedisLock(lockName = "insertUser", key = "#appConnect.appId + ':' + #appConnect.bizUserId")`中 `#appConnect.appId` 也仅仅是表示一串字符串而已,而能将其变成表达式,需要一定的转换`SpelUtil.parse` + +```java + /** + * 支持 #p0 参数索引的表达式解析 + * @param rootObject 根对象,method 所在的对象 + * @param spel 表达式 + * @param method ,目标方法 + * @param args 方法入参 + * @return 解析后的字符串 + */ + public static String parse(Object rootObject,String spel, Method method, Object[] args) { + if (StrUtil.isBlank(spel)) { + return StrUtil.EMPTY; + } + //获取被拦截方法参数名列表(使用Spring支持类库) + StandardReflectionParameterNameDiscoverer standardReflectionParameterNameDiscoverer = new StandardReflectionParameterNameDiscoverer(); + String[] paraNameArr = standardReflectionParameterNameDiscoverer.getParameterNames(method); + if (ArrayUtil.isEmpty(paraNameArr)) { + return spel; + } + //使用SPEL进行key的解析 + ExpressionParser parser = new SpelExpressionParser(); + //SPEL上下文 + StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,standardReflectionParameterNameDiscoverer); + //把方法参数放入SPEL上下文中 + for (int i = 0; i < paraNameArr.length; i++) { + context.setVariable(paraNameArr[i], args[i]); + } + return parser.parseExpression(spel).getValue(context, String.class); + } +``` + +同时我们也害怕redis的key发生冲突,所以会对key加上一些统一的前缀: + +redis 锁的key能够识别`spel` 表达式,并且不和其他方法的锁名称或缓存名称重复 + +```java +/** + * 将spel表达式转换为字符串 + * @param joinPoint 切点 + * @return redisKey + */ +private String getRedisKey(ProceedingJoinPoint joinPoint,String lockName,String spel) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + Method targetMethod = methodSignature.getMethod(); + Object target = joinPoint.getTarget(); + Object[] arguments = joinPoint.getArgs(); + return REDISSON_LOCK_PREFIX + lockName + StrUtil.COLON + SpelUtil.parse(target,spel, targetMethod, arguments); +} +``` + diff --git a/doc/基本框架设计/对xss攻击的防御.md b/doc/基本框架设计/对xss攻击的防御.md new file mode 100644 index 0000000..f9f9520 --- /dev/null +++ b/doc/基本框架设计/对xss攻击的防御.md @@ -0,0 +1,129 @@ +身为服务器的开发者,我们是无法相信用户输入的任何东西的。比如:金额不能从前端传过来,使用会失效的token等。当然,用户除了会传入一些假数据,也会传入一些假的脚本,比较出名的就是**xss攻击** + +网上有很多说解决xss攻击的方法,有很多都是和前端有关,而实际上,在后台这最后一个防御当中,是最为重要的。 + +在mall4j这个项目里面,使用了一个过滤器 `XssFilter` + +``` +public class XssFilter implements Filter { + Logger logger = LoggerFactory.getLogger(getClass().getName()); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException{ + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + + + logger.info("uri:{}",req.getRequestURI()); + // xss 过滤 + chain.doFilter(new XssWrapper(req), resp); + } +} + +``` + +主要是通过 `new XssWrapper(req)` 这个对象进行一系列的过滤,而 `XssWrapper` 是通过`Jsoup`进行用户输入的一系列过滤。毕竟专业的事情要交给专业的人来搞定。就此,我们通过简单的设置就完成了对**xss攻击**的防御。 + +```java +public class XssWrapper extends HttpServletRequestWrapper { + /** + * Constructs a request object wrapping the given request. + * + * @param request The request to wrap + * @throws IllegalArgumentException if the request is null + */ + public XssWrapper(HttpServletRequest request) { + super(request); + } + + /** + * 对数组参数进行特殊字符过滤 + */ + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (values == null) { + return null; + } + int count = values.length; + String[] encodedValues = new String[count]; + for (int i = 0; i < count; i++) { + encodedValues[i] = cleanXSS(values[i]); + } + return encodedValues; + } + + /** + * 对参数中特殊字符进行过滤 + */ + @Override + public String getParameter(String name) { + String value = super.getParameter(name); + if (StrUtil.isBlank(value)) { + return value; + } + return cleanXSS(value); + } + + /** + * 获取attribute,特殊字符过滤 + */ + @Override + public Object getAttribute(String name) { + Object value = super.getAttribute(name); + if (value instanceof String && StrUtil.isNotBlank((String) value)) { + return cleanXSS((String) value); + } + return value; + } + + /** + * 对请求头部进行特殊字符过滤 + */ + @Override + public String getHeader(String name) { + String value = super.getHeader(name); + if (StrUtil.isBlank(value)) { + return value; + } + return cleanXSS(value); + } + + private String cleanXSS(String value) { + return XssUtil.clean(value); + } +} + +``` + +这里面最主要的方法就是`XssUtil.clean(value)` -> `Jsoup.clean(content, "", WHITE_LIST, OUTPUT_SETTINGS)` 这面最总要的是有个白名单列表 `WHITE_LIST` 来自,我们仔细观察白名单列表会发现这里面是部分携带html的部分标签进入,从而防止xss攻击 + +```java +new Whitelist().addTags( + "a", "b", "blockquote", "br", "caption", "cite", "code", "col", + "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", + "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", + "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", + "ul") + + .addAttributes("a", "href", "title") + .addAttributes("blockquote", "cite") + .addAttributes("col", "span", "width") + .addAttributes("colgroup", "span", "width") + .addAttributes("img", "align", "alt", "height", "src", "title", "width") + .addAttributes("ol", "start", "type") + .addAttributes("q", "cite") + .addAttributes("table", "summary", "width") + .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width") + .addAttributes( + "th", "abbr", "axis", "colspan", "rowspan", "scope", + "width") + .addAttributes("ul", "type") + + .addProtocols("a", "href", "ftp", "http", "https", "mailto") + .addProtocols("blockquote", "cite", "http", "https") + .addProtocols("cite", "cite", "http", "https") + .addProtocols("img", "src", "http", "https") + .addProtocols("q", "cite", "http", "https") +``` + diff --git a/doc/基本框架设计/文件上传下载.md b/doc/基本框架设计/文件上传下载.md new file mode 100644 index 0000000..aec0d6b --- /dev/null +++ b/doc/基本框架设计/文件上传下载.md @@ -0,0 +1,226 @@ +# 上传下载 + +我们对文件上传进行了分别封装了多个组件: + +- 单图片上传(替换图片):`src\components\pic-upload` + +- 多图片上传:`src\components\mul-pic-upload` +- 文件上传:`src\components\file-upload` + +上述这些文件上传,都是基于`el-upload`进行封装 + + + +## 单图片上传 + +在商品分类这个模块的弹框中可以找到单图片上传的例子,对应vue代码位置:`src\views\modules\category-add-or-update.vue` + +html: + +```html + +``` + +js: + +```javascript +import PicUpload from '@/components/pic-upload' +export default { + data () { + return { + dataForm: { + pic: '' + } + }, + components: { + PicUpload + } +} +``` + +这里的文件上传使用起来非常简单,只需要将最终文件上传完成后的路径进行双向绑定即可 + + + +## 多图片上传 + +在商品发布这个模块的中可以找到多图片上传的例子,对应vue代码位置:`src\views\modules\category-add-or-update.vue` + +html: + +```html + +``` + +js: + +```javascript +import MulPicUpload from '@/components/mul-pic-upload' +export default { + data () { + return { + dataForm: { + imgs: '' + } + }, + components: { + MulPicUpload + } +} +``` + +这里的文件上传使用起来也非常简单,最后返回的数据,为以逗号分隔的图片路径连接的字符串 + + + +## 服务端代码 + +直接的文件上传的例子与多图片上传的例子类似,这里便不一一举例了。 + +我们可以查看三个文件上传的源码,都有那么两句话`:action="$http.adornUrl('/admin/file/upload/element')"` `:headers="{Authorization: $cookie.get('Authorization')}"`,其中由于规定后台所有请求都需要通过 `spring security`的授权,所以需要携带通用请求头`headers`,而`action`则是对应后台服务器的路径 + + + +我们查看后台`FileController` 这里对文件上传的接口进行了统一的管理: + +```java +@RestController +@RequestMapping("/admin/file") +public class FileController { + + @Autowired + private AttachFileService attachFileService; + + @PostMapping("/upload/element") + public ServerResponseEntity uploadElementFile(@RequestParam("file") MultipartFile file) throws IOException{ + if(file.isEmpty()){ + return ServerResponseEntity.success(); + } + String fileName = attachFileService.uploadFile(file.getBytes(),file.getOriginalFilename()); + return ServerResponseEntity.success(fileName); + } + + +} +``` + + + +同时我们查看`attachFileService` 的实现类,可以知道该文件上传是通过七牛云进行实现的 + +```java +@Service +public class AttachFileServiceImpl extends ServiceImpl implements AttachFileService { + + @Autowired + private AttachFileMapper attachFileMapper; + + @Autowired + private UploadManager uploadManager; + + @Autowired + private BucketManager bucketManager; + @Autowired + private Qiniu qiniu; + + @Autowired + private Auth auth; + + public final static String NORM_MONTH_PATTERN = "yyyy/MM/"; + + @Override + public String uploadFile(byte[] bytes,String originalName) throws QiniuException { + String extName = FileUtil.extName(originalName); + String fileName =DateUtil.format(new Date(), NORM_MONTH_PATTERN)+ IdUtil.simpleUUID() + "." + extName; + + + AttachFile attachFile = new AttachFile(); + attachFile.setFilePath(fileName); + attachFile.setFileSize(bytes.length); + attachFile.setFileType(extName); + attachFile.setUploadTime(new Date()); + attachFileMapper.insert(attachFile); + + String upToken = auth.uploadToken(qiniu.getBucket(),fileName); + Response response = uploadManager.put(bytes, fileName, upToken); + Json.parseObject(response.bodyString(), DefaultPutRet.class); + return fileName; + } +} +``` + + + +在这里面注入了非常多的七牛云的配置,而配置文件的来源,来自 + +```java +@Configuration +public class FileUploadConfig { + + + @Autowired + private Qiniu qiniu; + + /** + * 华南机房 + */ + @Bean + public com.qiniu.storage.Configuration qiniuConfig() { + return new com.qiniu.storage.Configuration(Zone.zone2()); + } + + /** + * 构建一个七牛上传工具实例 + */ + @Bean + public UploadManager uploadManager() { + return new UploadManager(qiniuConfig()); + } + + /** + * 认证信息实例 + * @return + */ + @Bean + public Auth auth() { + return Auth.create(qiniu.getAccessKey(), qiniu.getSecretKey()); + } + + /** + * 构建七牛空间管理实例 + */ + @Bean + public BucketManager bucketManager() { + return new BucketManager(auth(), qiniuConfig()); + } +} +``` + + + +## 注册七牛云账号 + +现在已经9102年了,很少上传文件到本地了,一般都是上传到oss,我们这里选择[七牛云存储](https://www.qiniu.com/products/kodo) ,如果没有账号的可以注册一个,创建一个华南地区的云存储空间 + +![img](https://box.kancloud.cn/c72238c384fb43c2c0b3161162880056_1909x545.png) + +### 修改后台配置 + +平台端(vue)修改文件`.env.production`(生产环境)/ `.env.development`(开发环境) +里面的`VUE_APP_BASE_API`为api接口请求地址, `VUE_APP_RESOURCES_URL`为静态资源文件url + + // api接口请求地址 + VUE_APP_BASE_API = 'http://127.0.0.1:8085' + // 静态资源文件url + VUE_APP_RESOURCES_URL = 'https://img.mall4j.com/' + + +### 更新于2023.03.27 本地上传配置 +- 在`shop.properties` 更新了本地上传的配置,修改`shop.imgUpload.uploadType=1`,文件上传类型为1是使用本地上传,vue中的`resourcesUrl`也配置对应的本地路径 +- 全局搜索`/mall4j/img`,替换成你想要的图片路径,若按本代码默认的则前端的资源路径为`http://ip: + admin服务的端口号/mall4j/img/` + + +### 更新于2024.09.14 七牛云配置 +- 在`shop.properties` 中,修改`shop.imgUpload.uploadType=2`,文件上传类型为2是使用七牛云,vue中的`resourcesUrl`配置对应的`shop.qiniu.resourcesUrl`, +- 分别将`shop.qiniu.accessKey`、`shop.qiniu.secretKey`、`shop.qiniu.bucket`、`shop.qiniu.zone`、`shop.qiniu.resourcesUrl` +替换成你的七牛云配置,最后前端的资源路径为`shop.qiniu.resourcesUrl` diff --git a/doc/基本框架设计/权限管理.md b/doc/基本框架设计/权限管理.md new file mode 100644 index 0000000..a8456ba --- /dev/null +++ b/doc/基本框架设计/权限管理.md @@ -0,0 +1,151 @@ +## 权限控制 + +#### 前端权限控制 + +在商城运营时,我们可能是多个人员共同操作我们的系统,但是每个操作人员所具备的权限应该不同,权限的不同主要表现在两个部分,即导航菜单的查看权限和页面增删改操作按钮的操作权限。我们的把页面导航菜单查看权限和页面操作按钮统一存储在菜单数据库表中,菜单类型页面资源的类型。类型包括目录 、菜单 、按钮。 + +#### 权限标识 + +权限标识用来进行权限控制的唯一标识,主要是进行增删改查的权限控制。 + +权限标识包括:新增 编辑 删除 查看等,格式结构类似**xxx:xxx:xxx** 如:**admin:user:update**。 + +#### 导航菜单权限流程 + +用户登录之后,跳转至首页,前端发送请求到后台获取该用户下的所有菜单权限与认证权限数据,认证权限为约束用户增删改查操作,在路由导航守卫路由时加载用户导航菜单并存储到本地存储中。导航栏从本地存储读取菜单列表并进行渲染。 + +#### 页面按钮权限实现 + +用户登录系统之后,跳转到首页,在路由导航守卫路由时加载用户权限标识集合。返回结果是用户权限标识的集合,页面操作按钮提供权限标识,查询该权限标识是否在用户权限标识集合中,如有存在,则将按钮为可见状态,如不存在,则将按钮为不可见状态,根据需求,也可以设置成禁用状态。 + +#### 加载导航菜单权限与页面按钮权限数据 + +##### 动态路由与导航栏 + +在`router/index.js`中,从后台加载导航菜单、页面按钮权限数据,并将数据保存到本地存储中,如下所示: + +```javascript +router.beforeEach((to, from, next) => { + // 添加动态(菜单)路由 + if (router.options.isAddDynamicMenuRoutes || fnCurrentRouteType(to, globalRoutes) === 'global') { + next() + } else { + http({ + url: http.adornUrl('/sys/menu/nav'), + method: 'get', + params: http.adornParams() + }).then(({ data }) => { + sessionStorage.setItem('authorities', JSON.stringify(data.authorities || '[]')) + fnAddDynamicMenuRoutes(data.menuList) + router.options.isAddDynamicMenuRoutes = true + sessionStorage.setItem('menuList', JSON.stringify(data.menuList || '[]')) + next({ ...to, replace: true }) + }).catch((e) => { + console.log(`%c${e} 请求菜单列表和权限失败,跳转至登录页!!`, 'color:blue') + router.push({ name: 'login' }) + }) + } +}) +``` + +通过`fnAddDynamicMenuRoutes()`方法,动态加载菜单到路由中保存到本地存储`sessionStorage`中。但是现在只有路由,还需要将导航栏展示出来。在`main-sidebar.vue`中,我们将本地存储中菜单数据取出来,然后对导航栏动态渲染出来,并通过**menuId**与动态(菜单)路由进行匹配跳转至指定路由,这样,当我们点击菜单的时候,就会跳转至特定的路由。 + +```javascript + created () { + this.menuList = JSON.parse(sessionStorage.getItem('menuList') || '[]') + this.dynamicMenuRoutes = JSON.parse(sessionStorage.getItem('dynamicMenuRoutes') || '[]') + this.routeHandle(this.$route) + } + + +``` + +`sub-menu`组件的部分代码 + +```html + +``` + +##### 按钮权限 + +在组件中根据外部方法传入的权限标识进行权限判断,如果权限存在,则显示为可见状态,否则不可见。 + +```html + 新增 +``` + +通过`isAuth(“权限标识”)`,判断按钮是否有相同的标识,如果有则可见,否则不可见 + +```javascript +/** + * 是否有权限 + * @param {*} key + */ +export function isAuth (key) { + let authorities = JSON.parse(sessionStorage.getItem('authorities') || '[]') + if (authorities.length) { + for (const i in authorities) { + const element = authorities[i] + if (element.authority === key) { + return true + } + } + } + return false +} +``` + +注:后台通过`@PreAuthorize("@pms.hasPermission('admin:user:update')")`来定义请求所需要的权限,如果用户没有该权限,后台就会抛出401未授权状态码,前端捕获到该状态码后,会登出当前的账号,让用户重新登陆。 +![img](https://box.kancloud.cn/627e371fbaf45d74782a99fb888026ec_546x519.png) + +#### 后台菜单管理、角色管理与管理员列表 + +##### 菜单管理 + +在【系统管理】-【菜单管理】中,我们可以通过类配置的方式,更直观的对菜单列表增删改查进行管理。 + +菜单类型包括目录 、菜单 、按钮。 + +目录为导航栏的大的分类,菜单为分类下的每一项,每个菜单需要绑定上级及填写对应跳转的路由,路由路径对应工程的目录如下图: +![img](https://box.kancloud.cn/fc23477a687599f9e01dee8f45f3b161_706x608.png) +在新增按钮权限时,注意授权标识要与后台一致,新增完之后需要重启刷新生效。 +![img](https://box.kancloud.cn/6ffabbbbf2b4f641126dd946f5d79f99_699x432.png) + +![img](https://box.kancloud.cn/0e22aebd4c42f3a48e2bf3a7b4e43548_770x82.png) + +##### 角色管理 + +在【系统管理】-【角色管理】中,管理员可以新增角色,并且赋予该角色可以访问的权限项。 +![img](https://box.kancloud.cn/2da280c4cb37ac290875afba404c15be_1374x959.png) + +##### 管理员管理 + +在【系统管理】- 【管理员列表】中,拥有该权限的管理员可以对其进行管理,该管理员可添加或修改管理权限,并可分配列表中的用户角色。 +![img](https://box.kancloud.cn/6e086f97d7d3adfa80cb1d25ff078c9e_677x429.png) \ No newline at end of file diff --git a/doc/基本框架设计/统一异常处理.md b/doc/基本框架设计/统一异常处理.md new file mode 100644 index 0000000..0f27c10 --- /dev/null +++ b/doc/基本框架设计/统一异常处理.md @@ -0,0 +1,214 @@ +## 后台异常处理 + +在开发过程中,不可避免的是需要处理各种异常,异常处理方法随处可见,所以代码中就会出现大量的`try {...} catch {...} finally {...}` 代码块,不仅会造成大量的冗余代码,而且还影响代码的可读性,所以对异常统一处理非常有必要。为此,我们定义了一个统一的异常类`YamiShopBindException` 与异常管理类 `DefaultExceptionHandlerConfig`。 + +我们先来看下 `YamiShopBindException`的代码 + +```java +@Getter +public class YamiShopBindException extends RuntimeException{ + + /** + * + */ + private static final long serialVersionUID = -4137688758944857209L; + + /** + * http状态码 + */ + private String code; + + private Object object; + + private ServerResponseEntity serverResponseEntity; + + public YamiShopBindException(ResponseEnum responseEnum) { + super(responseEnum.getMsg()); + this.code = responseEnum.value(); + } + /** + * @param responseEnum + */ + public YamiShopBindException(ResponseEnum responseEnum, String msg) { + super(msg); + this.code = responseEnum.value(); + } + + public YamiShopBindException(ServerResponseEntity serverResponseEntity) { + this.serverResponseEntity = serverResponseEntity; + } + + + public YamiShopBindException(String msg) { + super(msg); + this.code = ResponseEnum.SHOW_FAIL.value(); + } + + public YamiShopBindException(String msg, Object object) { + super(msg); + this.code = ResponseEnum.SHOW_FAIL.value(); + this.object = object; + } + +} +``` + +`ResponseEnum`为我们自定义的返回状态码的枚举类,定义为一个枚举类,更直观处理异常返回的状态码及异常内容,以后每增加一种异常情况,只需增加一个枚举实例即可,不用每一种异常都定义一个异常类。 + +```java +public enum ResponseEnum { + + /** + * ok + */ + OK("00000", "ok"), + SHOW_FAIL("A00001", ""), + + /** + * 用于直接显示提示用户的错误,内容由输入内容决定 + */ + + /** + * 用于直接显示提示系统的成功,内容由输入内容决定 + */ + SHOW_SUCCESS("A00002", ""), + + /** + * 未授权 + */ + UNAUTHORIZED("A00004", "Unauthorized"), + + /** + * 服务器出了点小差 + */ + EXCEPTION("A00005", "服务器出了点小差"), + /** + * 方法参数没有校验,内容由输入内容决定 + */ + METHOD_ARGUMENT_NOT_VALID("A00014", "方法参数没有校验"); + + private final String code; + + private final String msg; + + public String value() { + return code; + } + + public String getMsg() { + return msg; + } + + ResponseEnum(String code, String msg) { + this.code = code; + this.msg = msg; + } + + @Override + public String toString() { + return "ResponseEnum{" + "code='" + code + '\'' + ", msg='" + msg + '\'' + "} " + super.toString(); + } + +} +``` + +再来看看 `DefaultExceptionHandlerConfig`类 + +```java +@Slf4j +@RestController +@RestControllerAdvice +public class DefaultExceptionHandlerConfig { + + @ExceptionHandler({ MethodArgumentNotValidException.class, BindException.class }) + public ResponseEntity>> methodArgumentNotValidExceptionHandler(Exception e) { + log.error("methodArgumentNotValidExceptionHandler", e); + List fieldErrors = null; + if (e instanceof MethodArgumentNotValidException) { + fieldErrors = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors(); + } + if (e instanceof BindException) { + fieldErrors = ((BindException) e).getBindingResult().getFieldErrors(); + } + if (fieldErrors == null) { + return ResponseEntity.status(HttpStatus.OK) + .body(ServerResponseEntity.fail(ResponseEnum.METHOD_ARGUMENT_NOT_VALID)); + } + + List defaultMessages = new ArrayList<>(fieldErrors.size()); + for (FieldError fieldError : fieldErrors) { + defaultMessages.add(fieldError.getField() + ":" + fieldError.getDefaultMessage()); + } + return ResponseEntity.status(HttpStatus.OK) + .body(ServerResponseEntity.fail(ResponseEnum.METHOD_ARGUMENT_NOT_VALID, defaultMessages)); + } + + @ExceptionHandler(YamiShopBindException.class) + public ResponseEntity> unauthorizedExceptionHandler(YamiShopBindException e){ + log.error("mall4jExceptionHandler", e); + + ServerResponseEntity serverResponseEntity = e.getServerResponseEntity(); + if (serverResponseEntity!=null) { + return ResponseEntity.status(HttpStatus.OK).body(serverResponseEntity); + } + // 失败返回消息 状态码固定为直接显示消息的状态码 + return ResponseEntity.status(HttpStatus.OK).body(ServerResponseEntity.fail(e.getCode(),e.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> exceptionHandler(Exception e){ + log.error("exceptionHandler", e); + return ResponseEntity.status(HttpStatus.OK).body(ServerResponseEntity.fail(ResponseEnum.EXCEPTION)); + } +} +``` + + + +## 前台异常处理 + + + +前端请求与相应做了封装,请求响应的内容会被拦截器所拦截,当后台返回给前台特定的状态码,前台将显示不同报错信息。请求响应非常常见,我们查看在`src\utils\httpRequest.js`里面的其中一段代码 + + + +```javascript +http.interceptors.response.use(response => { + return response +}, error => { + switch (error.response.status) { + case 400: + Message.error(error.response.data) + break + case 401: + clearLoginInfo() + router.push({ name: 'login' }) + break + case 405: + Message.error('http请求方式有误') + break + case 500: + Message.error('服务器出了点小差,请稍后再试') + break + case 501: + Message.error('服务器不支持当前请求所需要的某个功能') + break + } + return Promise.reject(error) +}) +``` + +这里将会统一拦截返回的状态码如`400`,进行错误提示。 + + + +## RESTful 风格 + +我们的上述代码使用http状态码对请求进行统一响应,其中最大的 + +RESTful架构,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。 + + + +[RESTful概述](https://blog.igevin.info/posts/restful-architecture-in-general/) diff --git a/doc/基本框架设计/统一的系统日志.md b/doc/基本框架设计/统一的系统日志.md new file mode 100644 index 0000000..aebbc9d --- /dev/null +++ b/doc/基本框架设计/统一的系统日志.md @@ -0,0 +1,90 @@ +## 系统日志 + +利用`spring`框架中`aop`,我们可以实现业务代码与系统级服务进行解耦,例如日志记录、事务及其他安全业务等,可以使得我们的工程更加容易维护、优雅。如何在系统中添加相应的日志呢? + +##### 添加依赖 + +``` + + org.springframework.boot + spring-boot-starter-aop + +``` + +##### 自定义注解 + +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SysLog { + String value() default ""; +} + +``` + +##### 配置切面 + +```java +@Aspect +@Component +public class SysLogAspect { + @Autowired + private SysLogService sysLogService; + private static Logger logger = LoggerFactory.getLogger(SysLogAspect.class); + + @Around("@annotation(sysLog)") + public Object around(ProceedingJoinPoint joinPoint,com.yami.shop.common.annotation.SysLog sysLog) throws Throwable { + long beginTime = SystemClock.now(); + //执行方法 + Object result = joinPoint.proceed(); + //执行时长(毫秒) + long time = SystemClock.now() - beginTime; + + SysLog sysLogEntity = new SysLog(); + if(sysLog != null){ + //注解上的描述 + sysLogEntity.setOperation(sysLog.value()); + } + + //请求的方法名 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + sysLogEntity.setMethod(className + "." + methodName + "()"); + + //请求的参数 + Object[] args = joinPoint.getArgs(); + String params = Json.toJsonString(args[0]); + sysLogEntity.setParams(params); + + //设置IP地址 + sysLogEntity.setIp(IPHelper.getIpAddr()); + //用户名 + String username = SecurityUtils.getSysUser().getUsername(); + sysLogEntity.setUsername(username); + sysLogEntity.setTime(time); + sysLogEntity.setCreateDate(new Date()); + //保存系统日志 + sysLogService.save(sysLogEntity); + return result; + } + +} +``` + +将自定义的注解作为切入点,参数是`ProceedingJoinPoint`和`sysLog`,`ProceedingJoinPoint`用来获取当前执行的方法,`syslog`用来获取注解里面的值。 + +#### 在需要记录日志的方法上,添加注解`@SysLog(value)` + +```java +@SysLog("修改角色") +@PutMapping +@PreAuthorize("@pms.hasPermission('sys:role:update')") +public ServerResponseEntity update(@RequestBody SysRole role){ + sysRoleService.updateRoleAndRoleMenu(role); + return ServerResponseEntity.success(); +} +``` + +当操作这个方法时,将会被记录到数据库中,在日志管理中能看到相应操作的内容。 +![img](https://box.kancloud.cn/4ff625398e31974b7de6fe9e06c2b847_1373x202.png) diff --git a/doc/基本框架设计/统一验证.md b/doc/基本框架设计/统一验证.md new file mode 100644 index 0000000..08aa9d9 --- /dev/null +++ b/doc/基本框架设计/统一验证.md @@ -0,0 +1,139 @@ +我们后台使用`spring` 为我们提供好的统一校验的工具`spring-boot-starter-validation`对请求进行校验。 + +```xml + + org.springframework.boot + spring-boot-starter-validation + +``` + +这里通过注解封装了几种常用的校验 + +- `@NotNull` 不能为null +- `@NotEmpty` 不能为null、空字符串、空集合 +- `@NotBlank` 不能为null、空字符串、纯空格的字符串 +- `@Min` 数字最小值不能小于x +- `@Max` 数字最大值不能大于x +- `@Email` 字符串为邮件格式 +- `@Max` 数字最大值不能大于x +- `@Size` 字符串长度最小为x、集合长度最小为x +- `@Pattern` 正则表达式 + + + +我们以`SysUser`为例,看看怎么使用 + +```java +public class SysUser implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 用户ID + * + */ + @TableId + private Long userId; + + /** + * 用户名 + */ + @NotBlank(message="用户名不能为空") + @Size(min = 2,max = 20,message = "用户名长度要在2-20之间") + private String username; + + /** + * 密码 + */ + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String password; + + /** + * 邮箱 + */ + @NotBlank(message="邮箱不能为空") + @Email(message="邮箱格式不正确") + private String email; + + /** + * 手机号 + */ + @Pattern(regexp="0?1[0-9]{10}",message = "请输入正确的手机号") + private String mobile; + + /** + * 状态 0:禁用 1:正常 + */ + private Integer status; + + /** + * 用户所在店铺id + */ + private Long shopId; + + /** + * 角色ID列表 + */ + @TableField(exist=false) + private List roleIdList; + + /** + * 创建时间 + */ + private Date createTime; + +} +``` + + + +我们在Controller层使用该bean,并使用`@Valid`注解,使校验的注解生效,如`SysUserController` : + +```java +@RestController +@RequestMapping("/sys/user") +public class SysUserController { + /** + * 保存用户 + */ + @SysLog("保存用户") + @PostMapping + @PreAuthorize("@pms.hasPermission('sys:user:save')") + public ServerResponseEntity save(@Valid @RequestBody SysUser user){ + String username = user.getUsername(); + SysUser dbUser = sysUserService.getOne(new LambdaQueryWrapper() + .eq(SysUser::getUsername, username)); + if (dbUser!=null) { + return ServerResponseEntity.showFailMsg("该用户已存在"); + } + user.setShopId(SecurityUtils.getSysUser().getShopId()); + user.setPassword(passwordEncoder.encode(user.getPassword())); + sysUserService.saveUserAndUserRole(user); + return ServerResponseEntity.success(); + } +} +``` + + + +并且在`DefaultExceptionHandlerConfig` 拦截由`@Valid` 触发的异常信息并返回: + +```java +@RestController +@RestControllerAdvice +public class DefaultExceptionHandlerConfig { + + @ExceptionHandler(BindException.class) + public ServerResponseEntity bindExceptionHandler(BindException e){ + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getBindingResult().getFieldErrors().get(0).getDefaultMessage()); + + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ServerResponseEntity methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e){ + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getBindingResult().getFieldErrors().get(0).getDefaultMessage()); + } +} +``` + diff --git a/doc/基本框架设计/通用分页表格.md b/doc/基本框架设计/通用分页表格.md new file mode 100644 index 0000000..6f5609c --- /dev/null +++ b/doc/基本框架设计/通用分页表格.md @@ -0,0 +1,154 @@ +## 通用分页表格实现 + +前端基于VUE的轻量级表格插件 `avue` +后端分页组件使用Mybatis分页插件 `MybatisPlus` + + + +> 分页实现流程,以【系统管理-管理员列表】为例 + +后台vue文件位置目录 `\src\views\modules\sys\user.vue` + +1、`avue`组件的几个通用配置 + +```html + + +``` + +`avue`定义了很多的事件,其中一个为 `@on-load`当该组件加载的时候,将会调用该方法。同时也对很多数据进行了双向绑定如:`:page="page"` 分页参数、`:data="dataList"` 分页的具体列表数据、`:option="tableOption"` 表格显示的列 + + + +2、通用的列表、搜索 + +在`avue`规定,表格的构建,是通过JS对象,进行配置的,而不是通过dom,类似于传统的layui,还有一个主要的原因是这个表格,可以同时生成搜索、分页。 + +```javascript +import { tableOption } from '@/crud/sys/user' +``` + + + +我们查看下该类的代码: + +```javascript +export const tableOption = { + border: true, + selection: true, + index: false, + indexLabel: '序号', + stripe: true, + menuAlign: 'center', + menuWidth: 350, + align: 'center', + refreshBtn: true, + searchSize: 'mini', + addBtn: false, + editBtn: false, + delBtn: false, + viewBtn: false, + props: { + label: 'label', + value: 'value' + }, + column: [{ + label: '用户名', + prop: 'username', + search: true + }, { + label: '邮箱', + prop: 'email' + }, { + label: '手机号', + prop: 'mobile' + }, { + label: '创建时间', + prop: 'createTime' + }, { + label: '状态', + prop: 'status', + type: 'select', + dicData: [ + { + label: '禁用', + value: 0 + }, { + label: '正常', + value: 1 + } + ] + + }] +} +``` + +这里的 `search: true` 也就是搜索框出现用户名搜索 + +```javascript +{ + label: '用户名', + prop: 'username', + search: true +} +``` + +具体可以通过[avue官网-crud文档](https://avuejs.com/doc/crud/crud-doc)获取文档进行查询 + + + +3、 通用的搜索和加载 + +```javascript +getDataList (page, params) { + this.dataListLoading = true + this.$http({ + url: this.$http.adornUrl('/sys/user/page'), + method: 'get', + params: this.$http.adornParams( + Object.assign( + { + current: page == null ? this.page.currentPage : page.currentPage, + size: page == null ? this.page.pageSize : page.pageSize + }, + params + ) + ) + }).then(({ data }) => { + this.dataList = data.records + this.page.total = data.total + this.dataListLoading = false + }) +} +``` + + + +4、服务端`SysUserController` + +```java +@RestController +@RequestMapping("/sys/user") +public class SysUserController { + @Autowired + private SysUserService sysUserService; + /** + * 所有用户列表 + */ + @GetMapping("/page") + @PreAuthorize("@pms.hasPermission('sys:user:page')") + public ServerResponseEntity> page(String username,PageParam page){ + IPage sysUserPage = sysUserService.page(page, new LambdaQueryWrapper() + .eq(SysUser::getShopId, SecurityUtils.getSysUser().getShopId()) + .like(StrUtil.isNotBlank(username), SysUser::getUsername, username)); + + return ServerResponseEntity.success(sysUserPage); + } +} +``` diff --git a/doc/基本框架设计/项目目录结构.md b/doc/基本框架设计/项目目录结构.md new file mode 100644 index 0000000..6f3b316 --- /dev/null +++ b/doc/基本框架设计/项目目录结构.md @@ -0,0 +1,14 @@ +# 目录结构 + +~~~ +yami-shops +├── mall4m -- 小程序代码 +├── mall4v -- 后台vue代码 +├── yami-shop-admin -- 后台(vue)接口工程[8085] +├── yami-shop-api -- 前端(小程序)接口工程[8086] +├── yami-shop-bean -- 所有公共的实体类,商城基本流程所需的实体类 +├── yami-shop-common -- 前后台需要用到的公共配置,工具类等的集合地 +├── yami-shop-security -- oauth2.0 授权认证模块 +├── yami-shop-service -- 前后台需要用到的公共的、商城基本流程所需的service,dao的集合地 +├── yami-shop-sys -- 后台用户角色权限管理模块 +~~~ diff --git a/doc/常见问题.md b/doc/常见问题.md new file mode 100644 index 0000000..8a681bb --- /dev/null +++ b/doc/常见问题.md @@ -0,0 +1,14 @@ +这里整理了一些经常会被问到的问题: + +1. 为什么vue打包之后,或者修改url之后,无法登录? +答:你用chrome按f12看看console提示的信息如:`Access-Control-Allow-Origin` 那就是跨域了,再看看network的请求方法是不是`options`,但是返回不是200,这也是跨域了。 + +2. 跨域了怎么办? +跨域产生的原因是因为浏览器的同源策略,也就是说这个是浏览器的问题,你用`postman`去请求,都是没有问题,返回200的,浏览器才会出现这种奇怪的问题。要解决这个问题,就要清楚同源策略是啥,也就是浏览器认为:域名、协议、端口相同才是相同的源,也就是要想办法让前端的域名、协议、端口和接口的相同。而实际上前端和服务器怎么可以在一个端口呢?那就需要一些转发的工具,将同一个端口,不同路径的请求,转发到不同的端口,具体操作可以看 【生产环境nginx安装与跨域配置】 + +3. 上传图片后图片不显示 +检查前端代码中的VUE_APP_RESOURCES_URL配置是否为上传图片的地址 + +4. 前端登录显示无权限 +检查前端代码中配置的VUE_APP_BASE_API是否正确,h5页面配置的api服务的端口号,默认为8086 +后台vue页面配置的是admin服务的端口号,默认为8085 diff --git a/doc/接口设计/1. 购物车的设计.md b/doc/接口设计/1. 购物车的设计.md new file mode 100644 index 0000000..b092abf --- /dev/null +++ b/doc/接口设计/1. 购物车的设计.md @@ -0,0 +1,82 @@ +建议阅读前,先阅读《商城表设计-购物车》相关文档 + +我们的购物车只有一个表:`tz_basket` 非常简单,但是关联了非常多的表。比如: + +- 购物车有商品,关联商品表 +- 每个商品都有sku,关联sku表 +- 一个购物车有多个店铺的商品,关联店铺表 +- 一个购物车肯定是和用户有关的,关联用户表 + + + +我们对商品进行添加,修改,其实都很简单,最为让人难以理解的是如何将这些字段进行组合,关联满减满折等一系列的活动。 + +我们先来看下是如何获取商品信息的 + +```java + @PostMapping("/info") + @Operation(summary = "获取用户购物车信息" , description = "获取用户购物车信息,参数为用户选中的活动项数组,以购物车id为key") + public ServerResponseEntity> info(@RequestBody Map basketIdShopCartParamMap) { + String userId = SecurityUtils.getUser().getUserId(); + + // 更新购物车信息, + if (MapUtil.isNotEmpty(basketIdShopCartParamMap)) { + basketService.updateBasketByShopCartParam(userId, basketIdShopCartParamMap); + } + + // 拿到购物车的所有item + List shopCartItems = basketService.getShopCartItems(userId); + return ServerResponseEntity.success(basketService.getShopCarts(shopCartItems)); + + } +``` + +这里面传了一个参数:`Map basketIdShopCartParamMap` 这里是当用户改变了某件商品的满减满折活动时,重新改变满减满折信息以后计算加个的一个方法。当然在开源是没有这个满减模块的,只有思路,具体实现需要靠自己了。 + +我们继续往下看,这里面`basketService.getShopCartItems(userId)`使用的直接是从数据库中获取的数据,而真正对满减满折、店铺等进行排列组合的,在于`basketService.getShopCarts(shopCartItems)` 这个方法。 + + + +我们进到`getShopCarts`方法内部,可以查看到一行代码`applicationContext.publishEvent(new ShopCartEvent(shopCart, shopCartItemDtoList));`,这里使用的事件的模式。这个事件的主要作用是用于对模块之间的解耦,比如我们清楚的知道当购物车需要计算价格的时候,需要满减模块的配合,进行“装饰”。最后将装饰回来的东西,返回给前端。 + + + +我们现在看看购物车返回的数据`ServerResponseEntity>`,我们清楚一个购物车是分多个店铺的,每一个店铺就是一个`ShopCartDto`,我们看下这个`bean`。 + +```java +@Data +public class ShopCartDto implements Serializable { + + @Schema(description = "店铺ID" , required = true) + private Long shopId; + + @Schema(description = "店铺名称" , required = true) + private String shopName; + + @Schema(description = "购物车满减活动携带的商品" , required = true) + private List shopCartItemDiscounts; + +} +``` + +其实一个店铺下面是有多个商品的,但是根据京东的划分,每当有满减之类的活动时,满减活动的商品总是要归到一类的,所以,每个店铺下面是多个满减活动(`List`),满减活动下面是多个商品(购物项`List`),到此你就能明白了`ShopCartItemDiscountDto` 里面的`ChooseDiscountItemDto` 是什么东西了,这个是选中的满减项。 + +```java +public class ShopCartItemDiscountDto implements Serializable { + + @Schema(description = "已选满减项" , required = true) + private ChooseDiscountItemDto chooseDiscountItemDto; + + @Schema(description = "商品列表" ) + private List shopCartItems; +} +``` + +我们再留意`ShopCartItemDto` 这个`bean` ,发现还有这个东西: + +```java +@Schema(description = "参与满减活动列表" ) +private List discounts = new ArrayList<>(); +``` + +其实购物车的每个购物项,都是有很多个满减的活动的,可以自主选择满减活动,然后进行组合,生成新的优惠。而在这选择新的活动类型时,就需要购物车就行新的价格计算。这也就是为什么获取用户购物车信息,也就是`/info`接口需要一个这个参数的原因了`Map basketIdShopCartParamMap` diff --git a/doc/接口设计/2. 订单设计-确认订单.md b/doc/接口设计/2. 订单设计-确认订单.md new file mode 100644 index 0000000..40deec3 --- /dev/null +++ b/doc/接口设计/2. 订单设计-确认订单.md @@ -0,0 +1,200 @@ +下单简单的分成几个步骤: + +1. 用户点击“立即购买”或“购物车-结算”进入到“确认订单”页面 +2. 在“确认订单”页面选择收货地址,优惠券等,重新计算运费、订单价格 +3. 提交订单,选择支付方式进行支付 +4. 支付完毕 + + + +## 第一步: + +1. 用户点击“立即购买”或“购物车-结算”进入到“确认订单”页面,相关url`/p/order/confirm` + +我们希望能够有个统一下单的接口,不太希望“立即购买”和“购物车-结算”两个不同的接口影响到后面所有的流程,毕竟谁也不想一个差不多一样的接口,要写两遍,所以我们看下我们的系统是如何做的。 + + + +```java +public class OrderParam { + @Schema(description = "购物车id 数组" ) + private List basketIds; + + @Schema(description = "立即购买时提交的商品项" ) + private OrderItemParam orderItem; +} +``` + +这里使用了两种情况: + +- 假设`basketIds` 不为空,则说明是从购物车进入 +- 假设`orderItem` 不为空,则说明是从立即购买进入 + +通过`basketService.getShopCartItemsByOrderItems(orderParam.getBasketIds(),orderParam.getOrderItem(),userId)` 这个方法对两种情况进行组合,此时并不能将购物车商品删除,因为删除购物车中的商品,是在第三步提交订单的时候进行的,不然用户点击返回键,看到购物车里面的东西还没提交订单,东西就消失了,会感觉很奇怪。 + + + +我们重新回到`controller`层,我们看到了一行熟悉的代码`basketService.getShopCarts` + +```java + @PostMapping("/confirm") + @Operation(summary = "结算,生成订单信息" , description = "传入下单所需要的参数进行下单") + public ServerResponseEntity confirm(@Valid @RequestBody OrderParam orderParam) { + // 根据店铺组装购车中的商品信息,返回每个店铺中的购物车商品信息 + List shopCarts = basketService.getShopCarts(shopCartItems); + } +``` + +这行代码我们再《购物车的设计》这篇已经着重讲过了,但是我们在这为什么还需要这个东西呢? + +很简单,无论是点击“立即购买”或“购物车-结算”,事实上都是通过用户计算过一遍金额了,而且甚至有满减满折之类的活动,都是通过了统一的计算的。而这一套计算的流程,我们并不希望重新写一遍。所以当然是能够使用之前计算的金额,那是最好的咯。 + + + + + +## 第二步: + +2. 在“确认订单”页面选择收货地址,优惠券等,重新计算运费、订单价格 + +我们知道无论是在第一步还是第二步,本质上还是在确认订单的页面,其中订单页面的数据结构并没有发生任何的变化,所以其实第一步第二步是可以写在一起的。所以我们可以看到`OrderParam` 还多了两个参数 + +```java +public class OrderParam { + @Schema(description = "地址ID,0为默认地址" ,required=true) + @NotNull(message = "地址不能为空") + private Long addrId; + + @Schema(description = "用户是否改变了优惠券的选择,如果用户改变了优惠券的选择,则完全根据传入参数进行优惠券的选择" ) + private Integer userChangeCoupon; + + @Schema(description = "优惠券id数组" ) + private List couponIds; +} +``` + +但是有个问题,就是在于用户点击立即购买的时候,没有地址,那样如何计算运费呢?答案就是使用默认地址进行计算呀~ + + + +我们看下计算订单的事件,事实上有很多营销活动的时候,订单的计算也是非常的复杂的,所以我们和购物车一样,采用事件的驱动,一个接一个的对订单进行“装饰”,最后生成`ShopCartOrderMergerDto`一个合并的对象 + +```java + @PostMapping("/confirm") + @Operation(summary = "结算,生成订单信息" , description = "传入下单所需要的参数进行下单") + public ServerResponseEntity confirm(@Valid @RequestBody OrderParam orderParam) { + for (ShopCartDto shopCart : shopCarts) { + applicationContext.publishEvent(new ConfirmOrderEvent(shopCartOrder,orderParam,shopAllShopCartItems)); + + } + } +``` + +我们看下`ConfirmOrderListener` 这个事件里面的默认监听器,这里 + + + +```java +public class ConfirmOrderListener { + @EventListener(ConfirmOrderEvent.class) + @Order(ConfirmOrderOrder.DEFAULT) + public void defaultConfirmOrderEvent(ConfirmOrderEvent event) { + + + ShopCartOrderDto shopCartOrderDto = event.getShopCartOrderDto(); + + OrderParam orderParam = event.getOrderParam(); + + String userId = SecurityUtils.getUser().getUserId(); + + // 订单的地址信息 + UserAddr userAddr = userAddrService.getUserAddrByUserId(orderParam.getAddrId(), userId); + + double total = 0.0; + + int totalCount = 0; + + double transfee = 0.0; + + for (ShopCartItemDto shopCartItem : event.getShopCartItems()) { + // 获取商品信息 + Product product = productService.getProductByProdId(shopCartItem.getProdId()); + // 获取sku信息 + Sku sku = skuService.getSkuBySkuId(shopCartItem.getSkuId()); + if (product == null || sku == null) { + throw new YamiShopBindException("购物车包含无法识别的商品"); + } + if (product.getStatus() != 1 || sku.getStatus() != 1) { + throw new YamiShopBindException("商品[" + sku.getProdName() + "]已下架"); + } + + totalCount = shopCartItem.getProdCount() + totalCount; + total = Arith.add(shopCartItem.getProductTotalAmount(), total); + // 用户地址如果为空,则表示该用户从未设置过任何地址相关信息 + if (userAddr != null) { + // 每个产品的运费相加 + transfee = Arith.add(transfee, transportManagerService.calculateTransfee(shopCartItem, userAddr)); + } + + shopCartItem.setActualTotal(shopCartItem.getProductTotalAmount()); + shopCartOrderDto.setActualTotal(Arith.sub(total, transfee)); + shopCartOrderDto.setTotal(total); + shopCartOrderDto.setTotalCount(totalCount); + shopCartOrderDto.setTransfee(transfee); + } + } +} +``` + +值得留意的是,有那么一行代码 + +```java + // 用户地址如果为空,则表示该用户从未设置过任何地址相关信息 + if (userAddr != null) { + // 每个产品的运费相加 + transfee = Arith.add(transfee, transportManagerService.calculateTransfee(shopCartItem, userAddr)); + } +``` +运费是根据用户地址进行计算,当然还包括运费模板啦,想了解运费模板的,可以参考运费模板相关的章节。 + +那么有人就问了,那么优惠券呢?优惠券是有另一个监听器进行监听计算价格啦,购买了专业版或以上的版本就能看到源码咯~ + + + +我们看看返回给前端的订单信息: + +```java +@Data +public class ShopCartOrderMergerDto implements Serializable{ + + @Schema(description = "实际总值" , required = true) + private Double actualTotal; + + @Schema(description = "商品总值" , required = true) + private Double total; + + @Schema(description = "商品总数" , required = true) + private Integer totalCount; + + @Schema(description = "订单优惠金额(所有店铺优惠金额相加)" , required = true) + private Double orderReduce; + + @Schema(description = "地址Dto" , required = true) + private UserAddrDto userAddr; + + @Schema(description = "每个店铺的购物车信息" , required = true) + private List shopCartOrders; + + @Schema(description = "整个订单可以使用的优惠券列表" , required = true) + private List coupons; +} + +``` + +这里又有一段我们熟悉的代码: + +```java +@Schema(description = "每个店铺的购物车信息" , required = true) +private List shopCartOrders; +``` +没错这里返回的数据格式,和购物车的格式是一样的,因为第一步当中已经说明,订单来自于购物车的计算,所以会在基础上条件新的数据,基本上就是返回给前端的数据了。 diff --git a/doc/接口设计/3. 订单设计-提交订单.md b/doc/接口设计/3. 订单设计-提交订单.md new file mode 100644 index 0000000..d49b66f --- /dev/null +++ b/doc/接口设计/3. 订单设计-提交订单.md @@ -0,0 +1,151 @@ +> 首先我们在这里严重的批评一些,在接口订单的接口中,直接传订单金额,而不是使用下单是已经计算好金额的人,这些接口岂不是使用0.01就能将全部的商品都买下来了? + + + +我们回到订单设计这一个模块,首先我们在确认订单的时候就已经将价格计算完成了,那么我们肯定是想将计算结果给保留下来的,至于计算的过程,我们并不希望这个过程还要进行一遍的计算。 + + + +我们返回确认订单的接口,看到这样一行代码: + +```java + @Operation(summary = "结算,生成订单信息" , description = "传入下单所需要的参数进行下单") + public ServerResponseEntity confirm(@Valid @RequestBody OrderParam orderParam) { + orderService.putConfirmOrderCache(userId,shopCartOrderMergerDto); + } +``` + + + +这里每经过一次计算,就将整个订单通过`userId`进行了保存,而这个缓存的时间为30分钟,当用户使用 + +```java + @PostMapping("/submit") + @Operation(summary = "提交订单,返回支付流水号" , description = "根据传入的参数判断是否为购物车提交订单,同时对购物车进行删除,用户开始进行支付") + public ServerResponseEntity submitOrders(@Valid @RequestBody SubmitOrderParam submitOrderParam) { + ShopCartOrderMergerDto mergerOrder = orderService.getConfirmOrderCache(userId); + if (mergerOrder == null) { + throw new YamiShopBindException("订单已过期,请重新下单"); + } + + // 省略中间一大段。。。 + + orderService.removeConfirmOrderCache(userId); + } +``` + +当无法获取缓存的时候告知用户订单过期,当订单进行提交完毕的时候,将之前的缓存给清除。 + + + +我们又回到提交订单中间这几行代码: + +```java +List orders = orderService.submit(userId,mergerOrder); +``` + +这行代码也就是提交订单的核心代码 + +```java +eventPublisher.publishEvent(new SubmitOrderEvent(mergerOrder, orderList)); +``` + +其中这里依旧是使用时间的方式,将订单进行提交,看下这个`SubmitOrderEvent`的默认监听事件。 + +```java +@Component("defaultSubmitOrderListener") +@AllArgsConstructor +public class SubmitOrderListener { + public void defaultSubmitOrderListener(SubmitOrderEvent event) { + // ... + } +} +``` + + + +这里有几段值得注意的地方: + +- 这里是`UserAddrOrder` 并不是`UserAddr`: + +```java +// 把订单地址保存到数据库 +UserAddrOrder userAddrOrder = BeanUtil.copyProperties(mergerOrder.getUserAddr(), UserAddrOrder.class); +if (userAddrOrder == null) { + throw new YamiShopBindException("请填写收货地址"); +} +userAddrOrder.setUserId(userId); +userAddrOrder.setCreateTime(now); +userAddrOrderService.save(userAddrOrder); +``` + +这里是将订单的收货地址进行了保存入库的操作,这里是绝对不能只保存用户的地址id在订单中的,要将地址入库,原因是如果用户在订单中设置了一个地址,如果用户在订单还没配送的时候,将自己的地址改了的话。如果仅采用关联的地址,就会出现问题。 + + + +- 为每个店铺生成一个订单 + +```java +// 每个店铺生成一个订单 +for (ShopCartOrderDto shopCartOrderDto : shopCartOrders) { + +} +``` + +这里为每个店铺创建一个订单,是为了,以后平台结算给商家时,每个商家的订单单独结算。用户确认收货时,也可以为每家店铺单独确认收货。 + + + +- 使用雪花算法生成订单id, 如果对雪花算法感兴趣的,可以去搜索下相关内容: + +```java +String orderNumber = String.valueOf(snowflake.nextId()); +``` + +我们不想单多台服务器生成的id冲突,也不想生成uuid这样的很奇怪的字符串id,更不想直接使用数据库主键这种东西时,雪花算法就出现咯。 + + + +- 当用户提交订单的时候,购物车里面勾选的商品,理所当然的要清空掉 + +```java +// 删除购物车的商品信息 +if (!basketIds.isEmpty()) { + basketMapper.deleteShopCartItemsByBasketIds(userId, basketIds); + +} +``` + + + +- 使用数据库的乐观锁,防止超卖: + +```java +if (skuMapper.updateStocks(sku) == 0) { + skuService.removeSkuCacheBySkuId(key, sku.getProdId()); + throw new YamiShopBindException("商品:[" + sku.getProdName() + "]库存不足"); + } +``` + +```sql +update tz_sku set stocks = stocks - #{sku.stocks}, version = version + 1,update_time = NOW() where sku_id = #{sku.skuId} and #{sku.stocks} <= stocks +``` + +超卖一直是一件非常令人头疼的事情,如果对订单直接加悲观锁的话,那么下单的性能将会很差。商城最重要的就是下单啦,要是性能很差,那人家还下个鬼的单哟,所以我们采用数据库的乐观锁进行下单。 + +所谓乐观锁,就是在 where 条件下加上极限的条件,比如在这里就是更新的库存小于或等于商品的库存,在这种情况下可以对库存更新成功,则更新完成了,否则抛异常(真正的定义肯定不是这样的啦,你可以百度下 “乐观锁更新库存”)。注意这里在抛异常以前,应该将缓存也更新了,不然无法及时更新。 + + + +最后我们回到`controller` + +```java +return ServerResponseEntity.success(new OrderNumbersDto(orderNumbers.toString())); +``` + +这里面返回了多个订单项,这里就变成了并单支付咯,在多个店铺一起进行支付的时候需要进行并单支付的操作,一个店铺的时候,又要变成一个订单支付的操作,可是我们只希望有一个统一支付的接口进行调用,所以我们的支付接口要进行一点点的设计咯。 + + + + + diff --git a/doc/接口设计/4. 订单设计-支付.md b/doc/接口设计/4. 订单设计-支付.md new file mode 100644 index 0000000..44b7f08 --- /dev/null +++ b/doc/接口设计/4. 订单设计-支付.md @@ -0,0 +1,139 @@ +> 我们的支付时不允许在订单的支付接口传订单金额的,所以我们采用了订单号进行支付的形式 + +## 支付 + +我们来到`PayController` ,这里就是统一支付的接口,当然这里的统一支付采用的是模拟支付。 + +我们直接看一下核心代码: + +```java +PayInfoDto payInfo = payService.pay(userId, payParam); +``` + +再看看里面的代码: + +```java + // 修改订单信息 + for (String orderNumber : orderNumbers) { + OrderSettlement orderSettlement = new OrderSettlement(); + orderSettlement.setPayNo(payNo); + orderSettlement.setPayType(payParam.getPayType()); + orderSettlement.setUserId(userId); + orderSettlement.setOrderNumber(orderNumber); + orderSettlementMapper.updateByOrderNumberAndUserId(orderSettlement); + + Order order = orderMapper.getOrderByOrderNumber(orderNumber); + prodName.append(order.getProdName()).append(StrUtil.COMMA); + } +``` + +这里对传过来的支付参数`orderNumbers`进行了拆分,为每个订单的结算信息都进行了更新,所以这里便支持了分单支付和并单支付的流程。 + + + +订单金额: + +```java +// 除了ordernumber不一样,其他都一样 +List settlements = orderSettlementMapper.getSettlementsByPayNo(payNo); +// 应支付的总金额 +double payAmount = 0.0; +for (OrderSettlement orderSettlement : settlements) { + payAmount = Arith.add(payAmount, orderSettlement.getPayAmount()); +} +``` + +这里面应支付的金额是通过数据库中获取的订单金额,是不接受任何前端传入的订单金额的。 + + + +## 支付回调 + + + +我们回到`controller` + +```java +orderRequest.setNotifyUrl(apiConfig.getDomainName() + "/notice/pay/order"); +``` + +这里面规定的,订单回调的地址,这也就是为什么需要`api.properties` 传入`api.domainName`的原因 + + + +根据订单配置`/notice/pay/order`,我们去到订单回调的`controller`既`PayNoticeController` + +- 验签 + +因为订单的已经决定的订单已经支付成功,所以订单的回调是需要做一些验证的。不然谁都可以调用订单回调的地址,实在是十分危险。 + +其实`wxjava`这个工具包已经对返回的参数进行了校验 + +```java +WxPayOrderNotifyResult parseOrderNotifyResult = wxMiniPayService.parseOrderNotifyResult(xmlData); +``` + +在上面这个方法之下,就有那么一句话 + +```java +result.checkResult(this, this.getConfig().getSignType(), false); +``` + + + +- 更新支付状态 + +我们看看这里的业务核心方法: + +```java +// 根据内部订单号更新order settlement +payService.paySuccess(payNo, bizPayNo); +``` + + + +```java + @Override + @Transactional(rollbackFor = Exception.class) + public List paySuccess(String payNo, String bizPayNo) { + List orderSettlements = orderSettlementMapper.selectList(new LambdaQueryWrapper().eq(OrderSettlement::getPayNo, payNo)); + + OrderSettlement settlement = orderSettlements.get(0); + + // 订单已支付 + if (settlement.getPayStatus() == 1) { + log.info("订单已支付,settlement.id:{}",settlement.getSettlementId()); + return null; + } + // 修改订单结算信息 + if (orderSettlementMapper.updateToPay(payNo, settlement.getVersion()) < 1) { + throw new YamiShopBindException("结算信息已更改"); + } + + + List orderNumbers = orderSettlements.stream().map(OrderSettlement::getOrderNumber).collect(Collectors.toList()); + + // 将订单改为已支付状态 + orderMapper.updateByToPaySuccess(orderNumbers, PayType.WECHATPAY.value()); + + List orders = orderNumbers.stream().map(orderNumber -> { + Order order = orderMapper.getOrderByOrderNumber(orderNumber); + order.setOrderItems(orderItemMapper.listByOrderNumber(orderNumber)); + return order; + }).collect(Collectors.toList()); + eventPublisher.publishEvent(new PaySuccessOrderEvent(orders)); + return orderNumbers; + } +``` + +这里无非就是找到原来的订单,将订单变成已支付的状态。 + + + +而这里同样有事件支付成功的事件 + +```java +eventPublisher.publishEvent(new PaySuccessOrderEvent(orders)); +``` + +这里的事件也是和营销活动有关的,比如分销,这些代码也是商业版才有的。 diff --git a/doc/接口设计/必读.md b/doc/接口设计/必读.md new file mode 100644 index 0000000..2659abe --- /dev/null +++ b/doc/接口设计/必读.md @@ -0,0 +1,3 @@ +这里只有几点说明: + +1. 这里写的是接口设计,如果你整个接口的接口文档,只需要启动api这个项目,然后访问 http://localhost:8086/doc.html diff --git a/doc/生产环境/centos jdk安装.md b/doc/生产环境/centos jdk安装.md new file mode 100644 index 0000000..bd84f25 --- /dev/null +++ b/doc/生产环境/centos jdk安装.md @@ -0,0 +1,8 @@ +(1)安装JDK + +安装JDK,如果没有java-17-openjdk-devel就没有javac命令 + +```bash +yum install java-17-openjdk java-17-openjdk-devel +``` + diff --git a/doc/生产环境/docker/Docker Compose 安装与卸载.md b/doc/生产环境/docker/Docker Compose 安装与卸载.md new file mode 100644 index 0000000..b81d49c --- /dev/null +++ b/doc/生产环境/docker/Docker Compose 安装与卸载.md @@ -0,0 +1,142 @@ +`Compose` 支持 Linux、macOS、Windows 10 三大平台。 + +`Compose` 可以通过 Python 的包管理工具 `pip` 进行安装,也可以直接下载编译好的二进制文件使用,甚至能够直接在 Docker 容器中运行。 + +前两种方式是传统方式,适合本地环境下安装使用;最后一种方式则不破坏系统环境,更适合云计算场景。 + +`Docker for Mac` 、`Docker for Windows` 自带 `docker-compose` 二进制文件,安装 Docker 之后可以直接使用。 + +```bash +$ docker-compose --version + +docker-compose version 1.17.1, build 6d101fb +``` + +Linux 系统请使用以下介绍的方法安装。 + +## 安装方法一:二进制包 + +在 Linux 上的也安装十分简单,从 [官方 GitHub Release](https://github.com/docker/compose/releases) 处直接下载编译好的二进制文件即可。 + +例如,在 Linux 64 位系统上直接下载对应的二进制包。 + +```bash +$ sudo curl -L https://github.com/docker/compose/releases/download/1.17.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose +$ sudo chmod +x /usr/local/bin/docker-compose +``` + +## 安装方法二:PIP 安装 + +*注:* `x86_64` 架构的 Linux 建议按照上边的方法下载二进制包进行安装,如果您计算机的架构是 `ARM`(例如,树莓派),再使用 `pip` 安装。 + +这种方式是将 Compose 当作一个 Python 应用来从 pip 源中安装。 + +1、安装python-pip + +```bash +yum -y install epel-release + +yum -y install python-pip +``` + + +执行安装命令: + +```bash +pip install -U docker-compose +``` + +可以看到类似如下输出,说明安装成功。 + +```bash +Collecting docker-compose + Downloading docker-compose-1.17.1.tar.gz (149kB): 149kB downloaded +... +Successfully installed docker-compose cached-property requests texttable websocket-client docker-py dockerpty six enum34 backports.ssl-match-hostname ipaddress +``` + +查看版本号 +``` +docker-compose version +``` + +bash 补全命令 + +将对应版本号的docker-compose补全如:下面的`1.8.0`替换成 `1.24.1` + +```bash +$ curl -L https://raw.githubusercontent.com/docker/compose/1.8.0/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose +``` + + + +## 容器中执行 + +Compose 既然是一个 Python 应用,自然也可以直接用容器来执行它。 + +```bash +$ curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose +$ chmod +x /usr/local/bin/docker-compose +``` + +实际上,查看下载的 `run.sh` 脚本内容,如下 + +```bash +set -e + +VERSION="1.8.0" +IMAGE="docker/compose:$VERSION" + + +# Setup options for connecting to docker host +if [ -z "$DOCKER_HOST" ]; then + DOCKER_HOST="/var/run/docker.sock" +fi +if [ -S "$DOCKER_HOST" ]; then + DOCKER_ADDR="-v $DOCKER_HOST:$DOCKER_HOST -e DOCKER_HOST" +else + DOCKER_ADDR="-e DOCKER_HOST -e DOCKER_TLS_VERIFY -e DOCKER_CERT_PATH" +fi + + +# Setup volume mounts for compose config and context +if [ "$(pwd)" != '/' ]; then + VOLUMES="-v $(pwd):$(pwd)" +fi +if [ -n "$COMPOSE_FILE" ]; then + compose_dir=$(dirname $COMPOSE_FILE) +fi +# TODO: also check --file argument +if [ -n "$compose_dir" ]; then + VOLUMES="$VOLUMES -v $compose_dir:$compose_dir" +fi +if [ -n "$HOME" ]; then + VOLUMES="$VOLUMES -v $HOME:$HOME -v $HOME:/root" # mount $HOME in /root to share docker.config +fi + +# Only allocate tty if we detect one +if [ -t 1 ]; then + DOCKER_RUN_OPTIONS="-t" +fi +if [ -t 0 ]; then + DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i" +fi + +exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE "$@" +``` + +可以看到,它其实是下载了 `docker/compose` 镜像并运行。 + +## 卸载 + +如果是二进制包方式安装的,删除二进制文件即可。 + +```bash +rm /usr/local/bin/docker-compose +``` + +如果是通过 `pip` 安装的,则执行如下命令即可删除。 + +```bash +pip uninstall docker-compose +``` diff --git a/doc/生产环境/docker/docker centos 安装.md b/doc/生产环境/docker/docker centos 安装.md new file mode 100644 index 0000000..fbf4212 --- /dev/null +++ b/doc/生产环境/docker/docker centos 安装.md @@ -0,0 +1,90 @@ +## 安装 Docker +从 2017 年 3 月开始 docker 在原来的基础上分为两个分支版本: Docker CE 和 Docker EE。 + +Docker CE 即社区免费版,Docker EE 即企业版,强调安全,但需付费使用。 + +本文介绍 Docker CE 的安装使用。 + +移除旧的版本: + +``` +$ sudo yum remove docker \ + docker-client \ + docker-client-latest \ + docker-common \ + docker-latest \ + docker-latest-logrotate \ + docker-logrotate \ + docker-selinux \ + docker-engine-selinux \ + docker-engine +``` + +安装一些必要的系统工具: + +``` +sudo yum install -y yum-utils device-mapper-persistent-data lvm2 +``` + +添加软件源信息: + +``` +sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo +``` + +更新 yum 缓存: + +``` +sudo yum makecache fast +``` + +安装 Docker-ce: + +``` +sudo yum -y install docker-ce +``` + +查看已安装docker版本 +``` +docker version +``` + +启动 Docker 后台服务 + +``` +sudo systemctl start docker +``` + +开机启动 + +``` +systemctl enable docker +``` + + +## 镜像加速 +鉴于国内网络问题,后续拉取 Docker 镜像十分缓慢,我们可以需要配置加速器来解决,推荐使用的是docker官方推荐的中国镜像地址:https://registry.docker-cn.com + +新版的 Docker 使用 `/etc/docker/daemon.json`(Linux,没有请新建)。 + +请在该配置文件中加入(没有该文件的话,请先建一个): +```javascript +{ + "registry-mirrors": ["https://registry.docker-cn.com"] +} +``` + +重启docker + +``` +sudo systemctl daemon-reload +sudo systemctl restart docker +``` + +### 检查加速器是否生效 + +配置加速器之后,如果拉取镜像仍然十分缓慢,请手动检查加速器配置是否生效,在命令行执行 `docker info`,如果从结果中看到了如下内容,说明配置成功。 +``` +Registry Mirrors: + https://registry.docker-cn.com/ +``` diff --git a/doc/生产环境/docker/docker 容器的基本操作.md b/doc/生产环境/docker/docker 容器的基本操作.md new file mode 100644 index 0000000..46b15de --- /dev/null +++ b/doc/生产环境/docker/docker 容器的基本操作.md @@ -0,0 +1,41 @@ +## Docker 获取镜像 + +之前提到过,[Docker Hub](https://hub.docker.com/) 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。 + +从 Docker 镜像仓库获取镜像的命令是 `docker pull`。其命令格式为: +``` +# docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] +docker pull [OPTIONS] NAME[:TAG|@DIGEST] + +``` + +具体的选项可以通过 docker pull --help 命令看到,这里我们说一下镜像名称的格式。 + +- Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。 +- 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。 +比如: +``` +# 向docker拉取,最小化的jre 1.8的运行环境(anapsix/alpine-java 项目名称name,8_server-jre_unlimited为标签tag) +docker pull anapsix/alpine-java:8_server-jre_unlimited +``` + + +从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一致性。 + +### 查看已下载镜像列表 +`docker images` 或 `docker image ls` + +``` +[root@localhost ~]# docker images +REPOSITORY TAG IMAGE ID CREATED SIZE +anapsix/alpine-java 8_server-jre_unlimited 49d744fbb526 5 months ago 126MB +``` + +### 删除镜像 +`docker image rm IMAGE_ID|NAME [IMAGE_ID|NAME...]` 或 `docker rmi IMAGE_ID|NAME [IMAGE_ID|NAME...]` + +### 清空虚悬镜像 + +docker在构建了一个新的镜像(名字和tag都一样的)之后,旧的那个镜像就会变成一个虚悬镜像(旧的镜像就没有名字了),此时旧的镜像就没啥用了,可以一件清空 + +`docker image prune` diff --git a/doc/生产环境/docker/docker 镜像的基本操作.md b/doc/生产环境/docker/docker 镜像的基本操作.md new file mode 100644 index 0000000..46b15de --- /dev/null +++ b/doc/生产环境/docker/docker 镜像的基本操作.md @@ -0,0 +1,41 @@ +## Docker 获取镜像 + +之前提到过,[Docker Hub](https://hub.docker.com/) 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。 + +从 Docker 镜像仓库获取镜像的命令是 `docker pull`。其命令格式为: +``` +# docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] +docker pull [OPTIONS] NAME[:TAG|@DIGEST] + +``` + +具体的选项可以通过 docker pull --help 命令看到,这里我们说一下镜像名称的格式。 + +- Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。 +- 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。 +比如: +``` +# 向docker拉取,最小化的jre 1.8的运行环境(anapsix/alpine-java 项目名称name,8_server-jre_unlimited为标签tag) +docker pull anapsix/alpine-java:8_server-jre_unlimited +``` + + +从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一致性。 + +### 查看已下载镜像列表 +`docker images` 或 `docker image ls` + +``` +[root@localhost ~]# docker images +REPOSITORY TAG IMAGE ID CREATED SIZE +anapsix/alpine-java 8_server-jre_unlimited 49d744fbb526 5 months ago 126MB +``` + +### 删除镜像 +`docker image rm IMAGE_ID|NAME [IMAGE_ID|NAME...]` 或 `docker rmi IMAGE_ID|NAME [IMAGE_ID|NAME...]` + +### 清空虚悬镜像 + +docker在构建了一个新的镜像(名字和tag都一样的)之后,旧的那个镜像就会变成一个虚悬镜像(旧的镜像就没有名字了),此时旧的镜像就没啥用了,可以一件清空 + +`docker image prune` diff --git a/doc/生产环境/docker/使用docker部署商城.md b/doc/生产环境/docker/使用docker部署商城.md new file mode 100644 index 0000000..145513e --- /dev/null +++ b/doc/生产环境/docker/使用docker部署商城.md @@ -0,0 +1,15 @@ +> 在阅读本章节前,我们回认为您已经会安装并且使用docker,如果您不会安装使用docker的话,请阅读相关章节 + + + +**如果无法理解我们所编写的 `Dockerfile`强烈的不推荐使用docker进行生产环境部署!!!** + +0. 将整个项目上传到centos中,进入到项目根目录 +1. 安装 `docker` (参考《docker centos 安装》) +2. 安装`docker-compose`(参考《Docker Compose 安装与卸载》) +3. 安装`open-jdk17`(参考《centos jdk安装》) +4. 安装`maven`(参考《通过yum安装maven》) +5. 使用 `mvn clean package -DskipTests` 命令进行打包 +6. 使用 `docker-compose up` 启动项目 +7. 使用nginx将请求指向特定的端口。 + diff --git a/doc/生产环境/docker/通过yum安装maven.md b/doc/生产环境/docker/通过yum安装maven.md new file mode 100644 index 0000000..067cfc2 --- /dev/null +++ b/doc/生产环境/docker/通过yum安装maven.md @@ -0,0 +1,14 @@ +安装maven的前提是安装jdk,参考《linux jdk安装》 + +```bash +// 使用配置工具配置第三方epel源仓库 +yum-config-manager --add-repo http://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo +yum-config-manager --enable epel-apache-maven +// 安装maven +yum install -y apache-maven +// 验证maven,验证是否为Oracle字样,默认有版本输出 +mvn -version +//最后确认下yum源地址有没被误改 +yum repolist +``` + diff --git a/doc/生产环境/nginx安装与跨域配置.md b/doc/生产环境/nginx安装与跨域配置.md new file mode 100644 index 0000000..be9f08d --- /dev/null +++ b/doc/生产环境/nginx安装与跨域配置.md @@ -0,0 +1,77 @@ +本文为大家介绍了*CentOS* 7 64位 安装 *nginx*与跨域配置 的详细步骤 + +Nginx官方提供了Yum源 + +## 1、安装nginx + +```shell +yum install -y nginx +``` + + + +## 2、启动Nginx并设置开机自动运行 + +```shell +systemctl start nginx.service +systemctl enable nginx.service +``` + + + +## 3、配置nginx + +``` +vi /etc/nginx/nginx.conf +``` + +使用上面的命令编辑nginx的配置文件,先把配置文件中的server注释掉,然后添加下面的语句 + +```nginx +#小程序接口的域名配置,小程序规定要https,填写对应域名,并把https证书上传至服务器 +server { + listen 443; + server_name mall4j-api.gz-yami.com; + ssl on; + ssl_certificate /usr/share/nginx/cert/xxxxxxxxxxxxxxxx.pem; + ssl_certificate_key /usr/share/nginx/cert/xxxxxxxxxxxxxxxx.key; + ssl_session_timeout 5m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; + ssl_prefer_server_ciphers on; + location / { + proxy_pass http://127.0.0.1:8112; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } + +#后台域名配置,后台vue页面代码上传至 /usr/share/nginx/admin +server { + listen 80; + server_name mall4j-admin.gz-yami.com; + root /usr/share/nginx/admin; + + # Load configuration files for the default server block. + include /etc/nginx/default.d/*.conf; + + location / { + } + + # 跨域配置 + location /apis { + rewrite ^/apis/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8111; + } + +} +``` + + + +## 4、重启nginx,让配置生效 + +```shell +systemctl restart nginx.service +``` diff --git a/doc/生产环境/安装mysql.md b/doc/生产环境/安装mysql.md new file mode 100644 index 0000000..abf59fd --- /dev/null +++ b/doc/生产环境/安装mysql.md @@ -0,0 +1,104 @@ +本文为大家介绍了*CentOS* 7 64位 安装 *MySQL5.7* 的详细步骤 + +## 1、配置YUM源 + +在[MySQL]官网中下载YUM源rpm安装包:http://dev.mysql.com/downloads/repo/yum/ + +\# 下载mysql源安装包 + +``` +shell> wget http://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpm +``` + +#安装mysql源 + +```shell +shell> yum localinstall mysql57-community-release-el7-8.noarch.rpm +``` + +检查mysql源是否安装成功 + +```shell +shell> yum repolist enabled | grep "mysql.*-community.*" +``` + + + +## 2、安装MySQL + +```shell +shell> yum install mysql-community-server +``` + + + +## 3、配置默认编码为utf8 并且设置不区分大小写 + +修改/etc/my.cnf配置文件,在[mysqld]下添加编码配置,如下所示: + +```mysql +[mysqld] + +character_set_server=utf8 + +init_connect='SET NAMES utf8' + +lower_case_table_names=1 +``` + + + +## 4、启动MySQL服务 + +```shell +shell> systemctl start mysqld +``` + + + +## 5、开机启动 + +```shell +shell> systemctl enable mysqld +shell> systemctl daemon-reload +``` + + + +## 6、修改root默认密码 + +mysql安装完成之后,在/var/log/mysqld.log文件中给root生成了一个默认密码。通过下面的方式找到root默认密码,然后登录mysql进行修改: + +```shell +shell> grep 'temporary password' /var/log/mysqld.log +``` + +查看到密码后用root登录修改密码 + +```shell +shell> mysql -uroot -p +``` + +```mysql +mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'MyNewPass4!'; +``` + +或者 + +```mysql +mysql> set password for 'root'@'localhost'=password('MyNewPass4!'); +``` + +**注意**:mysql5.7默认安装了密码安全检查插件(validate_password),默认密码检查策略要求密码必须包含:大小写字母、数字和特殊符号,并且长度不能少于8位。否则会提示ERROR 1819 (HY000): Your password does not satisfy the current policy requirements错误 + + + +## 7、创建数据库并添加远程登录用户 + +默认只允许root帐户在本地登录,如果要在其它机器上连接mysql,必须修改root允许远程连接,或者添加一个允许远程连接的帐户,为了安全起见,我添加一个新的帐户: + +```mysql +mysql> create database yamidb CHARACTER SET utf8 COLLATE utf8_general_ci; +mysql> GRANT ALL PRIVILEGES ON yamidb.* TO 'yami'@'%' IDENTIFIED BY 'Yami@2019'; +``` + diff --git a/doc/生产环境/安装redis.md b/doc/生产环境/安装redis.md new file mode 100644 index 0000000..9dd1df2 --- /dev/null +++ b/doc/生产环境/安装redis.md @@ -0,0 +1,62 @@ +## 安装redis + +``` +#安装tcl redis需要 +wget http://downloads.sourceforge.net/tcl/tcl8.6.8-src.tar.gz +tar xzvf tcl8.6.8-src.tar.gz -C /usr/local/ +cd /usr/local/tcl8.6.8/unix/ +./configure +make && make install + +#安装redis +wget http://download.redis.io/releases/redis-4.0.11.tar.gz +tar xzvf redis-4.0.11.tar.gz -C /usr/local/ +cd /usr/local/redis-4.0.11/ +make && make test && make install +``` + +## redis的生产环境启动方案 + +要把redis作为一个系统的daemon进程去运行的,每次系统启动,redis进程一起启动 + +1. wget下载redis解压出来的文件夹里面有个utils,utils目录下有个redis_init_script脚本 +2. 将redis_init_script脚本拷贝到linux的/etc/init.d目录中,将redis_init_script重命名为redis_6379,6379是我们希望这个redis实例监听的端口号 +3. 修改redis_6379脚本的第6行的REDISPORT,设置为相同的端口号(默认就是6379) +4. 创建两个目录:/etc/redis(存放redis的配置文件),/var/redis/6379(存放redis的持久化文件) +5. 修改redis配置文件(默认在根目录下,redis.conf),拷贝到/etc/redis目录中,修改名称为6379.conf +6. 修改redis.conf中的部分配置为生产环境 + +``` +daemonize yes 让redis以daemon进程运行 +pidfile /var/run/redis_6379.pid 设置redis的pid文件位置 +port 6379 设置redis的监听端口号 +dir /var/redis/6379 设置持久化文件的存储位置 +``` + +1. 启动redis,执行cd /etc/init.d, chmod 777 redis_6379,./redis_6379 start +2. 确认redis进程是否启动,ps -ef | grep redis +3. 让redis跟随系统启动自动启动 + +在redis_6379脚本中,最上面,加入两行注释 + +``` +# chkconfig: 2345 90 10 + +# description: Redis is a persistent key-value database +``` + +执行 + +``` +chkconfig redis_6379 on +``` + +## redis cli的使用 + +redis-cli SHUTDOWN,连接本机的6379端口停止redis进程 + +redis-cli -h 127.0.0.1 -p 6379 SHUTDOWN,制定要连接的ip和端口号 + +redis-cli PING,ping redis的端口,看是否正常 + +redis-cli,进入交互式命令行 diff --git a/doc/生产环境/教你如何部署.md b/doc/生产环境/教你如何部署.md new file mode 100644 index 0000000..60b3883 --- /dev/null +++ b/doc/生产环境/教你如何部署.md @@ -0,0 +1,183 @@ +## 安装jdk + +安装JDK,如果没有java-17-openjdk-devel就没有javac命令 + +```bash +yum install java-17-openjdk java-17-openjdk-devel +``` + + + + + +## 编译打包项目 + +项目最终需要进行编译打包上传到服务器,生产环境上的配置与测试环境不同,需要独立配置一些东西,满足自己的需要 + + + +### 1. mall4j + +1. 修改`yami-shop-admin\src\main\resources\application-prod.yml` 更改为生产环境的数据库账号密码,端口号等 + +2. 修改`yami-shop-admin\src\main\resources\logback\logback-prod.xml` 修改里面的`PROJECT_PATH` 将`/opt/projects/yami-shops` 改为自己生产环境的项目路径 + +3. 修改`yami-shop-api\src\main\resources\application-prod.yml` 更改为生产环境的数据库账号密码,端口号等 + +4. 修改`yami-shop-api\src\main\resources\logback\logback-prod.xml` 修改里面的`PROJECT_PATH` 将`/opt/projects/yami-shops` 改为自己生产环境的项目路径 + +以上 1、2 工程目录为 `yami-shop-admin` 而 3、4 工程目录为 `yami-shop-api` 请注意区分 + +6. 修改完毕后打包,使用`mvn clean package -DskipTests` 命令进行打包,最终会生成很多的jar,我们需要其中两个。 + +- 商城后台接口 `yami-shop-admin\target\yami-shop-admin-0.0.1-SNAPSHOT.jar` +- 商城前端接口`yami-shop-api\target\yami-shop-api-0.0.1-SNAPSHOT.jar` + +7. 将两个jar上传到centos环境中 + +8. 在生产环境中运行时候,需要使用`-Dspring.profiles.active=prod` ,在使用admin这个工程项目于生产环境的时候要添加定时任务的配置如`-Dspring.profiles.active=prod`,运行: + +```bash +nohup java -jar -Dspring.profiles.active=prod "${jarPath}/${jarName}" > "${jarPath}/log/${moduleName}-console.log" & + +nohup java -jar -Dspring.profiles.active=prod "${jarPath}/${jarName}" > "${jarPath}/log/${moduleName}-console.log" & +``` + +- 替换`${jarPath}` 为`jar` 所在路径 +- 替换`${jarName}` 为`jar` 所在路径 +- 替换`${moduleName}` 为`admin`或`api` + +9. 查看控制台日志输出 + +```bash +# 后台日志 +tail -f ${PROJECT_PATH}/log/admin.log +# 前端接口日志 +tail -f ${PROJECT_PATH}/log/api.log +``` + +- 替换`${PROJECT_PATH}` 为`logback-prod.xml` 里面修改的`PROJECT_PATH` 路径 + +10. 使用nginx将请求指向特定的端口。 + +11. 关于定时任务,请使用xxl-job,看`XxlJobConfig`的配置说明 + +### 2.vue + +> mall4v:v代表vue项目,是后台管理员界面使用的前端项目,因为前后端分离的 +> +> mall4uni:用户前端h5项目 +> +> mall4m:小程序项目 + +下面以mall4v为主进行讲解 + +##### 1. 安装nodejs + 淘宝npm镜像 + +如果不了解怎么安装nodejs的,可以参考 [菜鸟教程的nodejs相关](https://www.runoob.com/nodejs/nodejs-install-setup.html) + + +将npm的镜像源更改为淘宝的镜像源,回车(千万不要用cnpm,否则会出现不可预知的后果): + +```bash +npm config set registry https://registry.npmmirror.com +``` + + + +##### 2. 安装依赖启动项目 + +项目要求使用 [pnpm](https://www.pnpm.cn/) 包管理工具 + +使用编辑器打开项目,在根目录执行以下命令安装依赖 + +```bash +pnpm install +``` + +如果不想使用 pnpm,请删除 `package.json` 文件中 `pnpm` 相关内容后再进行安装 + +```json +{ + "scripts" : { + "preinstall": "npx only-allow pnpm" // 删除此行 + }, + "engines": { + "pnpm": ">=7" // 删除此行 + }, + "pnpm": { // 删除此项 + ... + } +} +``` + +##### 3. 修改配置文件,连接后台 + +修改 `.env.production` 连接后台, + +- `VUE_APP_BASE_API` : `mall4v` 这个项目连接的是`admin.jar`提供的接口 + +- `VUE_APP_RESOURCES_URL` : 静态资源文件对应的url,比如七牛云之类的 + +```javascript +# just a flag +ENV = 'production' + +// api接口请求地址 +VUE_APP_BASE_API = 'https://mini-admin.mall4j.com/apis' + +// 静态资源文件url +VUE_APP_RESOURCES_URL = 'https://img.mall4j.com/' +``` + + + +如果你仔细查看我们默认的`.env.production`的设置的话,会看到我们的url后面加了`/apis`,实际上这是我们为了少创建几个子域名做的操作,如果你看到`《nginx安装与跨域配置》` 就能看出这里其实做了转发。 + +其实如果创建的子域名足够多,也就不需要nginx进行转发了,此时直接填域名即可,无需再加`/apis` 两个后缀了。 + +如下所示: + +```nginx + location /apis { + rewrite ^/apis/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8111; + } +``` + + + +##### 打包,上传到服务器 + +1. 使用 `npm run build` 命令对项目进行打包 +2. 将步骤1中生成的`dist` 文件夹中的文件,压缩,上传到服务器nginx指定好的目录(`/usr/share/nginx/admin` ),解压 + + + +`mall4j-admin.conf` + +```nginx +server { + listen 80; + server_name mall4j-admin.gz-yami.com; + root /usr/share/nginx/admin; + + # Load configuration files for the default server block. + include /etc/nginx/default.d/*.conf; + + location / { + } + + # 跨域配置 + location /apis { + rewrite ^/apis/(.*)$ /$1 break; + proxy_pass http://127.0.0.1:8111; + } + +} +``` + +用户端api接口的nginx配置文件参考《nginx安装与跨域配置》这篇文章,修改的配置文件路径`/mall4uni/utils/config.js`,同样打包上传到服务器,配置nginx即可 + + +