Commit 8aeef4e0 authored by gu-jinli1118's avatar gu-jinli1118
Browse files

20230831

parent 646116b0
Pipeline #31 failed with stages
in 0 seconds
> 我们的支付时不允许在订单的支付接口传订单金额的,所以我们采用了订单号进行支付的形式
## 支付
我们来到`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<OrderSettlement> 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<String> paySuccess(String payNo, String bizPayNo) {
List<OrderSettlement> orderSettlements = orderSettlementMapper.selectList(new LambdaQueryWrapper<OrderSettlement>().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<String> orderNumbers = orderSettlements.stream().map(OrderSettlement::getOrderNumber).collect(Collectors.toList());
// 将订单改为已支付状态
orderMapper.updateByToPaySuccess(orderNumbers, PayType.WECHATPAY.value());
List<Order> 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));
```
这里的事件也是和营销活动有关的,比如分销,这些代码也是商业版才有的。
这里只有几点说明:
1. 这里写的是接口设计,如果你整个接口的接口文档,只需要启动api这个项目,然后访问 http://localhost:8086/doc.html
(1)安装JDK
安装JDK,如果没有java-17-openjdk-devel就没有javac命令
```bash
yum install java-17-openjdk java-17-openjdk-devel
```
`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
```
## 安装 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/
```
## 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`
## 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`
> 在阅读本章节前,我们回认为您已经会安装并且使用docker,如果您不会安装使用docker的话,请阅读相关章节
**如果无法理解我们所编写的 `Dockerfile`强烈的不推荐使用docker进行生产环境部署!!!**
0. 将整个项目上传到centos中,进入到项目根目录
1. 安装 `docker` (参考《docker centos 安装》)
2. 安装`docker-compose`(参考《Docker Compose 安装与卸载》)
3. 安装`open-jdk1.8`(参考《centos jdk安装》)
4. 安装`maven`(参考《通过yum安装maven》)
5. 使用 `mvn clean package -DskipTests` 命令进行打包
6. 使用 `docker-compose up` 启动项目
7. 使用nginx将请求指向特定的端口。
安装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
```
本文为大家介绍了*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
```
本文为大家介绍了*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';
```
## 安装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,进入交互式命令行
## 安装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. 安装依赖启动项目
使用`vscode`打开vue项目,进入到该项目的根目录(根目录有`package.json` 的文件)
使用npm安装依赖(请记得配好淘宝的镜像源,除非你能确定你的网络能够畅快访问外网,另外千万不要用cnpm),使用命令行输入,回车:
```bash
npm i
```
如果用淘宝镜像装不上的话,报错信息含有“要安装python”的相关信息,就删掉node_modules 切换回原镜像试试
```bash
npm config set registry https://registry.npmjs.org/
```
##### 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:prod` 命令对项目进行打包
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即可
> 如果不理解oauth协议的推荐阅读 阮一峰的[理解OAuth 2.0](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)
当然,我们也要简单介绍下oauth的运行流程:
```
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
```
运行流程如下图,摘自RFC 6749。
- (A)用户打开客户端以后,客户端要求用户给予授权。
- (B)用户同意给予客户端授权。
- (C)客户端使用上一步获得的授权,向认证服务器申请令牌。
- (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
- (E)客户端使用令牌,向资源服务器申请获取资源。
- (F)资源服务器确认令牌无误,同意向客户端开放资源。
我们是对内的系统,并不需要那么复杂的流程,所以我们看下oauth的授权模式当中的密码模式:
```
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
```
这里的流程相对就比较简单了:
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
现在将简单的转换下思路:
- `Resource Owner`:资源拥有者,拥有订单,购物车等数据的人,既用户
- `Client`:客户端,浏览器
- `Authorization Server`:认证服务器,也就是服务器咯。
在此A、B、C三个流程就变成了:
(A)用户在浏览器输入用户名和密码。
(B)浏览器将用户名和密码发给服务器,向后者请求令牌(token)。
(C)服务器确认无误后,返回token给用户。
但是根据标准的流程,并没有验证码之类的容身之地。而`spring security oauth2` 给我们提供的只能是标准的流程,所以我们对代码进行一些适配,能够适应我们自己的需求。
## spring的部分源码
我们先来看下`spring security oauth2`的部分源码
首先我们直接进行授权的时候,调用的url大概为:`http://localhost:8080/oauth/token?username=user_1&password=123456&grant_type=password&scope=select&client_id=client_2&client_secret=123456`,那么授权肯定是与该链接相关联的。基于这个猜测,我们去寻找源码吧。
`idea`中使用全局搜索,搜索 字符串`"/oauth/token"`(带着引号),发现了一个类,似乎与这个请求有关 `ClientCredentialsTokenEndpointFilter`
```java
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
public ClientCredentialsTokenEndpointFilter() {
this("/oauth/token");
}
}
```
```
ClientCredentialsTokenEndpointFilter
---> AbstractAuthenticationProcessingFilter
---> GenericFilterBean
---> Filter
```
发现,这个类是一个 `Filter` 也就是过滤器,通过这个过滤器,过滤请求,那么,我们去看看`doFilter`方法咯,`doFilter``ClientCredentialsTokenEndpointFilter` 的父类 `AbstractAuthenticationProcessingFilter` 上。
我们看看`AbstractAuthenticationProcessingFilter`
```java
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 如果不是认证的请求,直接下一个filter
// 这里是怎么判断是否是下一个请求呢?
// 答:看看url是不是上面ClientCredentialsTokenEndpointFilter 创建时传过来的url,也就是 /oauth/token
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
// 调用attemptAuthentication 方法,返回一个 Authentication 的实现类,也就是认证信息,这个实现类非常重要!!!
authResult = attemptAuthentication(request, response);
// 如果找不到,那就没了
if (authResult == null) {
return;
}
}
// 调用成功的方法
successfulAuthentication(request, response, chain, authResult);
}
```
这里最重要的方法`attemptAuthentication` 生成一个授权信息,能够返回,则证明登录已经成功了,所以真正的登录与这里有关。
我们回到`ClientCredentialsTokenEndpointFilter` 这个实现类里面看看`attemptAuthentication`方法吧
```java
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
// ======精简没啥用的方法========
// 构造一个UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
// 调用认证方法进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
```
我们通过添加断点可以发现 `this.getAuthenticationManager()` 是一个`ProviderManager` 对象,我们看下
`this.getAuthenticationManager().authenticate()` 里面的 `authenticate`
```java
public class ProviderManager{
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Authentication result = null;
for (AuthenticationProvider provider : getProviders()) {
// 在一堆的provider中寻找到一个合适的授权提供者
if (!provider.supports(toTest)) {
continue;
}
// 由授权提供者进行授权
result = provider.authenticate(authentication);
}
if (result != null) {
return result;
}
}
}
```
一路追踪到这里,我们发现,实际上,是通过`provider.supports(toTest)` 寻找一个合适的授权提供者,使用`provider.authenticate(authentication)`就行授权,而`supports` 的依据是通过之前生成的token来判断是否支持:
```java
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
```
我们整理下这几个流程
```
ClientCredentialsTokenEndpointFilter.doFilter()
--> AbstractAuthenticationProcessingFilter.attemptAuthentication()
--> ProviderManager.authenticate()
--> AuthenticationProvider.supports()
--> AuthenticationProvider.authenticate()
```
我们可以看到这里主要就是干了几件事情
- 通过filter 确定登录要过滤的url
- 通过filter 确定生成的`AbstractAuthenticationToken` 比如 `UsernamePasswordAuthenticationToken`
- 通过生成的`AbstractAuthenticationToken` 确定`AuthenticationProvider`
- 通过`AuthenticationProvider` 最后调用 `authenticate()`方法最后进行授权
最后通过`RequestMapping` 返回
```java
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint{
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ServerResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
return getResponse(token);
}
}
```
通过【从授权开始看源码】我们可以看到这里主要就是干了几件事情
- 通过filter 确定登录要过滤的url
- 通过filter 确定生成的`AbstractAuthenticationToken` 比如 `UsernamePasswordAuthenticationToken`
- 通过生成的`AbstractAuthenticationToken` 确定`AuthenticationProvider`
- 通过`AuthenticationProvider` 最后调用 `authenticate()`方法最后进行授权
根据上面我们对自己对代码进行了一些封装
我们先来看`LoginAuthenticationFilter`
```java
public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationTokenParser authenticationTokenParser;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
AbstractAuthenticationToken authRequest = authenticationTokenParser.parse(requestBody);
return this.getAuthenticationManager().authenticate(authRequest);
}
public void setAuthenticationTokenParser(AuthenticationTokenParser authenticationTokenParser) {
this.authenticationTokenParser = authenticationTokenParser;
}
}
```
这里的登录继承了`UsernamePasswordAuthenticationFilter` 里面写了
```java
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
```
这就是为什么登录的接口是`/login`的原因
我们再来看看生成`AbstractAuthenticationToken `的方法
`AbstractAuthenticationToken authRequest = authenticationTokenParser.parse(requestBody);`
这里决定了生成什么token,将会决定后面的`AuthenticationProvider`
我们先来看`AdminAuthenticationProvider`
```
@Override
public boolean supports(Class<?> authentication) {
return AdminAuthenticationToken.class.isAssignableFrom(authentication);
}
```
这里决定`AdminAuthenticationToken` 是通过`AdminAuthenticationProvider` 进行校验
再来看下完整的`AdminAuthenticationProvider` 你就知道验证码在哪里校验的了,是不是很简单
```java
public class AdminAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private final YamiUserDetailsService yamiUserDetailsService;
private final PasswordEncoder passwordEncoder;
@Override
protected UserDetails retrieveUser(String username, Authentication authentication) throws BaseYamiAuth2Exception {
UserDetails user;
try {
user = yamiUserDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundExceptionBase var6) {
throw new UsernameNotFoundExceptionBase("账号或密码不正确");
}
if (!user.isEnabled()) {
throw new UsernameNotFoundExceptionBase("账号已被锁定,请联系管理员");
}
return user;
}
@Override
protected void additionalAuthenticationChecks(UserDetails sysUser, Authentication authentication) throws BaseYamiAuth2Exception {
AdminAuthenticationToken adminAuthenticationToken = (AdminAuthenticationToken) authentication;
String kaptchaKey = SecurityConstants.SPRING_SECURITY_RESTFUL_IMAGE_CODE + adminAuthenticationToken.getSessionUUID();
String kaptcha = RedisUtil.get(kaptchaKey);
RedisUtil.del(kaptchaKey);
if(StrUtil.isBlank(adminAuthenticationToken.getImageCode()) || !adminAuthenticationToken.getImageCode().equalsIgnoreCase(kaptcha)){
throw new ImageCodeNotMatchExceptionBase("验证码有误");
}
String encodedPassword = sysUser.getPassword();
String rawPassword = authentication.getCredentials().toString();
// 密码不正确
if (!passwordEncoder.matches(rawPassword,encodedPassword)){
throw new BadCredentialsExceptionBase("账号或密码不正确");
}
}
@Override
protected Authentication createSuccessAuthentication(Authentication authentication, UserDetails user) {
AdminAuthenticationToken result = new AdminAuthenticationToken(user, authentication.getCredentials());
result.setDetails(authentication.getDetails());
return result;
}
}
```
version: '3'
services:
mall4j-mysql:
build:
context: ./
dockerfile: ./db/Dockerfile
environment:
MYSQL_ROOT_PASSWORD: root
restart: always
container_name: mall4j-mysql
image: mall4j-mysql
ports:
- 3306:3306
volumes:
- ./mall4j-mysql:/var/lib/mysql
command: --lower_case_table_names=1
mall4j-redis:
image: redis:5.0.4
restart: always
container_name: mall4j-redis
ports:
- 6379:6379
mall4j-admin:
build:
context: ./
dockerfile: ./yami-shop-admin/Dockerfile
restart: always
container_name: mall4j-admin
image: mall4j-admin
ports:
- 8085:8085
depends_on:
- mall4j-redis
- mall4j-mysql
links:
- "mall4j-redis"
- "mall4j-mysql"
mall4j-api:
build:
context: ./
dockerfile: ./yami-shop-api/Dockerfile
restart: always
container_name: mall4j-api
image: mall4j-api
ports:
- 8086:8086
depends_on:
- mall4j-redis
- mall4j-mysql
links:
- "mall4j-redis"
- "mall4j-mysql"
\ No newline at end of file
This diff is collapsed.
一个基于vue、element ui 的轻量级、前后端分离、拥有完整sku和下单流程的完全开源商城 小程序端
## 前言
`mall4j商城`项目致力于为中小企业打造一个完整、易于维护的开源的电商系统,采用现阶段流行技术实现。后台管理系统包含商品管理、订单管理、运费模板、规格管理、会员管理、运营管理、内容管理、统计报表、权限管理、设置等模块。
## 授权
Mall4j官网 https://www.mall4j.com
Mall4j 使用 AGPLv3 开源,请遵守 AGPLv3 的相关条款,或者联系作者获取商业授权(https://www.mall4j.com)
## 项目链接
java后台:https://gitee.com/gz-yami/mall4j
vue中后台:https://gitee.com/gz-yami/mall4v
小程序:https://gitee.com/gz-yami/mall4m
## 演示地址
**由于我们并不希望小程序的数据被弄混乱,我们弄了两个数据库。因此,您修改了后台的商品信息,小程序并不能看到!**
后台:<http://mall4j-admin.mall4j.com> 账号:admin/123456
小程序:1. 扫描二维码
![小程序](https://gitee.com/gz-yami/mall4j/raw/master/screenshot/miniQrcode.jpg)
​ 2. 搜索小程序 **mall4j商城**
## 相关截图
![首页](https://gitee.com/gz-yami/mall4m/raw/master/screenshot/index.jpg)
![商品详情](https://gitee.com/gz-yami/mall4m/raw/master/screenshot/prodInfo.jpg)
![购物车](https://gitee.com/gz-yami/mall4m/raw/master/screenshot/shopCart.jpg)
![sku](https://gitee.com/gz-yami/mall4m/raw/master/screenshot/sku.jpg)
![我的](https://gitee.com/gz-yami/mall4m/raw/master/screenshot/my.jpg)
## 提交反馈
提问之前,请先阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md)
- QQ群:722835385
![QQ群](https://gitee.com/gz-yami/mall4j/raw/master/screenshot/qqGroup.png)
- 论坛:<http://bbs.mall4j.com>
- 商务邮箱:yamitech@163.com
\ No newline at end of file
//app.js
var http = require("utils/http.js");
App({
onLaunch: function () {
console.log('mall4j.v230313')
// http.getToken();
// wx.getSetting({
// success(res) {
// if (!res.authSetting['scope.userInfo']) {
// wx.navigateTo({
// url: '/pages/login/login',
// })
// }
// }
// })
},
globalData: {
// 定义全局请求队列
requestQueue: [],
// 是否正在进行登陆
isLanding: true,
// 购物车商品数量
totalCartCount: 0
}
})
\ No newline at end of file
{
"pages": [
"pages/index/index",
"pages/logs/logs",
"pages/user/user",
"pages/basket/basket",
"pages/category/category",
"pages/search-page/search-page",
"pages/delivery-address/delivery-address",
"pages/editAddress/editAddress",
"pages/orderList/orderList",
"pages/order-detail/order-detail",
"pages/submit-order/submit-order",
"pages/binding-phone/binding-phone",
"pages/express-delivery/express-delivery",
"pages/pay-result/pay-result",
"pages/search-prod-show/search-prod-show",
"pages/prod/prod",
"pages/login/login",
"pages/prod-classify/prod-classify",
"pages/recent-news/recent-news",
"pages/news-detail/news-detail"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "WeChat",
"navigationBarTextStyle": "black"
},
"tabBar": {
"selectedColor": "#3a86b9",
"color": "#b8b8b8",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/tabbar/homepage.png",
"selectedIconPath": "images/tabbar/homepage-sel.png"
},
{
"pagePath": "pages/category/category",
"text": "分类",
"iconPath": "images/tabbar/category.png",
"selectedIconPath": "images/tabbar/category-sel.png"
},
{
"pagePath": "pages/basket/basket",
"text": "购物车",
"iconPath": "images/tabbar/basket.png",
"selectedIconPath": "images/tabbar/basket-sel.png"
},
{
"pagePath": "pages/user/user",
"text": "我的",
"iconPath": "images/tabbar/user.png",
"selectedIconPath": "images/tabbar/user-sel.png"
}
]
},
"sitemapLocation": "sitemap.json"
}
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment