背景
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快速开发全栈导航网站文章中,我们从数据中获取网站数据并展示,例如你通过接口 https://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
,感谢。