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

2023年 10月 14日 35.5k 0

前言

最近开发一款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、宝塔、等..

    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

  • Update AL2023 Packages sudo dnf update
  • Installing Docker on Amazon Linux 2023 sudo dnf install docker
  • Start and Enable its Service sudo systemctl start docker
  • Install docker-compose

  • 从 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
  • 下载后修复权限 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
    

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

    bt.png

    安装 Nginx

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

    nginx.png

    添加站点

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

    • www.reverse.com
    • casdoor.reverse.com

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

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

    zd.png

    配置SSL证书

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

    zs.png
    这里我们使用 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

    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_PASSWORD 是DockerHub仓库的账号和密码,需要预先存储到 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 命令拉取镜像,并启动项目

  • cd /www/wwwroot/根目录
  • 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: () => ,
    });
    
    
      
      ...
    
    

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

    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
    
  • 为每个运行时(节点、浏览器、边缘)使用默认的 Sentry.init() 调用创建配置文件
    c1.png

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

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

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

  • 错误捕获

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

    生产环境捕获错误

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

  • use mysql;
  • update user set host='%' where user='root'
  • FLUSH PRIVILEGES;
  • mysql.png

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论