App router or Page router
T3 默认选项是Page Router,App router 还不稳定
正如在 T3-原则第二条 里提到的,我们热爱新技术,但是也看重稳定性,项目的整个路由不是很容易迁移,而你却在此使用风险较高的新技术,这不是一个明智的选择↗。
App Router 目前只是 对未来特性的一瞥而已↗,它还未做好准备被用于生产环境;这项 API 还处于 beta 阶段,可以预见接下来还会有破坏性的改变。
google登录
需要获取客户端id和密钥,在google控制台获取
还需要填写重定向路径
后端服务需要代理都则会出现登录超时 防止超时所以设置时间长一点
providers: [
GoogleProvider({
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
// 超时时间
httpOptions: {
timeout: 100000,
},
// // 下面的配置是为了让用户每次登录都能选择账号
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
trpc类型获取
export type GroupProps =
RouterOutputs["group"]["getInfiniteGroup"]["groups"][number];
trpc数据库使用
使用zod进行入参校验,查询使用query,修改使用mutation即可 查询:
import { type Prisma } from "@prisma/client";
import { z } from "zod";
import {
type createTRPCContext,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "~/server/api/trpc";
export const groupRouter = createTRPCRouter({
getAll: publicProcedure
.input(
z
.object({
name: z.string().optional(),
})
.default({
name: "",
}),
)
.query(async ({ ctx, input }) => {
const groups = await ctx.db.group.findMany({
where: {
...(input.name && { name: { contains: input.name } }),
deletedAt: null,
},
orderBy: {
createdAt: "desc",
},
});
return groups.map((group) => ({
...group,
rawPrice: convertCentsToDollars(group.rawPrice),
}));
}),
)}
const { data: groups } = api.group.getAll.useQuery();
创建:
create: protectedProcedure
.input(
z.object({
name: z.string().min(1),
description: z.string().min(1),
rawPrice: z.number().min(0),
}),
)
.mutation(async ({ ctx, input }) => {
const newGroup = await ctx.db.group.create({
data: {
name: input.name,
description: input.description,
rawPrice: convertDollarsToCents(input.rawPrice),
createdBy: { connect: { id: ctx.session.user.id } },
},
});
return newGroup;
}),
const createGroup = api.group.create.useMutation({
onSuccess: () => {
void router.push("/group/mine");
},
onError,
});
数据库连接命令
npx prisma migrate dev 数据库迁移同步数据库
prisma migrate deploy 在服务器部署数据库
prisma generate 生成数据库
无限加载实现
import { type Prisma } from "@prisma/client";
import { z } from "zod";
import {
type createTRPCContext,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "~/server/api/trpc";
import { convertCentsToDollars, convertDollarsToCents } from "~/utils/mount";
export const groupRouter = createTRPCRouter({
getInfiniteGroup: publicProcedure
.input(
z
.object({
name: z.string().optional(),
limit: z.number().optional(),
cursor: z.object({ id: z.string(), createdAt: z.date() }).optional(),
})
.default({
name: "",
}),
)
.query(async ({ input: { name, limit = 4, cursor }, ctx }) => {
return await getInfiniteGroups({
ctx,
whereClause: {
name: {
contains: name,
mode: "insensitive",
},
deletedAt: null,
},
limit,
cursor,
});
}),
}),
});
async function getInfiniteGroups({
whereClause,
ctx,
limit,
cursor,
}: {
whereClause?: Prisma.GroupWhereInput;
limit: number;
cursor: { id: string; createdAt: Date } | undefined;
ctx: Awaited<ReturnType<typeof createTRPCContext>>;
}) {
const userId = ctx.session?.user?.id;
const loggedIn = !!userId;
const groups = await ctx.db.group.findMany({
take: limit + 1,
cursor: cursor ? { id: cursor.id } : undefined,
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
where: whereClause,
select: {
id: true,
createdAt: true,
name: true,
description: true,
rawPrice: true,
members: loggedIn
? {
where: {
id: userId,
},
select: {
id: true,
},
}
: false,
createdBy: {
select: {
id: true,
},
},
},
});
let nextCursor: typeof cursor | undefined;
if (groups.length > limit) {
const nextItem = groups.pop();
if (nextItem) {
nextCursor = { id: nextItem.id, createdAt: nextItem.createdAt };
}
}
return {
groups: groups.map((group) => {
const isMember = loggedIn
? group.members.some((member) => member.id === userId)
: false;
const isOwner = loggedIn ? group.createdBy?.id === userId : false;
return {
...group,
rawPrice: convertCentsToDollars(group.rawPrice),
isMember,
isOwner,
memberCount: group.members?.length || 0,
};
}),
nextCursor,
};
}
客户端使用
import { useRouter } from "next/router";
import FloatingButton from "~/page_components/group/FloatingButton";
import SearchGroup from "~/page_components/group/SearchGroup";
import { Tabs } from "~/page_components/group/tabs";
import InfiniteArticleList from "~/shared/InfiniteArticleList";
import { api } from "~/utils/api";
const GroupsPage = () => {
const router = useRouter();
const groups = api.group.getInfiniteGroup.useInfiniteQuery(
{
name: router.query.q as string,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
);
return (
<div className="bg-nav h-full">
<div className="bg-nav sticky top-[56px] z-20">
<SearchGroup path="/group/all" />
<Tabs active="all" />
</div>
<InfiniteArticleList
groups={groups.data?.pages.flatMap((page) => page?.groups)}
isError={groups.isError}
isLoading={groups.isLoading}
hasMore={groups.hasNextPage}
fetchNewGroups={groups.fetchNextPage}
/>
<FloatingButton path={`/group/new`} />
</div>
);
};
export default GroupsPage;
组件代码
import React from "react";
import GroupCard, { type GroupProps } from "./GroupCard";
import InfiniteScroll from "react-infinite-scroll-component";
import InfiniteLoading from "./InfiniteLoading";
import Loading from "./Loading";
type InfiniteTweetListProps = {
isLoading: boolean;
isError: boolean;
hasMore: boolean | undefined;
fetchNewGroups: () => Promise<unknown>;
groups?: GroupProps[];
};
const InfiniteArticleList = ({
groups,
isError,
isLoading,
fetchNewGroups,
hasMore = false,
}: InfiniteTweetListProps) => {
if (isLoading) return <Loading />;
if (isError) return <h1>Error...</h1>;
if (groups == null || groups.length === 0) {
return (
<h2 className="my-4 text-center text-2xl text-gray-500">No Groups</h2>
);
}
return (
<div className=" bg-nav ">
<ul>
<InfiniteScroll
className="min-h-groupList bg-nav "
dataLength={groups.length}
next={fetchNewGroups}
hasMore={hasMore}
loader={<InfiniteLoading />}
endMessage={
<div className="pb-4 text-center">
<span className=" text-xl font-bold">
Yay! You have seen it all
</span>
</div>
}
>
{groups?.map((group) => {
return <GroupCard key={group.id} {...group} />;
})}
</InfiniteScroll>
</ul>
</div>
);
};
export default InfiniteArticleList;
获取Trpc的类型
export type GroupProps =
RouterOutputs["group"]["getInfiniteGroup"]["groups"][0];
env
如果使用google登录需要在这里加上对应配置
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
},
})
dynamic
在点击之后在加载,优化很重的第三方库
import dynamic from "next/dynamic";
import Loading from "~/shared/Loading";
const ArticleForm = dynamic(
() => import("~/page_components/article/ArticleForm"),
{
ssr: false,
loading: () => <Loading />,
},
);
const NewArticlePage = () => {
return <ArticleForm />;
};
export default NewArticlePage;
软删除
model Group {
id String @id @default(cuid())
createdAt DateTime @default(now())
deletedAt DateTime? // 添加这个字段用于软删除
updatedAt DateTime @updatedAt
createdById String
description String
image String?
rawPrice Int
name String
createdBy User @relation(fields: [createdById], references: [id])
members User[] @relation("UserGroups")
articles Article[]
@@index([createdById])
@@unique([createdAt,id])
}
服务端代码
// 软删除
deleteGroup: protectedProcedure
.input(z.object({ groupId: z.string() }))
.mutation(async ({ ctx, input }) => {
const userId = ctx.session.user.id;
const groupId = input.groupId;
const group = await ctx.db.group.findFirst({
where: {
id: groupId,
createdBy: {
id: userId,
},
deletedAt: null, // 确保要删除的群组尚未被软删除
},
});
if (!group) {
throw new Error("Group not found or already deleted");
}
// 软删除操作,将 deletedAt 设置为当前时间
await ctx.db.group.update({
where: {
id: groupId,
},
data: {
deletedAt: new Date(), // 设置为当前时间标记为已删除
},
});
return group;
}),
使用:
const deleteGroup = api.group.deleteGroup.useMutation({
onSuccess: () => {
void router.push(`/group/mine`);
},
onError,
});
const handleDeleteGroup = () => {
deleteGroup.mutate({ groupId: id as string });
};
部署vercel
- 增加build命令
- 增加环境变量
- 配置
NEXTAUTH_SECRET - 在google控制台增加vercel域名地址
nginx 配置开通https
## Version 2024/03/06 - Changelog: https://github.com/linuxserver/docker-swag/commits/master/root/defaults/nginx/site-confs/default.conf.sample
# redirect all traffic to https
server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
return 301 https://$host$request_uri;
}
}
# main server block
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name getseldwdwdwai.top;
# SSL证书路径(由SWAG管理)
include /config/nginx/ssl.conf;
# 访问日志和错误日志的路径
access_log /config/log/nginx/access.log;
error_log /config/log/nginx/error.log;
location / {
proxy_pass http://selected_ai_app:3000; # 指向你的Next.js应用服务
include /config/nginx/proxy.conf;
resolver 127.0.0.11 valid=30s;
set $upstream_app selected_ai_app;
set $upstream_port 3000;
set $upstream_proto https;
proxy_set_header X-Forwarded-Proto https;
}
}
# enable subdomain method reverse proxy confs
include /config/nginx/proxy-confs/*.subdomain.conf;
# enable proxy cache for auth
proxy_cache_path cache/ keys_zone=auth_cache:10m;
docker 部署相关命令收集
- docker exec -it selected_ai_app npx prisma migrate deploy
- docker ps
- docker ps -a
- docker system prune
- df -h
- docker start
containerId - docker logs selected_ai_app
- watch docker logs demo-1
- docker exec -it selected_ai_nginx bash
- history | grep "docker run "
- docker exec selected_ai_nginx nginx -s reload
- docker network ls
- docker exec -it psql_1 psql -U selected_ai -d selected_ai_dev
- docker exec -it next-app ash