feat: 完善微信认证功能,新增用户资料更新与token刷新接口
- 新增 userService.js,包含用户认证、资料更新、token 刷新等功能 - 新增 wechatService.js,处理微信API交互,获取openid和手机号 - 新增 appError.js,封装应用错误处理 - 新增 logger.js,提供日志记录功能 - 新增 response.js,统一成功响应格式 - 新增 sanitize.js,提供输入数据清洗功能 - 更新 OpenAPI 文档,描述新增接口及请求响应格式 - 更新 PocketBase 数据库结构,调整用户表字段及索引策略 - 增强错误处理机制,确保错误信息可观测性 - 更新变更记录文档,详细记录本次变更内容
This commit is contained in:
@@ -1,14 +1,7 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const request = require('supertest')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const env = require('../../src/config/env')
|
||||
const { createApp } = require('../../src/index')
|
||||
const userService = require('../../src/services/userService')
|
||||
|
||||
const buildInfoPath = path.resolve(__dirname, '..', '..', 'build-info.json')
|
||||
|
||||
test('POST /api/health 仍返回统一结构', async () => {
|
||||
const app = createApp()
|
||||
@@ -20,47 +13,6 @@ test('POST /api/health 仍返回统一结构', async () => {
|
||||
assert.equal(response.body.data.status, 'healthy')
|
||||
})
|
||||
|
||||
test('POST /api/test-helloworld 返回统一结构和构建时间', async (t) => {
|
||||
const app = createApp()
|
||||
const buildTime = '2026-03-20T08:00:00.000Z'
|
||||
|
||||
fs.writeFileSync(buildInfoPath, JSON.stringify({ buildTime }, null, 2))
|
||||
|
||||
t.after(() => {
|
||||
if (fs.existsSync(buildInfoPath)) {
|
||||
fs.rmSync(buildInfoPath)
|
||||
}
|
||||
})
|
||||
|
||||
const response = await request(app).post('/api/test-helloworld').send({})
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.equal(response.body.code, 200)
|
||||
assert.equal(response.body.msg, '请求成功')
|
||||
assert.equal(response.body.data.message, 'Hello, World!')
|
||||
assert.equal(response.body.data.build_time, buildTime)
|
||||
})
|
||||
|
||||
test('GET /api/test-helloworld 返回 404', async () => {
|
||||
const app = createApp()
|
||||
const response = await request(app).get('/api/test-helloworld')
|
||||
|
||||
assert.equal(response.status, 404)
|
||||
assert.equal(response.body.code, 404)
|
||||
assert.equal(response.body.msg, 'Route not found')
|
||||
})
|
||||
|
||||
test('POST /api/test-helloworld 返回统一结构', async () => {
|
||||
const app = createApp()
|
||||
const response = await request(app).post('/api/test-helloworld').send({})
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.equal(response.body.code, 200)
|
||||
assert.equal(response.body.msg, '请求成功')
|
||||
assert.equal(response.body.data.message, 'Hello, World!')
|
||||
assert.ok(Object.prototype.hasOwnProperty.call(response.body.data, 'build_time'))
|
||||
})
|
||||
|
||||
test('未匹配路由返回统一 404', async () => {
|
||||
const app = createApp()
|
||||
const response = await request(app).get('/not-found-route')
|
||||
@@ -70,154 +22,3 @@ test('未匹配路由返回统一 404', async () => {
|
||||
assert.equal(response.body.msg, 'Route not found')
|
||||
assert.equal(response.body.data.path, '/not-found-route')
|
||||
})
|
||||
|
||||
test('POST /api/wechat/login 未注册用户返回 404', async (t) => {
|
||||
const origin = userService.authenticateWechatUser
|
||||
userService.authenticateWechatUser = async () => {
|
||||
const error = new Error('未注册用户')
|
||||
error.statusCode = 404
|
||||
throw error
|
||||
}
|
||||
|
||||
t.after(() => {
|
||||
userService.authenticateWechatUser = origin
|
||||
})
|
||||
|
||||
const app = createApp()
|
||||
const response = await request(app)
|
||||
.post('/api/wechat/login')
|
||||
.send({ users_wx_code: 'mock-code-success' })
|
||||
|
||||
assert.equal(response.status, 404)
|
||||
assert.equal(response.body.code, 404)
|
||||
assert.equal(response.body.msg, '未注册用户')
|
||||
})
|
||||
|
||||
test('POST /api/wechat/login 非 JSON 请求体返回 415', async () => {
|
||||
const app = createApp()
|
||||
const response = await request(app)
|
||||
.post('/api/wechat/login')
|
||||
.set('Content-Type', 'application/x-www-form-urlencoded')
|
||||
.send('users_wx_code=mock-code')
|
||||
|
||||
assert.equal(response.status, 415)
|
||||
assert.equal(response.body.code, 415)
|
||||
assert.equal(response.body.msg, '请求体必须为 application/json')
|
||||
})
|
||||
|
||||
test('POST /api/wechat/login 成功时返回 is_info_complete', async (t) => {
|
||||
const origin = userService.authenticateWechatUser
|
||||
userService.authenticateWechatUser = async () => ({
|
||||
is_info_complete: true,
|
||||
status: 'login_success',
|
||||
token: 'mock-token',
|
||||
user: {
|
||||
users_id: 'U1003',
|
||||
users_type: '注册用户',
|
||||
users_name: '李四',
|
||||
},
|
||||
})
|
||||
|
||||
t.after(() => {
|
||||
userService.authenticateWechatUser = origin
|
||||
})
|
||||
|
||||
const app = createApp()
|
||||
const response = await request(app)
|
||||
.post('/api/wechat/login')
|
||||
.send({ users_wx_code: 'mock-code' })
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.equal(response.body.code, 200)
|
||||
assert.equal(response.body.msg, '登录成功')
|
||||
assert.equal(response.body.data.is_info_complete, true)
|
||||
assert.equal(response.body.data.user.users_type, '注册用户')
|
||||
})
|
||||
|
||||
test('POST /api/wechat/profile 更新成功后返回脱敏手机号', async (t) => {
|
||||
const origin = userService.updateWechatUserProfile
|
||||
userService.updateWechatUserProfile = async () => ({
|
||||
status: 'update_success',
|
||||
user: {
|
||||
users_id: 'U1002',
|
||||
users_type: '注册用户',
|
||||
users_name: '张三',
|
||||
users_phone: '13800138000',
|
||||
users_phone_masked: '138****8000',
|
||||
users_picture: 'https://example.com/a.png',
|
||||
},
|
||||
})
|
||||
|
||||
t.after(() => {
|
||||
userService.updateWechatUserProfile = origin
|
||||
})
|
||||
|
||||
const token = require('jsonwebtoken').sign({ users_id: 'U1002', users_wx_openid: 'openid-1' }, env.jwtSecret, {
|
||||
expiresIn: env.jwtExpiresIn,
|
||||
})
|
||||
|
||||
const app = createApp()
|
||||
const response = await request(app)
|
||||
.post('/api/wechat/profile')
|
||||
.set('users_wx_openid', 'openid-1')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({
|
||||
users_name: '张三',
|
||||
users_phone_code: 'phone-code',
|
||||
users_picture: 'https://example.com/a.png',
|
||||
})
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.equal(response.body.code, 200)
|
||||
assert.equal(response.body.msg, '信息更新成功')
|
||||
assert.equal(response.body.data.user.users_phone_masked, '138****8000')
|
||||
assert.equal(response.body.data.user.users_type, '注册用户')
|
||||
})
|
||||
|
||||
test('POST /api/wechat/profile 缺少 token 返回 401', async () => {
|
||||
const app = createApp()
|
||||
const response = await request(app)
|
||||
.post('/api/wechat/profile')
|
||||
.set('users_wx_openid', 'openid-1')
|
||||
.send({
|
||||
users_name: '张三',
|
||||
users_phone_code: 'phone-code',
|
||||
users_picture: 'https://example.com/a.png',
|
||||
})
|
||||
|
||||
assert.equal(response.status, 401)
|
||||
assert.equal(response.body.code, 401)
|
||||
})
|
||||
|
||||
test('POST /api/wechat/refresh-token 返回 token', async (t) => {
|
||||
const origin = userService.refreshWechatToken
|
||||
userService.refreshWechatToken = async () => ({
|
||||
token: 'new-token',
|
||||
})
|
||||
|
||||
t.after(() => {
|
||||
userService.refreshWechatToken = origin
|
||||
})
|
||||
|
||||
const app = createApp()
|
||||
const response = await request(app)
|
||||
.post('/api/wechat/refresh-token')
|
||||
.set('users_wx_openid', 'openid-1')
|
||||
.send({})
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.equal(response.body.code, 200)
|
||||
assert.equal(response.body.msg, '刷新成功')
|
||||
assert.equal(response.body.data.token, 'new-token')
|
||||
})
|
||||
|
||||
test('POST /api/wechat/refresh-token 缺少 users_wx_openid 返回 401', async () => {
|
||||
const app = createApp()
|
||||
const response = await request(app)
|
||||
.post('/api/wechat/refresh-token')
|
||||
.send({})
|
||||
|
||||
assert.equal(response.status, 401)
|
||||
assert.equal(response.body.code, 401)
|
||||
assert.equal(response.body.msg, '请求头缺少 users_wx_openid')
|
||||
})
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const wechatService = require('../src/services/wechatService')
|
||||
const pocketbaseService = require('../src/services/pocketbaseService')
|
||||
const jwtService = require('../src/services/jwtService')
|
||||
const userService = require('../src/services/userService')
|
||||
|
||||
test('authenticateWechatUser 首次注册默认写入游客类型', async (t) => {
|
||||
const originGetWxOpenId = wechatService.getWxOpenId
|
||||
const originListUsersByFilter = pocketbaseService.listUsersByFilter
|
||||
const originCreateUser = pocketbaseService.createUser
|
||||
const originSignAccessToken = jwtService.signAccessToken
|
||||
const originGetCompanyByCompanyId = pocketbaseService.getCompanyByCompanyId
|
||||
|
||||
wechatService.getWxOpenId = async () => 'openid-register'
|
||||
pocketbaseService.listUsersByFilter = async () => []
|
||||
pocketbaseService.createUser = async (payload) => ({
|
||||
id: 'pb-1',
|
||||
created: '2026-03-19T00:00:00Z',
|
||||
updated: '2026-03-19T00:00:00Z',
|
||||
...payload,
|
||||
})
|
||||
pocketbaseService.getCompanyByCompanyId = async () => null
|
||||
jwtService.signAccessToken = () => 'token-1'
|
||||
|
||||
t.after(() => {
|
||||
wechatService.getWxOpenId = originGetWxOpenId
|
||||
pocketbaseService.listUsersByFilter = originListUsersByFilter
|
||||
pocketbaseService.createUser = originCreateUser
|
||||
pocketbaseService.getCompanyByCompanyId = originGetCompanyByCompanyId
|
||||
jwtService.signAccessToken = originSignAccessToken
|
||||
})
|
||||
|
||||
const result = await userService.authenticateWechatUser({
|
||||
users_wx_code: 'code-1',
|
||||
})
|
||||
|
||||
assert.equal(result.status, 'register_success')
|
||||
assert.equal(result.is_info_complete, false)
|
||||
assert.equal(result.user.users_type, '游客')
|
||||
})
|
||||
|
||||
test('updateWechatUserProfile 首次完整补充资料时升级为注册用户', async (t) => {
|
||||
const originGetWxOpenId = wechatService.getWxOpenId
|
||||
const originGetWxPhoneNumber = wechatService.getWxPhoneNumber
|
||||
const originListUsersByFilter = pocketbaseService.listUsersByFilter
|
||||
const originUpdateUser = pocketbaseService.updateUser
|
||||
const originGetCompanyByCompanyId = pocketbaseService.getCompanyByCompanyId
|
||||
|
||||
wechatService.getWxOpenId = async () => 'openid-profile'
|
||||
wechatService.getWxPhoneNumber = async () => '13800138000'
|
||||
pocketbaseService.listUsersByFilter = async (filter) => {
|
||||
if (filter.includes('users_wx_openid')) {
|
||||
return [
|
||||
{
|
||||
id: 'pb-2',
|
||||
users_id: 'U2001',
|
||||
users_type: '游客',
|
||||
users_name: '',
|
||||
users_phone: '',
|
||||
users_picture: '',
|
||||
users_wx_openid: 'openid-profile',
|
||||
created: '2026-03-19T00:00:00Z',
|
||||
updated: '2026-03-19T00:00:00Z',
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
pocketbaseService.updateUser = async (_id, payload) => ({
|
||||
id: 'pb-2',
|
||||
users_id: 'U2001',
|
||||
users_wx_openid: 'openid-profile',
|
||||
users_type: payload.users_type || '游客',
|
||||
users_name: payload.users_name,
|
||||
users_phone: payload.users_phone,
|
||||
users_picture: payload.users_picture,
|
||||
created: '2026-03-19T00:00:00Z',
|
||||
updated: '2026-03-19T00:10:00Z',
|
||||
})
|
||||
pocketbaseService.getCompanyByCompanyId = async () => null
|
||||
|
||||
t.after(() => {
|
||||
wechatService.getWxOpenId = originGetWxOpenId
|
||||
wechatService.getWxPhoneNumber = originGetWxPhoneNumber
|
||||
pocketbaseService.listUsersByFilter = originListUsersByFilter
|
||||
pocketbaseService.updateUser = originUpdateUser
|
||||
pocketbaseService.getCompanyByCompanyId = originGetCompanyByCompanyId
|
||||
})
|
||||
|
||||
const result = await userService.updateWechatUserProfile('openid-profile', {
|
||||
users_name: '张三',
|
||||
users_phone_code: 'phone-code-1',
|
||||
users_picture: 'https://example.com/a.png',
|
||||
})
|
||||
|
||||
assert.equal(result.status, 'update_success')
|
||||
assert.equal(result.user.users_type, '注册用户')
|
||||
assert.equal(result.user.users_phone_masked, '138****8000')
|
||||
})
|
||||
|
||||
test('updateWechatUserProfile 非首次补充资料时不覆盖已确定类型', async (t) => {
|
||||
const originGetWxOpenId = wechatService.getWxOpenId
|
||||
const originGetWxPhoneNumber = wechatService.getWxPhoneNumber
|
||||
const originListUsersByFilter = pocketbaseService.listUsersByFilter
|
||||
const originUpdateUser = pocketbaseService.updateUser
|
||||
const originGetCompanyByCompanyId = pocketbaseService.getCompanyByCompanyId
|
||||
|
||||
wechatService.getWxOpenId = async () => 'openid-registered'
|
||||
wechatService.getWxPhoneNumber = async () => '13900139000'
|
||||
pocketbaseService.listUsersByFilter = async (filter) => {
|
||||
if (filter.includes('users_wx_openid')) {
|
||||
return [
|
||||
{
|
||||
id: 'pb-3',
|
||||
users_id: 'U2002',
|
||||
users_type: '注册用户',
|
||||
users_name: '老用户',
|
||||
users_phone: '13800138000',
|
||||
users_picture: 'https://example.com/old.png',
|
||||
users_wx_openid: 'openid-registered',
|
||||
created: '2026-03-19T00:00:00Z',
|
||||
updated: '2026-03-19T00:00:00Z',
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
pocketbaseService.updateUser = async (_id, payload) => ({
|
||||
id: 'pb-3',
|
||||
users_id: 'U2002',
|
||||
users_wx_openid: 'openid-registered',
|
||||
users_type: '注册用户',
|
||||
users_name: payload.users_name,
|
||||
users_phone: payload.users_phone,
|
||||
users_picture: payload.users_picture,
|
||||
created: '2026-03-19T00:00:00Z',
|
||||
updated: '2026-03-19T00:10:00Z',
|
||||
})
|
||||
pocketbaseService.getCompanyByCompanyId = async () => null
|
||||
|
||||
t.after(() => {
|
||||
wechatService.getWxOpenId = originGetWxOpenId
|
||||
wechatService.getWxPhoneNumber = originGetWxPhoneNumber
|
||||
pocketbaseService.listUsersByFilter = originListUsersByFilter
|
||||
pocketbaseService.updateUser = originUpdateUser
|
||||
pocketbaseService.getCompanyByCompanyId = originGetCompanyByCompanyId
|
||||
})
|
||||
|
||||
const result = await userService.updateWechatUserProfile('openid-registered', {
|
||||
users_name: '新名字',
|
||||
users_phone_code: 'phone-code-2',
|
||||
users_picture: 'https://example.com/new.png',
|
||||
})
|
||||
|
||||
assert.equal(result.user.users_type, '注册用户')
|
||||
})
|
||||
|
||||
test('refreshWechatToken 返回新的 token', async (t) => {
|
||||
const originListUsersByFilter = pocketbaseService.listUsersByFilter
|
||||
const originSignAccessToken = jwtService.signAccessToken
|
||||
|
||||
pocketbaseService.listUsersByFilter = async () => [
|
||||
{
|
||||
id: 'pb-4',
|
||||
users_id: 'U3001',
|
||||
users_wx_openid: 'openid-refresh',
|
||||
},
|
||||
]
|
||||
jwtService.signAccessToken = () => 'refresh-token'
|
||||
|
||||
t.after(() => {
|
||||
pocketbaseService.listUsersByFilter = originListUsersByFilter
|
||||
jwtService.signAccessToken = originSignAccessToken
|
||||
})
|
||||
|
||||
const result = await userService.refreshWechatToken('openid-refresh')
|
||||
|
||||
assert.equal(result.token, 'refresh-token')
|
||||
})
|
||||
Reference in New Issue
Block a user