feat: 完善微信认证功能,新增用户资料更新与token刷新接口
- 新增 userService.js,包含用户认证、资料更新、token 刷新等功能 - 新增 wechatService.js,处理微信API交互,获取openid和手机号 - 新增 appError.js,封装应用错误处理 - 新增 logger.js,提供日志记录功能 - 新增 response.js,统一成功响应格式 - 新增 sanitize.js,提供输入数据清洗功能 - 更新 OpenAPI 文档,描述新增接口及请求响应格式 - 更新 PocketBase 数据库结构,调整用户表字段及索引策略 - 增强错误处理机制,确保错误信息可观测性 - 更新变更记录文档,详细记录本次变更内容
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
"description": "",
|
||||
"main": "pocketbase.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"init:newpb": "node pocketbase.newpb.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
261
script/pocketbase.newpb.js
Normal file
261
script/pocketbase.newpb.js
Normal file
@@ -0,0 +1,261 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
function readEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return {};
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const result = {};
|
||||
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const index = line.indexOf('=');
|
||||
if (index === -1) continue;
|
||||
const key = line.slice(0, index).trim();
|
||||
const value = line.slice(index + 1).trim();
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env'));
|
||||
const PB_URL = (process.env.PB_URL || backendEnv.POCKETBASE_API_URL || 'http://127.0.0.1:8090').replace(/\/+$/, '');
|
||||
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL || backendEnv.PB_ADMIN_EMAIL || '';
|
||||
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD || backendEnv.PB_ADMIN_PASSWORD || '';
|
||||
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || backendEnv.POCKETBASE_AUTH_TOKEN || '';
|
||||
|
||||
const pb = new PocketBase(PB_URL);
|
||||
|
||||
const collections = [
|
||||
{
|
||||
name: 'tbl_auth_users',
|
||||
type: 'auth',
|
||||
fields: [
|
||||
{ name: 'user_id', type: 'text' },
|
||||
{ name: 'openid', type: 'text', required: true },
|
||||
{ name: 'user_name', type: 'text' },
|
||||
{ name: 'org_id', type: 'number' },
|
||||
{ name: 'rank_level', type: 'number' },
|
||||
{ name: 'status', type: 'number' },
|
||||
{ name: 'user_auth_type', type: 'number' },
|
||||
|
||||
{ name: 'users_id', type: 'text' },
|
||||
{ name: 'users_name', type: 'text' },
|
||||
{ name: 'users_idtype', type: 'text' },
|
||||
{ name: 'users_id_number', type: 'text' },
|
||||
{ name: 'users_phone', type: 'text' },
|
||||
{ name: 'users_level', type: 'text' },
|
||||
{ name: 'users_type', type: 'text' },
|
||||
{ name: 'users_status', type: 'text' },
|
||||
{ name: 'company_id', type: 'text' },
|
||||
{ name: 'users_parent_id', type: 'text' },
|
||||
{ name: 'users_promo_code', type: 'text' },
|
||||
{ name: 'users_id_pic_a', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_id_pic_b', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_title_picture', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_picture', type: 'text' },
|
||||
{ name: 'usergroups_id', type: 'text' }
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_users_user_id ON tbl_auth_users (user_id)',
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_users_openid ON tbl_auth_users (openid)',
|
||||
'CREATE INDEX idx_tbl_auth_users_org_id ON tbl_auth_users (org_id)',
|
||||
'CREATE INDEX idx_tbl_auth_users_rank_level ON tbl_auth_users (rank_level)',
|
||||
'CREATE INDEX idx_tbl_auth_users_status ON tbl_auth_users (status)',
|
||||
'CREATE INDEX idx_tbl_auth_users_user_auth_type ON tbl_auth_users (user_auth_type)',
|
||||
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_users_users_id ON tbl_auth_users (users_id)',
|
||||
// Allow unbound users with empty phone while still speeding up phone lookups.
|
||||
'CREATE INDEX idx_tbl_auth_users_users_phone ON tbl_auth_users (users_phone)',
|
||||
'CREATE INDEX idx_tbl_auth_users_company_id ON tbl_auth_users (company_id)',
|
||||
'CREATE INDEX idx_tbl_auth_users_usergroups_id ON tbl_auth_users (usergroups_id)',
|
||||
'CREATE INDEX idx_tbl_auth_users_users_parent_id ON tbl_auth_users (users_parent_id)'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'tbl_auth_resources',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'res_id', type: 'text', required: true },
|
||||
{ name: 'table_name', type: 'text', required: true },
|
||||
{ name: 'column_name', type: 'text' },
|
||||
{ name: 'res_type', type: 'text', required: true }
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_resources_res_id ON tbl_auth_resources (res_id)',
|
||||
'CREATE INDEX idx_tbl_auth_resources_table_name ON tbl_auth_resources (table_name)',
|
||||
'CREATE INDEX idx_tbl_auth_resources_res_type ON tbl_auth_resources (res_type)',
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_resources_unique_res ON tbl_auth_resources (table_name, column_name, res_type)'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'tbl_auth_roles',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'role_id', type: 'text', required: true },
|
||||
{ name: 'role_name', type: 'text', required: true },
|
||||
{ name: 'role_code', type: 'text' },
|
||||
{ name: 'role_status', type: 'number' },
|
||||
{ name: 'role_remark', type: 'text' }
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_roles_role_id ON tbl_auth_roles (role_id)',
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_roles_role_name ON tbl_auth_roles (role_name)',
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_roles_role_code ON tbl_auth_roles (role_code)'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'tbl_auth_role_perms',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'role_perm_id', type: 'text', required: true },
|
||||
{ name: 'role_id', type: 'text', required: true },
|
||||
{ name: 'res_id', type: 'text', required: true },
|
||||
{ name: 'access_level', type: 'number', required: true },
|
||||
{ name: 'priority', type: 'number' }
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_role_perms_role_perm_id ON tbl_auth_role_perms (role_perm_id)',
|
||||
'CREATE INDEX idx_tbl_auth_role_perms_role_id ON tbl_auth_role_perms (role_id)',
|
||||
'CREATE INDEX idx_tbl_auth_role_perms_res_id ON tbl_auth_role_perms (res_id)',
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_role_perms_unique_map ON tbl_auth_role_perms (role_id, res_id)'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'tbl_auth_user_overrides',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'override_id', type: 'text', required: true },
|
||||
{ name: 'user_id', type: 'text', required: true },
|
||||
{ name: 'res_id', type: 'text', required: true },
|
||||
{ name: 'access_level', type: 'number', required: true },
|
||||
{ name: 'priority', type: 'number' }
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_override_id ON tbl_auth_user_overrides (override_id)',
|
||||
'CREATE INDEX idx_tbl_auth_user_overrides_user_id ON tbl_auth_user_overrides (user_id)',
|
||||
'CREATE INDEX idx_tbl_auth_user_overrides_res_id ON tbl_auth_user_overrides (res_id)',
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_unique_map ON tbl_auth_user_overrides (user_id, res_id)'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'tbl_auth_row_scopes',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'scope_id', type: 'text', required: true },
|
||||
{ name: 'target_type', type: 'text', required: true },
|
||||
{ name: 'target_id', type: 'text', required: true },
|
||||
{ name: 'table_name', type: 'text', required: true },
|
||||
{ name: 'filter_sql', type: 'editor', required: true }
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_auth_row_scopes_scope_id ON tbl_auth_row_scopes (scope_id)',
|
||||
'CREATE INDEX idx_tbl_auth_row_scopes_target_type ON tbl_auth_row_scopes (target_type)',
|
||||
'CREATE INDEX idx_tbl_auth_row_scopes_target_id ON tbl_auth_row_scopes (target_id)',
|
||||
'CREATE INDEX idx_tbl_auth_row_scopes_table_name ON tbl_auth_row_scopes (table_name)'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
console.log('🔄 正在连接 PocketBase API...');
|
||||
|
||||
if (AUTH_TOKEN) {
|
||||
pb.authStore.save(AUTH_TOKEN, null);
|
||||
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
|
||||
} else if (ADMIN_EMAIL && ADMIN_PASSWORD) {
|
||||
await pb.collection('_superusers').authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
||||
console.log('✅ 已使用超级管理员账号登录。');
|
||||
} else {
|
||||
throw new Error('缺少 PocketBase API 认证信息,请提供 POCKETBASE_AUTH_TOKEN 或 PB_ADMIN_EMAIL/PB_ADMIN_PASSWORD');
|
||||
}
|
||||
|
||||
console.log('🚀 开始初始化 newpb 权限模型表结构...\n');
|
||||
|
||||
for (const collectionData of collections) {
|
||||
await createOrUpdateCollection(collectionData);
|
||||
}
|
||||
|
||||
await verifyCollections(collections);
|
||||
console.log('\n🎉 newpb 权限模型表结构初始化并校验完成!');
|
||||
} catch (error) {
|
||||
console.error('❌ 初始化失败:', error.response?.data || error.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrUpdateCollection(collectionData) {
|
||||
console.log(`🔄 正在处理表: ${collectionData.name} ...`);
|
||||
const payload = {
|
||||
name: collectionData.name,
|
||||
type: collectionData.type,
|
||||
fields: collectionData.fields,
|
||||
indexes: collectionData.indexes,
|
||||
};
|
||||
|
||||
try {
|
||||
await pb.collections.create(payload);
|
||||
console.log(`✅ ${collectionData.name} 创建完成。`);
|
||||
} catch (error) {
|
||||
const nameErrorCode = error.response?.data?.name?.code;
|
||||
if (
|
||||
error.status === 400
|
||||
&& (nameErrorCode === 'validation_not_unique' || nameErrorCode === 'validation_collection_name_exists')
|
||||
) {
|
||||
const existing = await pb.collections.getOne(collectionData.name);
|
||||
await pb.collections.update(existing.id, payload);
|
||||
console.log(`♻️ ${collectionData.name} 已存在,已按最新结构更新。`);
|
||||
return;
|
||||
}
|
||||
console.error(`❌ 处理集合 ${collectionData.name} 失败:`, {
|
||||
status: error.status,
|
||||
message: error.message,
|
||||
response: error.response?.data,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyCollections(targetCollections) {
|
||||
console.log('\n🔍 开始校验 newpb 表结构与索引...');
|
||||
|
||||
for (const target of targetCollections) {
|
||||
const remote = await pb.collections.getOne(target.name);
|
||||
const remoteFieldNames = new Set((remote.fields || []).map((field) => field.name));
|
||||
const missingFields = target.fields
|
||||
.map((field) => field.name)
|
||||
.filter((fieldName) => !remoteFieldNames.has(fieldName));
|
||||
|
||||
const remoteIndexes = new Set(remote.indexes || []);
|
||||
const missingIndexes = target.indexes.filter((indexSql) => !remoteIndexes.has(indexSql));
|
||||
|
||||
if (remote.type !== target.type) {
|
||||
throw new Error(`${target.name} 类型不匹配,期望 ${target.type},实际 ${remote.type}`);
|
||||
}
|
||||
|
||||
if (missingFields.length === 0 && missingIndexes.length === 0) {
|
||||
console.log(`✅ ${target.name} 校验通过。`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`❌ ${target.name} 校验失败:`);
|
||||
if (missingFields.length > 0) {
|
||||
console.log(` - 缺失字段: ${missingFields.join(', ')}`);
|
||||
}
|
||||
if (missingIndexes.length > 0) {
|
||||
console.log(` - 缺失索引: ${missingIndexes.join(' | ')}`);
|
||||
}
|
||||
|
||||
throw new Error(`${target.name} 结构与预期不一致`);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
Reference in New Issue
Block a user