新增微信登录/注册合一接口、资料完善接口和token刷新接口 重构用户服务层,支持自动维护用户类型和资料完整度 引入JWT认证中间件和请求验证中间件 更新文档与测试用例,支持dist构建部署
209 lines
6.3 KiB
JavaScript
209 lines
6.3 KiB
JavaScript
const crypto = require('crypto')
|
|
const AppError = require('../utils/appError')
|
|
const logger = require('../utils/logger')
|
|
const wechatService = require('./wechatService')
|
|
const jwtService = require('./jwtService')
|
|
const pocketbaseService = require('./pocketbaseService')
|
|
|
|
const userMutationLocks = new Map()
|
|
const GUEST_USER_TYPE = '游客'
|
|
const REGISTERED_USER_TYPE = '注册用户'
|
|
|
|
function buildUserId() {
|
|
const now = new Date()
|
|
const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`
|
|
const suffix = crypto.randomInt(1000, 9999)
|
|
return `U${date}${suffix}`
|
|
}
|
|
|
|
async function enrichUser(user) {
|
|
const company = await pocketbaseService.getCompanyByCompanyId(user.company_id)
|
|
return {
|
|
pb_id: user.id,
|
|
users_id: user.users_id,
|
|
users_type: user.users_type || GUEST_USER_TYPE,
|
|
users_name: user.users_name,
|
|
users_phone: user.users_phone,
|
|
users_phone_masked: maskPhone(user.users_phone),
|
|
users_picture: user.users_picture,
|
|
users_wx_openid: user.users_wx_openid,
|
|
company_id: user.company_id || '',
|
|
company,
|
|
created: user.created,
|
|
updated: user.updated,
|
|
raw: user,
|
|
}
|
|
}
|
|
|
|
async function findUserByOpenid(usersWxOpenid) {
|
|
const users = await pocketbaseService.listUsersByFilter(`users_wx_openid = "${usersWxOpenid}"`)
|
|
return users[0] || null
|
|
}
|
|
|
|
function maskPhone(phone = '') {
|
|
if (!phone || phone.length < 7) return ''
|
|
return `${phone.slice(0, 3)}****${phone.slice(-4)}`
|
|
}
|
|
|
|
function isInfoComplete(user = {}) {
|
|
return Boolean(user.users_name && user.users_phone && user.users_picture)
|
|
}
|
|
|
|
function isAllProfileFieldsEmpty(user = {}) {
|
|
return !user.users_name && !user.users_phone && !user.users_picture
|
|
}
|
|
|
|
async function withUserLock(lockKey, handler) {
|
|
const previous = userMutationLocks.get(lockKey) || Promise.resolve()
|
|
let release
|
|
const current = new Promise((resolve) => {
|
|
release = resolve
|
|
})
|
|
|
|
userMutationLocks.set(lockKey, previous.then(() => current))
|
|
|
|
await previous
|
|
|
|
try {
|
|
return await handler()
|
|
} finally {
|
|
release()
|
|
if (userMutationLocks.get(lockKey) === current) {
|
|
userMutationLocks.delete(lockKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function authenticateWechatUser(payload) {
|
|
const openid = await wechatService.getWxOpenId(payload.users_wx_code)
|
|
return withUserLock(`auth:${openid}`, async () => {
|
|
const existing = await findUserByOpenid(openid)
|
|
|
|
if (existing) {
|
|
logger.warn('微信注册命中已存在账号', {
|
|
users_wx_openid: openid,
|
|
users_type: existing.users_type || GUEST_USER_TYPE,
|
|
})
|
|
|
|
const user = await enrichUser(existing)
|
|
const token = jwtService.signAccessToken({
|
|
users_id: user.users_id,
|
|
users_wx_openid: user.users_wx_openid,
|
|
})
|
|
|
|
return {
|
|
status: 'login_success',
|
|
is_info_complete: isInfoComplete(existing),
|
|
token,
|
|
user,
|
|
}
|
|
}
|
|
|
|
const created = await pocketbaseService.createUser({
|
|
users_id: buildUserId(),
|
|
users_wx_openid: openid,
|
|
users_type: GUEST_USER_TYPE,
|
|
})
|
|
|
|
const user = await enrichUser(created)
|
|
const token = jwtService.signAccessToken({
|
|
users_id: user.users_id,
|
|
users_wx_openid: user.users_wx_openid,
|
|
})
|
|
|
|
logger.info('微信用户注册成功', {
|
|
users_id: user.users_id,
|
|
users_phone: user.users_phone,
|
|
users_type: user.users_type,
|
|
})
|
|
|
|
return {
|
|
status: 'register_success',
|
|
is_info_complete: false,
|
|
token,
|
|
user,
|
|
}
|
|
})
|
|
}
|
|
|
|
async function updateWechatUserProfile(usersWxOpenid, payload) {
|
|
return withUserLock(`profile:${usersWxOpenid}`, async () => {
|
|
const currentUser = await findUserByOpenid(usersWxOpenid)
|
|
|
|
if (!currentUser) {
|
|
throw new AppError('未找到待编辑的用户', 404)
|
|
}
|
|
|
|
const usersPhone = await wechatService.getWxPhoneNumber(payload.users_phone_code)
|
|
|
|
if (usersPhone && usersPhone !== currentUser.users_phone) {
|
|
const samePhoneUsers = await pocketbaseService.listUsersByFilter(`users_phone = "${usersPhone}"`)
|
|
const phoneUsedByOther = samePhoneUsers.some((item) => item.id !== currentUser.id)
|
|
if (phoneUsedByOther) {
|
|
throw new AppError('手机号已被注册', 400)
|
|
}
|
|
}
|
|
|
|
const shouldPromoteUserType =
|
|
isAllProfileFieldsEmpty(currentUser)
|
|
&& payload.users_name
|
|
&& usersPhone
|
|
&& payload.users_picture
|
|
&& (currentUser.users_type === GUEST_USER_TYPE || !currentUser.users_type)
|
|
|
|
const updatePayload = {
|
|
users_name: payload.users_name,
|
|
users_phone: usersPhone,
|
|
users_picture: payload.users_picture,
|
|
}
|
|
|
|
if (shouldPromoteUserType) {
|
|
updatePayload.users_type = REGISTERED_USER_TYPE
|
|
}
|
|
|
|
const updated = await pocketbaseService.updateUser(currentUser.id, updatePayload)
|
|
const user = await enrichUser(updated)
|
|
|
|
logger.info('微信用户资料更新成功', {
|
|
users_id: user.users_id,
|
|
users_phone: user.users_phone,
|
|
users_type_before: currentUser.users_type || GUEST_USER_TYPE,
|
|
users_type_after: user.users_type,
|
|
users_type_promoted: shouldPromoteUserType,
|
|
})
|
|
|
|
return {
|
|
status: 'update_success',
|
|
user,
|
|
}
|
|
})
|
|
}
|
|
|
|
async function refreshWechatToken(usersWxOpenid) {
|
|
const userRecord = await findUserByOpenid(usersWxOpenid)
|
|
|
|
if (!userRecord) {
|
|
throw new AppError('未注册用户', 404)
|
|
}
|
|
|
|
const token = jwtService.signAccessToken({
|
|
users_id: userRecord.users_id,
|
|
users_wx_openid: userRecord.users_wx_openid,
|
|
})
|
|
|
|
logger.info('微信用户刷新令牌成功', {
|
|
users_id: userRecord.users_id,
|
|
users_wx_openid: userRecord.users_wx_openid,
|
|
})
|
|
|
|
return {
|
|
token,
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
authenticateWechatUser,
|
|
updateWechatUserProfile,
|
|
refreshWechatToken,
|
|
}
|