前言
最近开发一款AI聊天应用(工作助手),内核调用的是OpenAI chatGPT的接口
技术栈
用到的技术栈有 Next.js、Express、Prisma、Casdoor、Sentry、Mysql、微信支付、宝塔运维
服务器 Amazon AWS
创建EC2
应用程序和操作系统
这里我选的是Amazon Linux,架构是64位(X86)
密钥对
创建你的密钥 🔑,我选的 .pem 格式,下载保存到你的本地
网络设置
创建安全组
- 允许来自SSH流量 ✅
- 允许来自互联网的HTTPS流量 ✅
- 允许来自互联网HTTP流量 ✅
存储卷
有资格使用免费套餐的客户最多可获得 30GB 的通用型 (SSD) 或磁存储空间,所以把磁盘拉满到30GB
通过SSH客户端连接服务器
实例创建成功后,会显示SSH连接服务器的代码
chmod 400 reverse-aws.pem
ssh -i "reverse-aws.pem" ec2-user@ec2-16-226-209-217.ap-northeast-3.compute.amazonaws.com
配置安全组
在实例页的【安全】里,打开端口访问权限,编辑入站规则:
App应用、 Mysql数据库、Casdoor、宝塔、等..
Rout53 绑定域名
托管区域
在 Route53 下的托管区,创建记录
把域名(三级域名) 和 IP 关联起来
- 主应用服务:www.reverse.com
- 用户注册登陆入口:casdoor.reverse.com
安装 Docker、Docker-Compose
我不建议到处查(百度、Google),直接去Docker官网 - Manuals - Docker Engine - install,找到对应的系统(CentOS、Debian 或 Ubuntu)一步一步的执行
这里可能有个坑(也许你遇不到)就是安装时报:
/etc/yum.repos.d/docker-ce.repo
这个文件里面的地址可能是404
即使你 sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
重新指定了 repo 的地址,也可能是 404
必过的安装
Install docker
sudo dnf update
sudo dnf install docker
sudo systemctl start docker
Install docker-compose
curl -SL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose version
❗️这里重点说明一下,不要下 docker-compose 1.x.x
版本的,docker-compose --env-file .env up
不识别, 下 2.x.x
✅
容器和镜像的基本操作
- 查看容器(包括隐藏)
docker ps -a
- 停止容器
docker stop containerId
- 删除容器
docker rm containerId
- 查看镜像
docker images
- 删除镜像
docker rmi imageId
宝塔运维
在宝塔官网找到自己系统的安装脚本,也可以选万能安装脚本
if [ -f /usr/bin/curl ];then curl -sSO https://download.bt.cn/install/install_panel.sh;else wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh;fi;bash install_panel.sh ed8484bec
安装成功之后会出现如下提示,将给出外网、内网地址、 账户和密码信息
安装 Nginx
进入宝塔管理后台之后先把 nginx 安装好,之后在软件商店的【已安装】里把nginx 在首页展示,方便重启应用
添加站点
我们需要创建 3 个站点,应用服务是一个,casdoor后台管理是一个,member用户订单详情是一个
- www.reverse.com
- casdoor.reverse.com
当我们创建好站点之后,宝塔会在系统中分配我们的项目地址(目录)
⚠️注意 主应用 和 注册站点 的根目录都写: /www/wwwroot/www.reverse.com/
配置SSL证书
我们需要给站点申请证书,不然提示 SSL证书 是 未部署 的状态
这里我们使用 Let's Encryt 创建证书,⚠️注意:我们需要把 强制HTTPS ✅ 开启
这里一并记得要把 站点备份
反向代理
因为是 docker-compose 容器启动的服务,所以访问服务器中容器的端口,需要 nginx 配置反向代理,才能真正的访问到
因为App应用的容器端口是 3000,Casdoor的容器端口是 8000,所以我们需要
- 源URL:www.reverse.com ---> 目标URL: http://localhost:3000
- 源URL:casdoor.reverse.com ---> 目标URL: http://localhost:8000
设置完毕,记得重载配置
此时再回到站点的【配置文件】你发现,已经引入了该站点【反向代理】的配置文件
APP应用
docker-compose.yml
项目要 Docker-compose 来部署,下面是 .yml
文件,由 3 个服务组成 app、db(mysql)、casdoor。
app服务已预先打成镜像存在我自己的 dockerHub 仓库里
version: "3"
services:
app:
image: myhub/reverse:latest
ports:
- 3000:3000
env_file:
- .env.local
environment:
- DATABASE_URL=${DATABASE_URL}
- OPENAI_API_KEY=${OPENAI_API_KEY}
depends_on:
- db
restart: always
volumes:
- ./.sentryclirc:/app/.sentryclirc
db:
image: mysql:latest
ports:
- 3306:3306
restart: always
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
environment:
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
- MYSQL_DATABASE=${DB_NAME}
volumes:
- ./mysql/data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
expose:
- 3306
casdoor:
image: casbin/casdoor:latest
ports:
- 8000:8000
environment:
RUNNING_IN_DOCKER: "true"
depends_on:
- db
restart: always
volumes:
- ./conf:/conf/
Dockerfile
Dockerfile 内容里值得注意的是,RUN npx prisma generate
这条指令生成 Prisma 客户端
ENTRYPOINT ["sh", "-c", "yarn run init-prod-db && node server.js"]
这条命令实现数据库的迁移 migrate
FROM node:18-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN set -eux;
sed -i 's/npmmirror.com/npmjs.org/g' yarn.lock;
yarn install;
FROM base AS builder
RUN apk update && apk add --no-cache git
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV DATABASE_URL=""
RUN npx prisma generate;
yarn build;
FROM base AS runner
WORKDIR /app
ENV PROXY_URL=""
ENV OPENAI_API_KEY=""
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/server ./.next/server
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma
EXPOSE 3000
ENTRYPOINT ["sh", "-c", "yarn run init-prod-db && node server.js"]
Github Actions Workflow
通过 Github Actions WorkFlow 工作流自动构建、打包、发布镜像到 DockerHub
监听 Push、Pull 事件(自动构建镜像)
name: Docker Image CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: docker build . --file Dockerfile --tag myApp:$(date +%s)
发布镜像到 DockerHub
name: Publish Docker image
on:
workflow_dispatch:
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
-
name: Check out the repo
uses: actions/checkout@v3
-
name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: mydocker/myApp
tags: |
type=raw,value=latest
type=ref,event=tag
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
secrets.DOCKER_USERNAME
和 secrets.DOCKER_PASSWORD
是DockerHub仓库的账号和密码,需要预先存储到 github 的环境变量里
把 docker-compose.yml 文件迁移到服务器
把 docker-compose.yml 、.env 、casdoor.conf 等必须的文件迁移到服务器中项目的根目录中
-
conf/app.conf:是 casdoor 的配置文件,在docker-compose.yml 的 casdoor服务中 volumns(卷) 中已经与容器内部的默认配置路径挂载
-
.env:中存储了
- OPENAI_API_KEY
- 数据库信息:
DATABASE_URL、DB_USERNAME、DB_PASSWORD、DB_NAME
- 微信支付证书:
APICLIENTCERT、APICLIENTKEY
- Casdoor证书:
CASDOOR_CLIENT_ID、CASDOOR_CLIENT_SECRET、CASDOORCERT
-
.sentryclirc:里存储了sentry 的
auth token
-
init.sql:中存储了数据库初始化时创建库表的脚本
我们在宝塔的【文件】中找到 项目根目录 然后上传以上文件(或者拖入)
启动项目
我们在客户端终端通过SSH连接服务器,进入到项目根目录,运行 docker-compose 命令拉取镜像,并启动项目
cd /www/wwwroot/根目录
docker-compose --env-file .env up
docker 守护进程
如果启动的过程中报以下错 ❌,有可能是 docker的守护进程 没有开启,或者残留网络导致
⠿ Network xxx_default Error
failed to create network xxx_default: Error response from daemon: Failed to Setup IP tables: Unable to enable SKIP DNAT rule: (iptables failed: iptables --wait -t nat -I DOCKER -i br-41dcbe0ed35b -j RETURN: iptables: No chain/target/match by that name.
检查守护进程: sudo systemctl status docker
开启守护进程:sudo systemctl start docker
清理残留的 Docker 网络:sudo docker network prune
Prisma在项目中的应用
Prisma 是Node.js 和 TypeScript的 ORM,直观的数据模型、自动迁移、类型安全和自动完成功能,让前端也能玩转数据库
安装 prisma 和 prisma Client
yarn add prisma @prisma/client
实例化 PrismaClient
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;
创建 schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid()) @db.VarChar(100)
username String @unique
password String
avatar String? @db.VarChar(500)
email String? @db.VarChar(100)
phone String? @db.VarChar(20)
weChat String? @db.VarChar(100)
createdAt String? @db.VarChar(100)
usages Int @default(0)
isMember Boolean @default(false)
memberExpirationDate String? @db.VarChar(100)
subscriptionId String?
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
orders Order[]
// 一个用户可以有很多订单
// 最新订单的截止时间,就是会员到期时间
}
model Subscription {
id String @id
type String // 套餐类型:三天、月卡、季卡、年卡
price Float // 套餐价格
users User[]
orders Order[] // 一个档位里可以有很多订单
}
model Order {
id Int @id @default(autoincrement())
orderNumber String @unique
userId String // 订单肯定是某用户创建的
subscriptionId String
createdAt String // 订单创建时间
amount Float // 订单金额
subscriptionType String // 订阅类型
transaction_id String // 交易id
success_time String // 交易时间
// 关联关系
user User @relation(fields: [userId], references: [id])
subscription Subscription @relation(fields: [subscriptionId], references: [id])
}
初始化数据库脚本
"scripts": {
"init-dev-db": "prisma migrate dev --name init", // dev
"init-prod-db": "prisma migrate deploy" // prod
},
用migrate
命令创建一个新的 SQL 迁移文件
我们就可以使用 prisma对象 访问数据库里的数据
import { NextResponse, NextRequest } from "next/server";
import prisma from "/db/prisma";
export async function POST(req: NextRequest) {
const { id } = await req.json();
try {
const user = await prisma.user.findUnique({
where: { id },
});
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
const updatedUser = await prisma.user.update({
where: { id },
data: {
usages: user.usages + 1,
},
});
return NextResponse.json({ usages: updatedUser.usages }, { status: 200 });
} catch (error) {
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
casdoor在项目中的应用
身份访问管理(IAM)/单点登录(SSO)平台,支持OAuth 2.0、OIDC、SAML和CAS,与Casbin RBAC和ABAC权限管理集成
Casdoor就是一道安检门🚪,检查你是否是一个买了票 🎫 的游客,这个门功能很强,使用起来又很简单
创建 OAuth 路由
当我们需要登陆的时候,要先路由到OAuth页 您还没有登录,请前往[登录](/#/oauth)页面登录
或 navigate(Path.OAuth)
const OAuthPage = dynamic(async () => (await import("./oAuth")).OAuthPage, {
loading: () => ,
});
...
CASDOOR 配置
// constant.ts
export const CASDOOR = {
endpoint: "https://casdoor.yourdomain.com", // casdoor服务地址
clientId: "1b424r587rf0b11258", // casdoor应用生成的
appName: "reverse",
organizationName: "reverse", // 对应casdoor后台创建的组织名称
redirectPath: "/#/oauth", // casdoor验证后回调的前端路由
clientSecret: "3407d3f4f5ce5c55476iaf1d3e6141g5090793sq8", // casdoor应用生成的
signinPath: "/api/signin", // 你自己项目的后端接口(与casdoor无关)
};
CasdoorSDK.signin
// Setting.ts
import Sdk from "casdoor-js-sdk";
import { CASDOOR } from "./constant";
// 去casdoor那之前,你需要带上你是从哪里来的 的信息
const serverUrl = location.origin;
const sdkConfig = {
serverUrl: CASDOOR.endpoint,
clientId: CASDOOR.clientId,
appName: CASDOOR.appName,
organizationName: CASDOOR.organizationName,
redirectPath: CASDOOR.redirectPath,
signinPath: `${serverUrl}${CASDOOR.signinPath}`,
};
export const CasdoorSDK = new Sdk(sdkConfig);
export const setJwt = (jwt: string) => {
localStorage.setItem("jwt", jwt);
};
export const getJwt = () => {
return localStorage.getItem("jwt");
};
export const logout = () => {
localStorage.removeItem("jwt");
};
export const setUserInfo = (userInfo: string) => {
localStorage.setItem("userInfo", userInfo);
};
export const getUserInfo = () => {
return localStorage.getItem("userInfo");
};
export function signin(params: {
code?: string;
state?: string;
successCb: () => void;
failCb: () => void;
}) {
// 如果没有code或者state,说明未登陆过
if (params.code === undefined || params.state === undefined) {
// 引导到casdoor的登陆注册页
window.location.href = CasdoorSDK.getSigninUrl();
return;
}
// 登陆过了,携带着code和state去换token
return CasdoorSDK.signin(
serverUrl, // casdoor知道你是从哪来的
CASDOOR.signinPath, // 后端登陆接口(给你code和state去获取token)
params.code,
params.state,
).then((res: any) => {
if (res.status === "ok") {
setJwt(res.data);
params.successCb();
} else {
params.failCb();
}
return res;
});
}
OAuth UI组件
// OAuth.tsx
import { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import * as Setting from "../Setting";
export function OAuthPage() {
// 初始是没有token的,所以authing为true,正在验证ing
const [authing, setAuthing] = useState(!localStorage.getItem("jwt"));
const navigate = useNavigate();
const location = useLocation();
async function saveUserInfo() {
try {
const token = Setting.getJwt() as string;
// 去我们自己项目的后端服务,验证token的有效性
const response = await fetch(`/api/parse-jwt-token`, {
method: "POST",
body: JSON.stringify({ token }),
});
const data = await response.json();
// token有效的话,把返回的用户信息本地缓存
Setting.setUserInfo(JSON.stringify(data?.data));
} catch (error) {
console.error("Failed to fetch data:", error);
}
}
useEffect(() => {
const fetchData = async () => {
// 抓取url中的query参数
const searchParams = new URLSearchParams(location.search);
Setting.signin({
code: searchParams.get("code") ?? undefined,
state: searchParams.get("state") ?? undefined,
successCb() {
// 验证成功后修改authing状态
setAuthing(false);
},
failCb() {
console.error("认证失败,请重试");
},
});
// 此时已经验证(登陆)了,还要去验证token的有效性
if (!authing) {
try {
saveUserInfo();
navigate("/home");
} catch (error) {
console.error("获取用户信息失败:", error);
}
}
};
fetchData();
}, [authing, location.search, navigate]);
return (
{authing ? "认证中,请稍后..." : "认证成功,即将跳转..."}
);
};
获取 Token 后端接口
import { NextResponse, NextRequest } from "next/server";
import { CASDOOR } from "@/app/constant";
import { SDK } from "casdoor-nodejs-sdk";
// 你的 casdoor 公钥证书,在 casdoor 面板中可以找到
export async function POST(req: NextRequest) {
const authCfg: any = {
endpoint: CASDOOR.endpoint,
clientId: CASDOOR.clientId,
clientSecret: CASDOOR.clientSecret,
certificate: Buffer.from(process.env.CASDOORCERT),
orgName: CASDOOR.organizationName,
appName: CASDOOR.appName,
};
const sdk = new SDK(authCfg);
const searchParams = req.nextUrl.searchParams;
const code = searchParams.get("code");
const state = searchParams.get("state");
if (code === null || state === null) {
return NextResponse.json(
{ error: "Invalid code or state" },
{ status: 400 },
);
}
try {
const result = await sdk.getAuthToken(code);
return NextResponse.json({ data: result, status: "ok" }, { status: 200 });
} catch (error) {
return NextResponse.json({ error, status: "error" }, { status: 500 });
}
}
验证 Token 后端接口
import { NextRequest, NextResponse } from "next/server";
import { CASDOOR } from "@/app/constant";
import { SDK } from "casdoor-nodejs-sdk";
export async function POST(req: NextRequest) {
const { token } = await req.json();
const authCfg: any = {
endpoint: CASDOOR.endpoint,
clientId: CASDOOR.clientId,
clientSecret: CASDOOR.clientSecret,
certificate: Buffer.from(process.env.CASDOORCERT),
orgName: CASDOOR.organizationName,
appName: CASDOOR.appName,
};
const sdk = new SDK(authCfg);
try {
const result = await sdk.parseJwtToken(token);
return NextResponse.json({ data: result }, { status: 200 });
} catch (error) {
return NextResponse.json({ error }, { status: 500 });
}
}
微信支付 wechatpay-node-v3
wechatpay-node-v3 是微信支付平台官网推荐的包,封装了h5支付、native支付、app支付、JSAPI或小程序支付等多种方式
实例化 WxPay
import WxPay from 'wechatpay-node-v3';
import fs from 'fs';
const pay = new WxPay({
appid: '直连商户申请的公众号或移动应用appid',
mchid: '商户号',
publicKey: fs.readFileSync('./apiclient_cert.pem'), // 公钥
privateKey: fs.readFileSync('./apiclient_key.pem'), // 秘钥
});
Native支付前端代码
async function nativaPay() {
const params = {
description: 'nativepay',
out_trade_no: '订单号', // 需要生成订单号
notify_url: '回调url', // 你项目的home页路由
amount: {
total: 1, // 支付金额
},
scene_info: {
payer_client_ip: 'ip',
},
};
const res = await fetch("/api/transactions_native", {
method: "POST",
mode: "cors",
body: JSON.stringify({ params }),
});
return res;
};
Native支付后端代码
// /api/transactions_native
export async function POST(req: NextRequest) {
const { params } = await req.json();
try {
const result = await pay.transactions_native(params);
if (result.status === 200) {
const codeUrl = result?.code_url;
// 解析code_url,生成二维码图片
const qrUrl = await new Promise((resolve, reject) => {
qrcode.toDataURL(codeUrl, (error, url) => {
if (error) {
console.error(error);
reject(error);
} else {
resolve(url);
}
});
});
return NextResponse.json({ data: { qrUrl } }, { status: 200 });
};
return NextResponse.json({ error: result.message }, { status: 500 });
} catch (error) {
return NextResponse.json({ error }, { status: 500 });
};
};
查询订单结果前端
// 每2秒执行一次查询订单结果
function startPollingOrderResult() {
pollingInterval = setInterval(queryOrderResult, 2000);
};
async function queryOrderResult() {
const res = await fetch("/api/order-query", {
method: "POST",
mode: "cors",
body: JSON.stringify({ out_trade_no }),
});
const data = await res.json();
};
查询订单结果后端
import { NextResponse, NextRequest } from "next/server";
import prisma from "@/app/db/prisma";
import WxPay from "wechatpay-node-v3";
export async function POST(req: NextRequest) {
const { out_trade_no} = await req.json();
const pay = new WxPay(payOptions as Ipay);
try {
const result = await pay.query({ out_trade_no });
if (result?.trade_state === "SUCCESS") {
// 把用户修改成会员
try {
const updatedUser = await prisma.user.update({
where: { id: userId },
data: { isMember: true },
});
NextResponse.json({ data: updatedUser }, { status: 200 });
} catch (error) {
NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
// 订单支付成功,存储订单信息到 Order 表
// 存会员到期时间
// 数据存储成功,计算会员到期时间
// 根据订阅类型计算会员到期时间
// 数据存储成功,返回成功提示给前端
};
};
} catch (error) {
return NextResponse.json({ error }, { status: 500 });
};
};
Casdoor
证书
Casdoor 原有的 cert-built-in 证书不要动,创建一个你应用的证书,例如:reverse-cert
组织:选择新创建的组织(reverse),一个证书对应一个应用
证书:把生成的 证书 复制到项目根目录中 .env 中的 CASDOORCERT
变量中
提供商
提供商是第三方注册登陆的服务,例如:
- 微信扫码登陆
- 阿里云SMS短信验证码服务
- 邮箱注册验证码接收
微信扫码登陆服务
这里的微信 Client ID
和 Client secret
是在微信开放平台申请获取
阿里云SMS短信服务
阿里云SMS短信的 Client ID
和 Client secret
是在阿里云 - RAM访问控制 - 访问凭证管理 中申请
邮箱验证码服务
应用
Casdoor 默认的 built-in 应用不删除也不要编辑,创建一个新的应用,例如:reverse
组织:选择新创建的组织(reverse),一个应用对应一个组织
Client ID: 把生成的 Client ID 复制到 源码 casdoor 配置的 clientId
中
Client secret:把生成的 Client secret 复制到 源码 casdoor 配置的 clientSecret
中
证书:选择新创建的 cert-reverse
重定向 URLs:这个要看你项目中用来做 oAuth认证 的前端路由地址,我的认证路由是 /oauth
提供商: 我们需要添加上文(提供商)中创建出的微信、短信、邮箱三个服务商
组织
Casdoor默认的组织不要动,创建一个新的组织,例如:reverse
默认应用:选择新创建的应用(reverse)
同步器
默认的 Casdoor 数据库和表最好不要动,创建我们应用自己的库和表,但是 Casdoor 的 user 用户表
怎么同步到我们自己创建的表中那,这时需要用到 同步器
要开启 已启用 才会在源表(Casdoor)有改动的时候,自动同步到目标表
Sentry
Sentry is a developer-first error tracking and performance monitoring platform.
Sentry是一个错误跟踪和性能监控平台
自动执行
在你的 应用项目 路径内执行以下命令,Sentry将自动执行内部命令,具体做了什么?
npx @sentry/wizard@latest -i nextjs
为每个运行时(节点、浏览器、边缘)使用默认的 Sentry.init() 调用创建配置文件
使用默认Sentry配置创建或更新 Next.js 配置
创建 .sentryclirc 用于授权的值(此文件会自动添加到 .gitignore)
向应用添加示例页面以验证哨兵设置
错误捕获
登陆 Sentry 已查询捕获的错误
生产环境捕获错误
productionBrowserSourceMaps: true
debug: true
Mysql
如果在启动 数据库镜像 时不能连接到 Mysql,通过进入容器内部 docker exec -it containerId /bin/sh(bash)
检查 初始密码 与你 .env文件里的DB_PASSWORD
是否一致,或需要重置密码
denied access
如果镜像启动的时候报了Error: P1010: User root was denied access on the database mysql
,可以试着给 root 放开点权限
use mysql;
update user set host='%' where user='root'
FLUSH PRIVILEGES;