背景
3月份时,想着用ChatGPT
的Api
接口来开发一个简单的小程序来玩玩,这也是我第一次开发小程序,为了快速开发,我最后选定了uniapp
+ uncloud
云开发,原因是使用uniapp
我可以使用Vue3
+Tailwindcss
快速构建页面UI
和组织逻辑,使用unicloud
是因为我需要数据库存储服务并想快速开发后台接口。实际使用下来,unicloud
也确实省去了很大的环境准备或者部署工作,于是我的第一个小程序就这样上线了,但是来了,Unicloud
也有很多缺点
-
收费
最便宜一个月20,关键这其中云函数资源使用量 = 函数配置内存 X 运行计费时长,10万GBs很可能不够。
-
开发工具和代码写法强要求
unicloud和Hbuilder编辑器是强绑定的,你必须使用这个编辑器,并且比如数据库操作也有一套它指定的写法,那么这就会有一定的学习成本
-
node
版本太低使用中发现,unicloud云函数的node版本最大好像是14,具体版本我忘了,目前node稳定版都18了,并且很多npm包都要求更高版本了,这就使得很多包无法直接使用
最近我在学习Next.js
并用其开发了一个导航网站时,体验下来我突然发现,这不就是免费的云服务么,于是我放弃了Unicloud
转而使用Next.js
重构了我的小程序,便有了这篇文章,以个人小程序举例,结合Nextjs
来实现0成本开发小程序。
总的来看,如果你不太熟悉nodejs
开发,愿意付费,但是想快速开发后端服务时,它提供了比如微信支付、图形验证码、文本内容安全识别等很多插件,只要简单熟悉他指定的一些语法并可上手开发了,这种情况下推荐unicloud
,但如果你熟悉nodejs
开发,一些简单的业务接口和数据库处理就能满足要求,那么免费的更符合我们熟知的云函数写法可能选择更好。
?本项目已上线,你可以微信小程序搜索:智障问答Bot 来体验,同时本项目也已开源,你可以在项目开源地址查看完整代码
实践
在我的使用Nextjs快速开发全栈导航网站文章中,我们从数据中获取网站数据并展示,例如你通过接口 webnav.codefe.top/api/links 便可以获取页面所有数据。接下来以一个ChatGPT
小程序来实践,功能包括用户登录注册、ChatGPT
接口调用生成文案两个功能。
小程序基础目录结构如下
├── index.html
├── package.json
├── project.config.json
├── README.md
├── src
│ ├── apis
│ ├── App.vue
│ ├── components
│ ├── composables
│ ├── layouts
│ ├── main.ts
│ ├── manifest.json
│ ├── pages
│ ├── pages.json
│ ├── static
│ ├── stores
│ ├── theme.json
│ └── uni.scss
├── tsconfig.json
├── unocss.config.ts
└── vite.config.ts
页面UI
部分没啥特别的,使用了uniapp
+ Vite
+ UnoCSS
+ Pinia
+ uview-plus
,用这套下来开发体验还可以,就是有部分Unocss
的写法还没支持全,偶尔会遇到写了但是没生效的问题,页面UI
如下。
用户注册
使用下面命令初始化后台server
部分
npx create-next-app@latest server
创建用户表,我这使用的是MongoDB官网提供的免费版本,使用Prisma ORM
工具操作数据库,关于其使用可以参考我上一篇文章。执行
npx prisma init
会创建prisma/schema.prisma
文件,创建模型如下
generator client {
provider = "prisma-client-js"
}
enum Role {
ADMIN
USER
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
nickname String?
wx_openid String @unique
credit Int?
role Role @default(USER)
avatar String?
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt
@@map(name: "user")
}
其中
provider
指明我们是使用mongdb
wx_openid
是用户微信小程序的唯一openid
credit
标识用户对话可使用次数role
标识用户角色
接下来创建注册接口,注册流程图如下
新增app/api/auth/route.ts
文件,代码如下
import { NextRequest } from "next/server";
import prisma from '@/lib/db'
import type { User } from "@prisma/client";
import { signJWT } from "@/lib/utils";
const { WX_APPID, WX_SECRET, DEFAULT_CREDIT } = process.env;
/**
* 注册用户
* @param req
* @returns
*/
export async function POST(req: NextRequest) {
const body = await req.json();
const { code } = body;
const response = await fetch(`https://api.weixin.qq.com/sns/jscode2session?appid=${WX_APPID}&secret=${WX_SECRET}&js_code=${code}&grant_type=authorization_code`)
const jscode2session = await response.json();
let user: Partial<User | null> = await prisma.user.findUnique({
where: {
wx_openid: jscode2session.openid,
},
})
if (!user) {
user = await prisma.user.create({
data: {
wx_openid: jscode2session.openid,
credit: Number(DEFAULT_CREDIT),
},
})
}
const token = await signJWT({ sub: user.id! }, { exp: '7d' });
delete user.wx_openid;
return new Response(JSON.stringify({
status: "success",
data: {
token,
user,
},
}))
}
该文件创建了一个对应/api/auth
的POST
请求路径接口,从参数中获取小程序调用uni.login()
获取到的code
,通过小程序appid
和app_secret
调用微信接口获取用户的小程序openid
, 然后通过prisma.user.create
创建保存用户到数据库,生成jwt
并后续用其来处理接口认证。
鉴权中间件
根目录下创建middleware.ts
文件,代码如下
import { NextRequest, NextResponse } from "next/server";
import { getErrorResponse, verifyJWT } from "./lib/utils";
export async function middleware(req: NextRequest) {
let token: string | undefined;
if (req.headers.get("Authorization")?.startsWith("Bearer ")) {
token = req.headers.get("Authorization")?.substring(7);
}
if (!token) {
return getErrorResponse(
401,
"You are not logged in. Please provide a token to gain access."
);
}
const response = NextResponse.next();
try {
if (token) {
const { sub } = await verifyJWT<{ sub: string }>(token);
(req as AuthenticatedRequest).user = { id: sub };
response.headers.set("X-USER-ID", sub);
}
} catch (error) {
return getErrorResponse(401, "Token is invalid or user doesn't exists");
}
return response;
}
export const config = {
matcher: ["/api/user/:path*", "/api/chat-stream"],
};
从header
头中渠道jwt
并解析出userId
, 并注入到X-USER-ID
,后续请求从这个字段取,其中matcher
配置了需要鉴权的api
路径
对话接口
接下来创建app/api/chat-stream/route.ts
文件,代码如下
import type { NextRequest } from "next/server";
import prisma from '@/lib/db'
import { ChatGPTMessage, OpenAIStream, OpenAIStreamPayload } from "@/lib/openAIStream";
import { User } from "@prisma/client";
const handler = async (req: NextRequest) => {
const body = await req.json();
const { messages } = body;
const userId = req.headers.get("X-USER-ID");
const user: User | null = await prisma.user.findUnique({
where: {
id: userId!,
},
});
const isAdmin = user.role === 'ADMIN';
if (!isAdmin && (!user.credit || user.credit <= 0)) {
return new Response(JSON.stringify({
status: "fail",
message: '使用次数不足',
}))
}
if (!messages) {
return new Response(JSON.stringify({
status: "fail",
message: '请先输入你的问题',
}))
}
const payload: OpenAIStreamPayload = {
model: "gpt-3.5-turbo",
messages,
temperature: 0.7,
top_p: 1,
max_tokens: 800,
stream: true,
};
const stream = await OpenAIStream(payload);
!isAdmin && await prisma.user.update({
where: {
id: userId!,
},
data: {
credit: {
decrement: 1
},
},
})
return new Response(stream);
};
export { handler as POST, handler as GET };
首先根据X-USER-ID
查询用户,如果用户使用次数不足终止请求,接下来调用openai
对话API
接口处理,结束后通过decrement
将用户可使用次数减1。
部署
vercel
部署时选择server
文件夹,构建命令为
pnpx prisma generate && next build
填入环境变量信息后点击部署即可
总结
本文以自己的小程序举例,前端页面通过微信小程序平台托管,后台服务使用Next.js
开发,通过vercel
免费托管,结合免费mongodb
实现了0成本开发和上线微信小程序,本项目已上线,你可以微信小程序搜索:智障问答Bot 来体验,同时本项目已开源,你可以在项目开源地址查看完整代码,希望这篇文章对大家有所帮助,欢迎体验和star
,感谢。
本文首发于个人博客