Skip to content

Commit

Permalink
feat: 新增用户管理路由;验证码增加场景字段
Browse files Browse the repository at this point in the history
  • Loading branch information
CaoMeiYouRen committed Oct 1, 2024
1 parent b2ac434 commit d214ef2
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 17 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ NODEJS_HELPERS=0
# 是否写入日志到文件
LOGFILES=false

# 基础路径,在生产环境中请设置为真实的域名
# BASE_URL='http://localhost:3000'
BASE_URL=''

# 微信公众号相关配置
WX_TOKEN=''
WX_APP_ID=''
Expand Down
3 changes: 3 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { winstonLogger } from './utils/logger'
import eventRoute from './routes/event'
import messageRoute from './routes/message'
import codeRoute from './routes/code'
import userRoute from './routes/user'
import { getDataSource } from './db'
import { auth } from './middlewares/auth'

Expand Down Expand Up @@ -63,6 +64,8 @@ app.route('/message', messageRoute)

app.route('/code', codeRoute)

app.route('/user', userRoute)

app.post('/synchronize', auth, async (c) => {
const dataSource = await getDataSource()
await dataSource.synchronize()
Expand Down
9 changes: 9 additions & 0 deletions src/db/models/verify-code.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Column, Entity } from 'typeorm'
import { Base } from './base'

/**
* 场景类型
*/
export type SceneType = 'login' | 'register' | 'resetPassword' | 'bindWechat' | 'unbindWechat'

/**
* 验证码实体类
*
Expand All @@ -16,6 +21,10 @@ export class VerifyCode extends Base {
@Column({ type: 'varchar', length: 50, nullable: false })
code: string

// 场景,比如登录、注册、找回密码等
@Column({ type: 'varchar', length: 50, nullable: true })
scene: SceneType

// 微信 openid,也就是 fromUserName
@Column({ type: 'varchar', length: 255, nullable: true })
wechatOpenid: string
Expand Down
3 changes: 3 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ export const DATABASE_URL = process.env.DATABASE_URL || process.env.POSTGRES_URL

// admin 密钥,通过 Bearer Auth 认证
export const ADMIN_KEY = process.env.ADMIN_KEY || ''

// 基础路径
export const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`
59 changes: 56 additions & 3 deletions src/routes/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
import dayjs from 'dayjs'
import { MoreThanOrEqual } from 'typeorm'
import { merge, uniq } from 'lodash-es'
import { getDataSource } from '@/db'
import { VerifyCode } from '@/db/models/verify-code'
import { User } from '@/db/models/user'
import { auth } from '@/middlewares/auth'
import { CrudQuery } from '@/interfaces/crud-query'
import { transformQueryOperator } from '@/utils/helper'
// 验证码路由
const app = new Hono()

// 第三方登录时,请求该接口校验验证码是否有效
app.post('/verify', async (c) => {
const { code } = await c.req.json()
const { code, scene } = await c.req.json()
const verifyCodeRepository = (await getDataSource()).getRepository(VerifyCode)
const verifyCode = await verifyCodeRepository.findOneBy({ code, expiredAt: MoreThanOrEqual(dayjs().add(-5, 'minutes').toDate()) })
if (code !== verifyCode.code) {
const verifyCode = await verifyCodeRepository.findOneBy({ code, scene, expiredAt: MoreThanOrEqual(dayjs().add(-5, 'minutes').toDate()) })
if (code !== verifyCode?.code) {
throw new HTTPException(400, { message: '验证码错误' })
}
// 查找用户信息
Expand All @@ -26,4 +30,53 @@ app.post('/verify', async (c) => {
})
})

// 获取所有验证码
app.get('/', auth, async (c) => {
const query = c.req.query() as CrudQuery
const limit = Number(query.limit) || 10
const page = Number(query.page) || 1
const skip = Number(query.skip) || (page - 1) * limit
const { where = {}, sort = {}, select = [], relations = [] } = query
const repository = (await getDataSource()).getRepository(VerifyCode)
const [data, total] = await repository.findAndCount({
where: {
...transformQueryOperator(where),
},
skip,
take: limit,
order: merge({
createdAt: 'DESC',
}, sort),
relations: uniq([...relations]),
select: uniq([...select || []]) as any,
})

return c.json({
data,
total,
lastPage: Math.ceil(total / limit),
currentPage: page,
})
})

// 获取单个验证码
app.get('/:id', auth, async (c) => {
const id = parseInt(c.req.param('id'))
const repository = (await getDataSource()).getRepository(VerifyCode)
const message = await repository.findOne({ where: { id } })
return c.json(message)
})

// 删除单个验证码
app.delete('/:id', auth, async (c) => {
const id = parseInt(c.req.param('id'))
const repository = (await getDataSource()).getRepository(VerifyCode)
const message = await repository.findOne({ where: { id } })
if (!message) {
throw new HTTPException(404, { message: '验证码不存在' })
}
await repository.delete({ id })
return c.json({ message: '删除成功' })
})

export default app
15 changes: 8 additions & 7 deletions src/routes/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ app.get('/', async (c) => {
const page = Number(query.page) || 1
const skip = Number(query.skip) || (page - 1) * limit
const { where = {}, sort = {}, select = [], relations = [] } = query
const entityManager = (await getDataSource()).manager
const [data, total] = await entityManager.findAndCount(BaseMessage, {
const repository = (await getDataSource()).getRepository(BaseMessage)
const [data, total] = await repository.findAndCount({
where: {
...transformQueryOperator(where),
},
Expand All @@ -40,23 +40,24 @@ app.get('/', async (c) => {
currentPage: page,
})
})

// 获取单个消息
app.get('/:id', async (c) => {
const id = parseInt(c.req.param('id'))
const entityManager = (await getDataSource()).manager
const message = await entityManager.findOne(BaseMessage, { where: { id } })
const repository = (await getDataSource()).getRepository(BaseMessage)
const message = await repository.findOne({ where: { id } })
return c.json(message)
})

// 删除单个消息
app.delete('/:id', async (c) => {
const id = parseInt(c.req.param('id'))
const entityManager = (await getDataSource()).manager
const message = await entityManager.findOne(BaseMessage, { where: { id } })
const repository = (await getDataSource()).getRepository(BaseMessage)
const message = await repository.findOne({ where: { id } })
if (!message) {
throw new HTTPException(404, { message: '消息不存在' })
}
await entityManager.delete(BaseMessage, { id })
await repository.delete({ id })
return c.json({ message: '删除成功' })
})

Expand Down
77 changes: 77 additions & 0 deletions src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Hono } from 'hono'
import { merge, uniq } from 'lodash-es'
import { HTTPException } from 'hono/http-exception'
import { auth } from '@/middlewares/auth'
import { getDataSource } from '@/db'
import { User } from '@/db/models/user'
import { CrudQuery } from '@/interfaces/crud-query'
import { transformQueryOperator } from '@/utils/helper'

const app = new Hono()

// user 接口需要 admin 权限
app.use('/*', auth)

// 获取所有用户
app.get('/', async (c) => {
const query = c.req.query() as CrudQuery
const limit = Number(query.limit) || 10
const page = Number(query.page) || 1
const skip = Number(query.skip) || (page - 1) * limit
const { where = {}, sort = {}, select = [], relations = [] } = query
const repository = (await getDataSource()).getRepository(User)
const [data, total] = await repository.findAndCount({
where: {
...transformQueryOperator(where),
},
skip,
take: limit,
order: merge({
createdAt: 'DESC',
}, sort),
relations: uniq([...relations]),
select: uniq([...select || []]) as any,
})

return c.json({
data,
total,
lastPage: Math.ceil(total / limit),
currentPage: page,
})
})

// 获取单个用户
app.get('/:id', async (c) => {
const id = parseInt(c.req.param('id'))
const repository = (await getDataSource()).getRepository(User)
const user = await repository.findOne({ where: { id } })
return c.json(user)
})

// 更新单个用户
app.put('/:id', async (c) => {
const id = parseInt(c.req.param('id'))
const body = await c.req.json()
const repository = (await getDataSource()).getRepository(User)
const user = await repository.findOne({ where: { id } })
if (!user) {
throw new HTTPException(404, { message: '用户不存在' })
}
await repository.update({ id }, body)
return c.json({ message: '更新成功' })
})

// 删除单个用户
app.delete('/:id', async (c) => {
const id = parseInt(c.req.param('id'))
const repository = (await getDataSource()).getRepository(User)
const message = await repository.findOne({ where: { id } })
if (!message) {
throw new HTTPException(404, { message: '用户不存在' })
}
await repository.delete({ id })
return c.json({ message: '删除成功' })
})

export default app
5 changes: 3 additions & 2 deletions src/services/code.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dayjs from 'dayjs'
import { MoreThanOrEqual } from 'typeorm'
import { getDataSource } from '@/db'
import { VerifyCode } from '@/db/models/verify-code'
import { SceneType, VerifyCode } from '@/db/models/verify-code'

/**
* 生成验证码
Expand All @@ -27,9 +27,10 @@ export function generateCode(length: number = 6): string {
* @export
* @param wechatOpenid
*/
export async function createVerifyCode(wechatOpenid: string) {
export async function createVerifyCode(wechatOpenid: string, scene: SceneType) {
const repository = (await getDataSource()).getRepository(VerifyCode)
const verifyCode = new VerifyCode()
verifyCode.scene = scene
verifyCode.wechatOpenid = wechatOpenid
for (let i = 0; i < 3; i++) {
verifyCode.code = generateCode()
Expand Down
26 changes: 21 additions & 5 deletions src/services/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IWechatEventBody } from '@/interfaces/wechat-event-body'
import { IWechatReplyMessage } from '@/interfaces/wechat-reply-message'
import { json2xml, toPascalCase } from '@/utils/helper'
import { User } from '@/db/models/user'
import { BASE_URL } from '@/env'

/**
* 回复消息
Expand Down Expand Up @@ -52,7 +53,7 @@ export async function handleEvent(body: IWechatEventBody) {
// 如果发送的是 '验证码',则创建新的验证码
// TODO 验证码关键词应该可以自定义
if (content === '验证码') {
const verifyCode = await createVerifyCode(fromUserName)
const verifyCode = await createVerifyCode(fromUserName, 'login')
const respContent = `您的验证码是:${verifyCode.code}`
return replyMessage({
toUserName: fromUserName,
Expand All @@ -61,14 +62,29 @@ export async function handleEvent(body: IWechatEventBody) {
content: respContent,
})
}
if (content === '登录') {
// 直接返回一个登录链接
const verifyCode = await createVerifyCode(fromUserName, 'login')
const url = new URL(BASE_URL)
url.pathname = '/login'
url.searchParams.set('code', verifyCode.code)
const loginLink = url.toString()
const respContent = `请点击链接登录:${loginLink}`
return replyMessage({
toUserName: fromUserName,
fromUserName: toUserName,
msgType: 'text',
content: respContent,
})
}
// 否则复读
// TODO 如果未匹配到关键词,则转发请求到下一个服务器
const RespContent = `您发送的消息是:${content}`
const respContent = `您发送的消息是:${content}`
return replyMessage({
toUserName: fromUserName,
fromUserName: toUserName,
msgType: 'text',
content: RespContent,
content: respContent,
})
}
case 'image': { // 图片消息
Expand All @@ -87,12 +103,12 @@ export async function handleEvent(body: IWechatEventBody) {
switch (event) {
case 'subscribe': { // 订阅
// TODO 处理用户订阅事件
const RespContent = '感谢订阅'
const respContent = '感谢订阅'
return replyMessage({
toUserName: fromUserName,
fromUserName: toUserName,
msgType: 'text',
content: RespContent,
content: respContent,
})
}
case 'unsubscribe': // 取消订阅
Expand Down

0 comments on commit d214ef2

Please sign in to comment.