提交 9623d948 authored 作者: 王鹏飞's avatar 王鹏飞

refactor!: 迁移到 Fastify 5 并统一 DMS snake_case 契约

结构(fastify-cli 官方布局): - plugins/ 用 fastify-plugin 装饰共享资源(db/mysql/mongoose),生命周期随实例 - routes/ 目录即 URL 前缀,autoload 注册;DMS 用 autohooks.js 做 onRequest 鉴权 - 新增 schemas/(zod 契约)、clients/(外部系统)、lib/(纯工具)、db/schema/、models/ - 跨目录引用改用 #src/* 子路径别名 契约(破坏性): - DMS 接口的请求体/查询参数/响应字段统一 snake_case,与 MySQL 列名一致, 派生字段(can_edit_project、editable_stages、project_manager_name 等)同样下划线 - logs(Mongo) 与微信接口保持 camelCase - center-dms 需与本次同批次发布 其他: - 错误处理:4xx 记 warn(不打堆栈),5xx 记 error;统一错误响应结构 - 性能:日志索引、MySQL 连接池可配、gzip 压缩 - 运维:/health/ready 探活、x-request-id 回传、SIGTERM 优雅退出 - 依赖升级(axios/mysql2/mongoose/zod/fastify 等),生产漏洞 7 → 0
上级 6fcb7409
This source diff could not be displayed because it is too large. You can view the blob instead.
{ {
"name": "ezijing-node-server", "name": "ezijing-node-server",
"version": "2.0.0", "version": "2.0.0",
"description": "ezijing-node-server - Modern Node.js API server", "description": "ezijing-node-server - Fastify API server",
"private": true,
"type": "module", "type": "module",
"main": "src/index.js", "repository": {
"type": "git",
"url": "http://172.16.3.193/ezijing-web/ezijing-node-server.git"
},
"imports": {
"#src/*": "./src/*"
},
"scripts": { "scripts": {
"start": "node src/index.js", "start": "node src/index.js",
"dev": "cross-env NODE_ENV=development node --watch src/index.js", "dev": "cross-env NODE_ENV=development node --watch src/index.js",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"lint": "eslint src/", "lint": "eslint .",
"lint:fix": "eslint --fix src/", "lint:fix": "eslint --fix .",
"deploy": "cross-env NODE_ENV=production SERVER_PORT=5101 DOTENV_CONFIG_PATH=.env.prod pm2 start src/index.js -n ezijing-node-server", "deploy": "NODE_ENV=production DOTENV_CONFIG_PATH=.env.prod pm2 start src/index.js -n ezijing-node-server --kill-timeout 10000",
"reload": "pm2 reload ezijing-node-server" "reload": "pm2 reload ezijing-node-server",
"test": "node --test",
"db:migrate": "drizzle-kit migrate",
"db:status": "node scripts/db-status.mjs",
"check": "npm run lint && npm test",
"test:watch": "node --test --watch"
}, },
"author": "King", "author": "King",
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=20.19.0"
}, },
"devDependencies": { "devDependencies": {
"cross-env": "^10.1.0",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"eslint": "^9.15.0", "eslint": "^10.10.0",
"globals": "^15.12.0", "globals": "^17.12.0"
"pino-pretty": "^13.1.3"
}, },
"dependencies": { "dependencies": {
"@fastify/autoload": "^6.5.0",
"@fastify/compress": "^9.2.0",
"@fastify/cors": "^11.3.0",
"@fastify/formbody": "^9.0.0",
"@fastify/http-proxy": "^11.6.2",
"@fastify/swagger": "^9.8.1",
"@fastify/swagger-ui": "^6.1.1",
"ali-oss": "^6.23.0", "ali-oss": "^6.23.0",
"axios": "^1.15.2", "axios": "^1.20.0",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"dotenv": "^16.4.5", "dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"express": "^5.2.1", "fastify": "^5.12.3",
"express-http-proxy": "^2.1.2", "fastify-plugin": "^6.0.0",
"mongoose": "^8.23.1", "fastify-type-provider-zod": "^7.0.0",
"lru-cache": "^11.5.2",
"mongoose": "^9.9.5",
"mysql2": "^3.20.0", "mysql2": "^3.20.0",
"pino": "^10.3.1", "pino": "^10.3.1",
"pino-http": "^11.0.0",
"ua-parser-js": "^2.0.6", "ua-parser-js": "^2.0.6",
"zod": "^4.4.3" "zod": "^4.4.3"
} }
......
import express from 'express' import path from 'node:path'
import cors from 'cors' import { fileURLToPath } from 'node:url'
import proxy from 'express-http-proxy' import autoload from '@fastify/autoload'
import pinoHttp from 'pino-http' import { hasZodFastifySchemaValidationErrors } from 'fastify-type-provider-zod'
import logger from './lib/logger.js' import { error } from './lib/response.js'
import { notFound, errorHandler } from './middleware/error.js'
import wechatRoutes from './modules/wechat/wechat.routes.js' const __dirname = path.dirname(fileURLToPath(import.meta.url))
import wxChartRoutes from './modules/wx-chart/wx-chart.routes.js'
import logsRoutes from './modules/logs/logs.routes.js' /**
import dmsRoutes from './modules/dms/dms.routes.js' * 根插件(Fastify 官方推荐结构):
* plugins/ 共享插件,使用 fastify-plugin 包装,装饰器/编译器对全局可见
const app = express() * routes/ 路由插件,目录名即路由前缀;autohooks.js 为同级及子级路由挂钩子
*
// Trust proxy * 错误处理器在这里(根上下文)设置,并且必须在加载 routes 之前,
app.set('trust proxy', true) * 子插件在注册时继承父级的 error handler。
*/
// Request logging export default async function app(fastify, opts) {
app.use(pinoHttp({ logger, autoLogging: process.env.NODE_ENV !== 'test' })) fastify.setNotFoundHandler((request, reply) => {
return error(reply, '接口未定义', 404, { path: request.url, method: request.method })
// Middleware })
app.use(cors({ credentials: true }))
app.use('/api/usercenter', proxy('https://api-usercenter.ezijing.com')) fastify.setErrorHandler((err, request, reply) => {
// 增加请求体大小限制,支持批量日志上报(默认 100kb,增加到 10mb) if (hasZodFastifySchemaValidationErrors(err)) {
app.use(express.json({ limit: '10mb' })) const message = err.validation
app.use(express.urlencoded({ extended: true, limit: '10mb' })) .map((e) => {
const p = e.instancePath.replace(/^\//, '').replaceAll('/', '.')
// Health check return `${p ? p + ': ' : ''}${e.message}`
app.get('/health', (req, res) => res.json({ status: 'ok', timestamp: Date.now() })) })
.join('; ')
// Modules return error(reply, message, 400, { details: err.validation })
app.use(wechatRoutes) }
app.use(wxChartRoutes)
app.use('/api', logsRoutes) const candidate = Number(err.statusCode ?? err.status)
app.use('/api', dmsRoutes) const statusCode = Number.isInteger(candidate) && candidate >= 100 && candidate < 1000 ? candidate : 500
const message = process.env.NODE_ENV === 'production' && statusCode === 500 ? 'Internal Server Error' : err.message
// Error handling
app.use(notFound) if (statusCode >= 500) {
app.use(errorHandler) request.log.error({ err, url: request.url, method: request.method }, 'Request error')
} else {
export default app // 4xx 是正常业务结果(登录态失效、参数不合法等),记摘要即可,避免刷屏淹没真正的故障
request.log.warn(
{ statusCode, url: request.url, method: request.method, message: err.message },
'Request failed',
)
}
return error(reply, message, statusCode)
})
await fastify.register(autoload, {
dir: path.join(__dirname, 'plugins'),
options: { ...opts },
})
await fastify.register(autoload, {
dir: path.join(__dirname, 'routes'),
autoHooks: true,
cascadeHooks: true,
options: { ...opts },
})
}
import OSS from 'ali-oss' import OSS from 'ali-oss'
import config from '../config.js' import config from '#src/config.js'
let client let client
......
import crypto from 'node:crypto' import crypto from 'node:crypto'
import axios from 'axios' import axios from 'axios'
import config from '../../../config.js' import config from '#src/config.js'
import { createPermissionSignature } from './permission-sign.js' import { createCachedFetcher } from '#src/lib/cached-fetch.js'
import logger from '#src/lib/logger.js'
import { createPermissionSignature } from '#src/lib/sign.js'
import { getUserProfiles } from './user-center.client.js' import { getUserProfiles } from './user-center.client.js'
const cache = new Map() // 权限取数器:TTL + 容量上限 + 并发去重
const requests = new Map() const getAuthorization = createCachedFetcher({ ttlMs: config.dmsAuth.permissionCacheTtlSeconds * 1000 })
const unwrapItems = (payload) => payload?.data?.items ?? [] const unwrapItems = (payload) => payload?.data?.items ?? []
...@@ -37,33 +39,17 @@ const requestRaw = async (path, tgc, params = {}) => { ...@@ -37,33 +39,17 @@ const requestRaw = async (path, tgc, params = {}) => {
const request = async (path, tgc, params = {}) => unwrapItems(await requestRaw(path, tgc, params)) const request = async (path, tgc, params = {}) => unwrapItems(await requestRaw(path, tgc, params))
export const getUserAuthorization = async (ssoId, tgc) => { export const getUserAuthorization = (ssoId, tgc) =>
const cached = cache.get(ssoId) getAuthorization(ssoId, async () => {
if (cached && cached.expiresAt > Date.now()) return cached.value const [roles, routes] = await Promise.all([
request('/api/v1/user/roles', tgc),
const pending = requests.get(ssoId) request('/api/v1/user/routes', tgc),
if (pending) return pending ])
return {
const requestPromise = Promise.all([ roles: Array.isArray(roles) ? roles : [],
request('/api/v1/user/roles', tgc), routes: Array.isArray(routes) ? routes : [],
request('/api/v1/user/routes', tgc), }
]) })
.then(([roles, routes]) => {
const value = {
roles: Array.isArray(roles) ? roles : [],
routes: Array.isArray(routes) ? routes : [],
}
cache.set(ssoId, {
value,
expiresAt: Date.now() + config.dmsAuth.permissionCacheTtlSeconds * 1000,
})
return value
})
.finally(() => requests.delete(ssoId))
requests.set(ssoId, requestPromise)
return requestPromise
}
export const getApplicationUsers = async (tgc, params = {}) => { export const getApplicationUsers = async (tgc, params = {}) => {
const payload = await requestRaw('/api/v1/app/users', tgc, params) const payload = await requestRaw('/api/v1/app/users', tgc, params)
...@@ -75,7 +61,7 @@ export const getApplicationUsers = async (tgc, params = {}) => { ...@@ -75,7 +61,7 @@ export const getApplicationUsers = async (tgc, params = {}) => {
try { try {
profiles = await getUserProfiles(items.map((item) => item.sso_id)) profiles = await getUserProfiles(items.map((item) => item.sso_id))
} catch (error) { } catch (error) {
console.warn('用户中心批量查询失败,成员列表将保留权限中心数据', error.message) logger.warn({ err: error, count: items.length }, '用户中心批量查询失败,成员列表将保留权限中心数据')
} }
return { return {
......
import crypto from 'node:crypto' import crypto from 'node:crypto'
import axios from 'axios' import axios from 'axios'
import config from '../../../config.js' import config from '#src/config.js'
const MAX_BATCH_SIZE = 500 const MAX_BATCH_SIZE = 500
...@@ -34,7 +34,7 @@ export const getUserProfiles = async (ssoIds) => { ...@@ -34,7 +34,7 @@ export const getUserProfiles = async (ssoIds) => {
if (!ids.length) return {} if (!ids.length) return {}
if (!userCenterBatchUrl || !userCenterSalt) { if (!userCenterBatchUrl || !userCenterSalt) {
console.warn('用户中心批量查询未配置 USER_CENTER_BATCH_URL 或 USER_CENTER_SALT') logger.warn('用户中心批量查询未配置 USER_CENTER_SALT,成员列表将缺少姓名(检查 .env)')
return {} return {}
} }
......
import 'dotenv/config' import 'dotenv/config'
import fs from 'node:fs'
import path from 'path' import path from 'path'
import { z } from 'zod'
const port = (fallback) => z.coerce.number().int().min(1).max(65535).default(fallback)
const int = (fallback) => z.coerce.number().int().positive().default(fallback)
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
ENABLE_DOCS: z.string().optional(),
SERVER_PORT: port(4101),
DATA_DIR: z.string().optional(),
MONGODB_URI: z.string().default(''),
MONGO_MAX_POOL_SIZE: int(10),
DATABASE_URL: z.string().default(''),
MYSQL_HOST: z.string().default('127.0.0.1'),
MYSQL_PORT: port(3306),
MYSQL_USER: z.string().default('root'),
MYSQL_PASSWORD: z.string().default(''),
MYSQL_DATABASE: z.string().default('com_dms'),
MYSQL_POOL_SIZE: int(10),
OSS_REGION: z.string().default('oss-cn-beijing'),
OSS_BUCKET: z.string().default('webapp-pub'),
OSS_ACCESS_KEY_ID: z.string().default(''),
OSS_ACCESS_KEY_SECRET: z.string().default(''),
OSS_ENDPOINT: z.string().default(''),
CASE_JSON_OBJECT_KEY: z.string().default('case_library/case.json'),
SSO_USER_INFO_URL: z.string().default('https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info'),
USER_CENTER_BATCH_URL: z.string().default('https://api-usercenter.ezijing.com/v2/server/user/multi-get-user-info'),
USER_CENTER_SALT: z.string().default(''),
PERMISSION_API_URL: z.string().default(''),
PERMISSION_APP_ID: z.string().default(''),
PERMISSION_APP_SECRET: z.string().default(''),
SSO_USER_CACHE_TTL_SECONDS: int(180),
PERMISSION_CACHE_TTL_SECONDS: int(180),
})
// 启动即校验环境变量,配置错误立刻失败,而不是运行到某个请求时才炸
const parsed = envSchema.safeParse(process.env)
if (!parsed.success) {
const details = parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`).join('; ')
throw new Error(`环境变量配置错误: ${details}`)
}
const env = parsed.data
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
const config = { const config = {
env: process.env.NODE_ENV || 'development', env: env.NODE_ENV,
port: parseInt(process.env.SERVER_PORT || '4101', 10) || 4101, version: process.env.APP_VERSION || pkg.version,
dataDir: process.env.DATA_DIR || path.resolve(process.cwd(), '../node-server-data'), docs: {
// 生产默认不暴露 /docs;需要临时查看时设 ENABLE_DOCS=true
enabled: env.ENABLE_DOCS === undefined ? env.NODE_ENV !== 'production' : env.ENABLE_DOCS === 'true',
routePrefix: '/docs',
},
port: env.SERVER_PORT,
dataDir: env.DATA_DIR || path.resolve(process.cwd(), '../node-server-data'),
mongodb: { mongodb: {
uri: process.env.MONGODB_URI || '', uri: env.MONGODB_URI,
maxPoolSize: env.MONGO_MAX_POOL_SIZE,
}, },
mysql: { mysql: {
url: process.env.DATABASE_URL || '', url: env.DATABASE_URL,
host: process.env.MYSQL_HOST || '127.0.0.1', host: env.MYSQL_HOST,
port: parseInt(process.env.MYSQL_PORT || '3306', 10) || 3306, port: env.MYSQL_PORT,
user: process.env.MYSQL_USER || 'root', user: env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD || '', password: env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE || 'com_dms', database: env.MYSQL_DATABASE,
poolSize: env.MYSQL_POOL_SIZE,
}, },
oss: { oss: {
region: process.env.OSS_REGION || 'oss-cn-beijing', region: env.OSS_REGION,
bucket: process.env.OSS_BUCKET || 'webapp-pub', bucket: env.OSS_BUCKET,
accessKeyId: process.env.OSS_ACCESS_KEY_ID || '', accessKeyId: env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || '', accessKeySecret: env.OSS_ACCESS_KEY_SECRET,
endpoint: process.env.OSS_ENDPOINT || '', endpoint: env.OSS_ENDPOINT,
caseJsonObjectKey: process.env.CASE_JSON_OBJECT_KEY || 'case_library/case.json', caseJsonObjectKey: env.CASE_JSON_OBJECT_KEY,
}, },
dmsAuth: { dmsAuth: {
ssoUserInfoUrl: ssoUserInfoUrl: env.SSO_USER_INFO_URL,
process.env.SSO_USER_INFO_URL || 'https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info', userCenterBatchUrl: env.USER_CENTER_BATCH_URL,
userCenterBatchUrl: userCenterSalt: env.USER_CENTER_SALT,
process.env.USER_CENTER_BATCH_URL || 'https://api-usercenter.ezijing.com/v2/server/user/multi-get-user-info', permissionApiUrl: env.PERMISSION_API_URL,
userCenterSalt: process.env.USER_CENTER_SALT || '', permissionAppId: env.PERMISSION_APP_ID,
permissionApiUrl: process.env.PERMISSION_API_URL || '', permissionAppSecret: env.PERMISSION_APP_SECRET,
permissionAppId: process.env.PERMISSION_APP_ID || '', userInfoCacheTtlSeconds: env.SSO_USER_CACHE_TTL_SECONDS,
permissionAppSecret: process.env.PERMISSION_APP_SECRET || '', permissionCacheTtlSeconds: env.PERMISSION_CACHE_TTL_SECONDS,
userInfoCacheTtlSeconds: parseInt(process.env.SSO_USER_CACHE_TTL_SECONDS || '180', 10) || 180,
permissionCacheTtlSeconds: parseInt(process.env.PERMISSION_CACHE_TTL_SECONDS || '180', 10) || 180,
}, },
wechat: { wechat: {
apps: { apps: {
......
import { drizzle } from 'drizzle-orm/mysql2' import { drizzle } from 'drizzle-orm/mysql2'
import { getMysqlPool } from '../lib/mysql.js' import { getMysqlPool } from '#src/lib/mysql.js'
export const db = drizzle(getMysqlPool()) export const db = drizzle(getMysqlPool())
import { bigint, index, longtext, mysqlTable, text, varchar } from 'drizzle-orm/mysql-core' import { bigint, index, longtext, mysqlTable, text, varchar } from 'drizzle-orm/mysql-core'
import { timestamps } from '../db/columns.js' import { timestamps } from './columns.js'
export const cases = mysqlTable( export const cases = mysqlTable(
'case_list', 'case_list',
...@@ -7,15 +7,15 @@ export const cases = mysqlTable( ...@@ -7,15 +7,15 @@ export const cases = mysqlTable(
id: bigint('id', { mode: 'number', unsigned: true }).autoincrement().primaryKey(), id: bigint('id', { mode: 'number', unsigned: true }).autoincrement().primaryKey(),
name: varchar('name', { length: 255 }).notNull(), name: varchar('name', { length: 255 }).notNull(),
description: text('description'), description: text('description'),
productId: bigint('product_id', { mode: 'number', unsigned: true }), product_id: bigint('product_id', { mode: 'number', unsigned: true }),
productName: varchar('product_name', { length: 255 }), product_name: varchar('product_name', { length: 255 }),
files: longtext('files'), files: longtext('files'),
operatorUserId: varchar('operator_user_id', { length: 64 }), operator_user_id: varchar('operator_user_id', { length: 64 }),
operatorName: varchar('operator_name', { length: 120 }), operator_name: varchar('operator_name', { length: 120 }),
...timestamps, ...timestamps,
}, },
(table) => ({ (table) => ({
productIdIdx: index('idx_cases_product_id').on(table.productId), productIdIdx: index('idx_cases_product_id').on(table.product_id),
operatorUserIdIdx: index('idx_cases_operator_user_id').on(table.operatorUserId), operatorUserIdIdx: index('idx_cases_operator_user_id').on(table.operator_user_id),
}) })
) )
...@@ -2,8 +2,8 @@ import { datetime } from 'drizzle-orm/mysql-core' ...@@ -2,8 +2,8 @@ import { datetime } from 'drizzle-orm/mysql-core'
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
export const timestamps = { export const timestamps = {
createdAt: datetime('created_at', { mode: 'string' }).notNull().default(sql`CURRENT_TIMESTAMP`), created_at: datetime('created_at', { mode: 'string' }).notNull().default(sql`CURRENT_TIMESTAMP`),
updatedAt: datetime('updated_at', { mode: 'string' }) updated_at: datetime('updated_at', { mode: 'string' })
.notNull() .notNull()
.default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), .default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
} }
import { bigint, index, int, mysqlTable, text, uniqueIndex, varchar } from 'drizzle-orm/mysql-core' import { bigint, index, int, mysqlTable, text, uniqueIndex, varchar } from 'drizzle-orm/mysql-core'
import { timestamps } from '../db/columns.js' import { timestamps } from './columns.js'
export const PRODUCT_STATUSES = { export const PRODUCT_STATUSES = {
DISABLED: 0, DISABLED: 0,
...@@ -13,13 +13,13 @@ export const products = mysqlTable( ...@@ -13,13 +13,13 @@ export const products = mysqlTable(
name: varchar('name', { length: 120 }).notNull(), name: varchar('name', { length: 120 }).notNull(),
status: int('status').notNull().default(PRODUCT_STATUSES.ENABLED), status: int('status').notNull().default(PRODUCT_STATUSES.ENABLED),
description: text('description'), description: text('description'),
operatorUserId: varchar('operator_user_id', { length: 64 }), operator_user_id: varchar('operator_user_id', { length: 64 }),
operatorName: varchar('operator_name', { length: 120 }), operator_name: varchar('operator_name', { length: 120 }),
...timestamps, ...timestamps,
}, },
(table) => ({ (table) => ({
nameIdx: uniqueIndex('uk_products_name').on(table.name), nameIdx: uniqueIndex('uk_products_name').on(table.name),
statusIdx: index('idx_products_status').on(table.status), statusIdx: index('idx_products_status').on(table.status),
operatorUserIdIdx: index('idx_products_operator_user_id').on(table.operatorUserId), operatorUserIdIdx: index('idx_products_operator_user_id').on(table.operator_user_id),
}) })
) )
import { bigint, datetime, index, mysqlTable, tinyint, uniqueIndex, varchar } from 'drizzle-orm/mysql-core' import { bigint, datetime, index, mysqlTable, tinyint, uniqueIndex, varchar } from 'drizzle-orm/mysql-core'
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
import { timestamps } from '../db/columns.js' import { timestamps } from './columns.js'
import { projects } from '../projects/schema.js' import { projects } from './projects.js'
export const PROJECT_ROLES = [ export const PROJECT_ROLES = [
'project_manager', 'project_manager',
...@@ -16,20 +16,20 @@ export const PROJECT_ROLES = [ ...@@ -16,20 +16,20 @@ export const PROJECT_ROLES = [
export const projectRoleAssignments = mysqlTable('project_role_assignments', { export const projectRoleAssignments = mysqlTable('project_role_assignments', {
id: bigint('id', { mode: 'number', unsigned: true }).autoincrement().primaryKey(), id: bigint('id', { mode: 'number', unsigned: true }).autoincrement().primaryKey(),
projectId: bigint('project_id', { mode: 'number', unsigned: true }) project_id: bigint('project_id', { mode: 'number', unsigned: true })
.notNull() .notNull()
.references(() => projects.id, { onDelete: 'cascade' }), .references(() => projects.id, { onDelete: 'cascade' }),
ssoId: varchar('sso_id', { length: 64 }).notNull(), sso_id: varchar('sso_id', { length: 64 }).notNull(),
userName: varchar('user_name', { length: 120 }), user_name: varchar('user_name', { length: 120 }),
mobile: varchar('mobile', { length: 64 }), mobile: varchar('mobile', { length: 64 }),
role: varchar('role', { length: 32 }).notNull(), role: varchar('role', { length: 32 }).notNull(),
status: tinyint('status', { unsigned: true }).notNull().default(1), status: tinyint('status', { unsigned: true }).notNull().default(1),
assignedBySsoId: varchar('assigned_by_sso_id', { length: 64 }), assigned_by_sso_id: varchar('assigned_by_sso_id', { length: 64 }),
assignedAt: datetime('assigned_at', { mode: 'string' }).notNull().default(sql`CURRENT_TIMESTAMP`), assigned_at: datetime('assigned_at', { mode: 'string' }).notNull().default(sql`CURRENT_TIMESTAMP`),
...timestamps, ...timestamps,
}, (table) => ({ }, (table) => ({
projectIdx: index('idx_project_roles_project_id').on(table.projectId), projectIdx: index('idx_project_roles_project_id').on(table.project_id),
userIdx: index('idx_project_roles_sso_id').on(table.ssoId), userIdx: index('idx_project_roles_sso_id').on(table.sso_id),
projectRoleIdx: index('idx_project_roles_project_role').on(table.projectId, table.role), projectRoleIdx: index('idx_project_roles_project_role').on(table.project_id, table.role),
assignmentIdx: uniqueIndex('uk_project_roles_assignment').on(table.projectId, table.ssoId, table.role), assignmentIdx: uniqueIndex('uk_project_roles_assignment').on(table.project_id, table.sso_id, table.role),
})) }))
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论