全链路AI聊天应用开发部署

608 阅读11分钟

前言

最近开发一款AI聊天应用(工作助手),内核调用的是OpenAI chatGPT的接口

技术栈

用到的技术栈有 Next.js、Express、Prisma、Casdoor、Sentry、Mysql、微信支付、宝塔运维

expressImg.png

服务器 Amazon AWS

创建EC2

应用程序和操作系统

这里我选的是Amazon Linux,架构是64位(X86)

密钥对

创建你的密钥 🔑,我选的 .pem 格式,下载保存到你的本地

网络设置

创建安全组

  • 允许来自SSH流量 ✅
  • 允许来自互联网的HTTPS流量 ✅
  • 允许来自互联网HTTP流量 ✅

存储卷

有资格使用免费套餐的客户最多可获得 30GB 的通用型 (SSD) 或磁存储空间,所以把磁盘拉满到30GB

通过SSH客户端连接服务器

实例创建成功后,会显示SSH连接服务器的代码

  1. chmod 400 reverse-aws.pem
  2. ssh -i "reverse-aws.pem" ec2-user@ec2-16-226-209-217.ap-northeast-3.compute.amazonaws.com

配置安全组

在实例页的【安全】里,打开端口访问权限,编辑入站规则:

App应用、 Mysql数据库、Casdoor、宝塔、等..

aq.png

Rout53 绑定域名

托管区域

Route53 下的托管区,创建记录

域名(三级域名)IP 关联起来

  • 主应用服务:www.reverse.com
  • 用户注册登陆入口:casdoor.reverse.com

ym.png

安装 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

  1. Update AL2023 Packages sudo dnf update
  2. Installing Docker on Amazon Linux 2023 sudo dnf install docker
  3. Start and Enable its Service sudo systemctl start docker

Install docker-compose

  1. 从 GitHub 复制相应的 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
  2. 下载后修复权限 sudo chmod +x /usr/local/bin/docker-compose
  3. 验证成功 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

安装成功之后会出现如下提示,将给出外网、内网地址、 账户和密码信息

bt.png

安装 Nginx

进入宝塔管理后台之后先把 nginx 安装好,之后在软件商店的【已安装】里把nginx 在首页展示,方便重启应用

nginx.png

添加站点

我们需要创建 3 个站点,应用服务是一个,casdoor后台管理是一个,member用户订单详情是一个

当我们创建好站点之后,宝塔会在系统中分配我们的项目地址(目录)

⚠️注意 主应用注册站点 的根目录都写: /www/wwwroot/www.reverse.com/

zd.png

配置SSL证书

我们需要给站点申请证书,不然提示 SSL证书未部署 的状态

zs.png 这里我们使用 Let's Encryt 创建证书,⚠️注意:我们需要把 强制HTTPS ✅ 开启

这里一并记得要把 站点备份

反向代理

因为是 docker-compose 容器启动的服务,所以访问服务器中容器的端口,需要 nginx 配置反向代理,才能真正的访问到

因为App应用的容器端口是 3000,Casdoor的容器端口是 8000,所以我们需要

fd.png 设置完毕,记得重载配置

cz.png 此时再回到站点的【配置文件】你发现,已经引入了该站点【反向代理】的配置文件

yr.png

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_USERNAMEsecrets.DOCKER_PASSWORDDockerHub仓库的账号和密码,需要预先存储到 github 的环境变量里

hj.png

把 docker-compose.yml 文件迁移到服务器

把 docker-compose.yml 、.env 、casdoor.conf 等必须的文件迁移到服务器中项目的根目录

dc.png

  • 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:中存储了数据库初始化时创建库表的脚本

我们在宝塔的【文件】中找到 项目根目录 然后上传以上文件(或者拖入)

tr.png

启动项目

我们在客户端终端通过SSH连接服务器,进入到项目根目录,运行 docker-compose 命令拉取镜像,并启动项目

  1. cd /www/wwwroot/根目录
  2. docker-compose --env-file .env up

lq.png

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

jc.png

Prisma在项目中的应用

prisma.png 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 迁移文件

qy.png

我们就可以使用 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在项目中的应用

casdoor.png 身份访问管理(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: () => <Loading noLogo />,
});
<Routes>
  <Route path={Path.OAuth} element={<OAuthPage />} />
  ...
</Routes>

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 (
    <div>
      {authing ? "认证中,请稍后..." : "认证成功,即将跳转..."}
    </div>
  );
};

获取 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或小程序支付等多种方式

we.png

实例化 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

zs1.png

组织:选择新创建的组织(reverse),一个证书对应一个应用

证书:把生成的 证书 复制到项目根目录中 .env 中的 CASDOORCERT 变量中

提供商

提供商是第三方注册登陆的服务,例如:

  • 微信扫码登陆
  • 阿里云SMS短信验证码服务
  • 邮箱注册验证码接收

tgs2.png

微信扫码登陆服务

wx.png 这里的微信 Client IDClient secret 是在微信开放平台申请获取

阿里云SMS短信服务

aly.png 阿里云SMS短信的 Client IDClient secret是在阿里云 - RAM访问控制 - 访问凭证管理 中申请

邮箱验证码服务

qq.png

应用

Casdoor 默认的 built-in 应用不删除也不要编辑,创建一个新的应用,例如:reverse

yy.png

组织:选择新创建的组织(reverse),一个应用对应一个组织

Client ID: 把生成的 Client ID 复制到 源码 casdoor 配置的 clientId

Client secret:把生成的 Client secret 复制到 源码 casdoor 配置的 clientSecret

证书:选择新创建的 cert-reverse

重定向 URLs:这个要看你项目中用来做 oAuth认证 的前端路由地址,我的认证路由是 /oauth

提供商: 我们需要添加上文(提供商)中创建出的微信、短信、邮箱三个服务商

tgs.png

组织

Casdoor默认的组织不要动,创建一个新的组织,例如:reverse

zz.png

默认应用:选择新创建的应用(reverse)

同步器

默认的 Casdoor 数据库和表最好不要动,创建我们应用自己的库和表,但是 Casdoor 的 user 用户表 怎么同步到我们自己创建的表中那,这时需要用到 同步器

tbq1.png

tbq2.png

tbq3.png 要开启 已启用 才会在源表(Casdoor)有改动的时候,自动同步到目标表

Sentry

Sentry is a developer-first error tracking and performance monitoring platform.

Sentry是一个错误跟踪和性能监控平台

自动执行

在你的 应用项目 路径内执行以下命令,Sentry将自动执行内部命令,具体做了什么?

npx @sentry/wizard@latest -i nextjs
  1. 为每个运行时(节点、浏览器、边缘)使用默认的 Sentry.init() 调用创建配置文件 c1.png

  2. 使用默认Sentry配置创建或更新 Next.js 配置 c2.png

  3. 创建 .sentryclirc 用于授权的值(此文件会自动添加到 .gitignore) c3.png

  4. 向应用添加示例页面以验证哨兵设置 c4.png

错误捕获

登陆 Sentry 已查询捕获的错误 w1.png

生产环境捕获错误

  1. 在 next.config.mjs 中开启 webpack productionBrowserSourceMaps: true
  2. 在 sentry.server.config 中 开启 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 放开点权限

  1. use mysql;
  2. update user set host='%' where user='root'
  3. FLUSH PRIVILEGES;

mysql.png

Reverse 应用

cp1.png

cp2.png

pc3.png

pc4.png