提交 6fcb7409 authored 作者: 王鹏飞's avatar 王鹏飞

feat: add case synchronization feature and integrate OSS for JSON storage

- Added `ali-oss` dependency for OSS integration. - Implemented `import-cases.js` script to read and normalize case data from a JSON source. - Enhanced configuration to include OSS settings for case JSON storage. - Created `oss.js` library for handling OSS uploads. - Updated error handling middleware to improve status code validation. - Refactored authentication service to cache user data and reduce redundant requests. - Introduced `syncCasesToPublic` function in the cases service to upload case data to OSS. - Added new route for case synchronization in the DMS routes. - Updated project service to ensure consistent product name handling.
上级 2bcd3587
...@@ -9,13 +9,19 @@ MYSQL_USER=root ...@@ -9,13 +9,19 @@ MYSQL_USER=root
MYSQL_PASSWORD= MYSQL_PASSWORD=
MYSQL_DATABASE=com_dms MYSQL_DATABASE=com_dms
OSS_REGION=oss-cn-beijing
OSS_BUCKET=webapp-pub
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_ENDPOINT=
CASE_JSON_OBJECT_KEY=case_library/case.json
# DMS unified authentication and authorization # DMS unified authentication and authorization
# Only set DMS_AUTH_DISABLED=true for local development without SSO.
DMS_AUTH_DISABLED=false
SSO_USER_INFO_URL=https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info SSO_USER_INFO_URL=https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info
USER_CENTER_BATCH_URL=https://api-usercenter.ezijing.com/v2/server/user/multi-get-user-info USER_CENTER_BATCH_URL=https://api-usercenter.ezijing.com/v2/server/user/multi-get-user-info
USER_CENTER_SALT= USER_CENTER_SALT=
PERMISSION_API_URL= PERMISSION_API_URL=
PERMISSION_APP_ID= PERMISSION_APP_ID=
PERMISSION_APP_SECRET= PERMISSION_APP_SECRET=
SSO_USER_CACHE_TTL_SECONDS=180
PERMISSION_CACHE_TTL_SECONDS=180 PERMISSION_CACHE_TTL_SECONDS=180
...@@ -9,14 +9,19 @@ MYSQL_USER=u_com_dms ...@@ -9,14 +9,19 @@ MYSQL_USER=u_com_dms
MYSQL_PASSWORD=com_dms2026 MYSQL_PASSWORD=com_dms2026
MYSQL_DATABASE=com_dms MYSQL_DATABASE=com_dms
OSS_REGION=oss-cn-beijing
OSS_BUCKET=webapp-pub
OSS_ACCESS_KEY_ID=LTAI5t6yKc2m47ZUZTBbUBuP
OSS_ACCESS_KEY_SECRET=mhuPVC43NAkzBaheHyWGjREiIrLax0
OSS_ENDPOINT=oss-cn-beijing.aliyuncs.com
CASE_JSON_OBJECT_KEY=case_library/case.json
# DMS unified authentication and authorization # DMS unified authentication and authorization
# Only set DMS_AUTH_DISABLED=true for local development without SSO.
DMS_AUTH_DISABLED=false
SSO_USER_INFO_URL=https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info SSO_USER_INFO_URL=https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info
USER_CENTER_BATCH_URL=https://api-usercenter.ezijing.com/v2/server/user/multi-get-user-info USER_CENTER_BATCH_URL=https://api-usercenter.ezijing.com/v2/server/user/multi-get-user-info
USER_CENTER_SALT=n7xAav2tSjn9Ysnr USER_CENTER_SALT=n7xAav2tSjn9Ysnr
PERMISSION_API_URL=https://permissions-api.ezijing.com PERMISSION_API_URL=https://permissions-api.ezijing.com
PERMISSION_APP_ID=ezijing_82b452761ada1b79fd12b9fcbd73629e PERMISSION_APP_ID=ezijing_82b452761ada1b79fd12b9fcbd73629e
PERMISSION_APP_SECRET=f19d1e788ac9ea0e3c72ef2e3caee8f0 PERMISSION_APP_SECRET=f19d1e788ac9ea0e3c72ef2e3caee8f0
SSO_USER_CACHE_TTL_SECONDS=180
PERMISSION_CACHE_TTL_SECONDS=180 PERMISSION_CACHE_TTL_SECONDS=180
差异被折叠。
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"pino-pretty": "^13.1.3" "pino-pretty": "^13.1.3"
}, },
"dependencies": { "dependencies": {
"ali-oss": "^6.23.0",
"axios": "^1.15.2", "axios": "^1.15.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
......
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import dotenv from 'dotenv'
import mysql from 'mysql2/promise'
dotenv.config({ path: process.env.DOTENV_CONFIG_PATH || '.env' })
const DEFAULT_SOURCE = 'https://webapp-pub.ezijing.com/case_library/case.json'
const args = process.argv.slice(2)
const source = args.find((arg) => arg !== '--dry-run') || process.env.CASE_JSON_SOURCE || DEFAULT_SOURCE
const dryRun = process.env.DRY_RUN === 'true' || args.includes('--dry-run')
const fileTypeNames = {
'1': '案例原文',
'2': '案例PPT',
'3': '案例指导书',
'4': '案例操作视频',
'5': '案例数据集',
'6': '其他资料',
}
async function readSource(sourcePath) {
if (/^https?:\/\//i.test(sourcePath)) {
const response = await fetch(sourcePath)
if (!response.ok) throw new Error(`读取 case.json 失败:HTTP ${response.status}`)
return response.json()
}
const content = await fs.readFile(path.resolve(sourcePath), 'utf8')
return JSON.parse(content)
}
function unwrapCases(payload) {
if (Array.isArray(payload)) return payload
if (Array.isArray(payload?.data)) return payload.data
if (Array.isArray(payload?.list)) return payload.list
if (Array.isArray(payload?.data?.list)) return payload.data.list
throw new Error('case.json 不是案例数组格式')
}
function normalizeText(value, fallback = '') {
return value == null ? fallback : String(value)
}
function normalizeFiles(files, index) {
if (files == null) return []
if (!Array.isArray(files)) throw new Error(`第 ${index + 1} 条案例的 files 不是数组`)
return files.map((file, fileIndex) => {
if (!file || typeof file !== 'object') {
throw new Error(`第 ${index + 1} 条案例的第 ${fileIndex + 1} 个附件格式错误`)
}
const normalized = { ...file }
if (normalized.type != null) normalized.type = String(normalized.type)
if (normalized.type && !normalized.type_name) normalized.type_name = fileTypeNames[normalized.type] || ''
if (normalized.name != null) normalized.name = String(normalized.name)
if (normalized.url != null) normalized.url = String(normalized.url)
return normalized
})
}
function toMysqlDate(value, fallback) {
const text = normalizeText(value).trim()
if (!text) return fallback
// 已经是数据库时间时原样保留,避免时区转换。
const plain = text.match(/^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})/)
if (plain) return plain[1].replace('T', ' ')
const date = new Date(text)
if (Number.isNaN(date.getTime())) throw new Error(`无法解析时间:${text}`)
const pad = (part) => String(part).padStart(2, '0')
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`
}
function normalizeCase(item, index) {
if (!item || typeof item !== 'object') throw new Error(`第 ${index + 1} 条案例格式错误`)
const now = toMysqlDate(new Date().toISOString())
const operator = item.operator || {}
const operatorUserId = normalizeText(item.operatorUserId ?? operator.id)
const operatorName = normalizeText(item.operatorName ?? operator.name, '数据迁移')
const name = normalizeText(item.name, '未命名案例').trim()
if (!name) throw new Error(`第 ${index + 1} 条案例名称为空`)
if (name.length > 255) throw new Error(`第 ${index + 1} 条案例名称超过 255 个字符`)
if (operatorUserId.length > 64) throw new Error(`第 ${index + 1} 条案例操作人 ID 超过 64 个字符`)
if (operatorName.length > 120) throw new Error(`第 ${index + 1} 条案例操作人姓名超过 120 个字符`)
return {
name,
description: normalizeText(item.description),
productId: 2,
productName: '办公智能与应用实践教学平台',
files: JSON.stringify(normalizeFiles(item.files, index)),
operatorUserId,
operatorName,
createdAt: toMysqlDate(item.created_at ?? item.createdAt, now),
updatedAt: toMysqlDate(item.updated_at ?? item.updatedAt, now),
}
}
function createPool() {
if (process.env.DATABASE_URL) return mysql.createPool(process.env.DATABASE_URL)
return mysql.createPool({
host: process.env.MYSQL_HOST || '127.0.0.1',
port: Number(process.env.MYSQL_PORT || 3306),
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || '',
database: process.env.MYSQL_DATABASE || 'com_dms',
})
}
async function main() {
console.log(`读取案例数据:${source}`)
const cases = unwrapCases(await readSource(source)).map(normalizeCase)
console.log(`发现 ${cases.length} 条案例`)
if (dryRun) {
console.log('DRY_RUN=true,仅校验数据,不写入数据库')
return
}
const pool = createPool()
const connection = await pool.getConnection()
try {
const [rows] = await connection.query('SELECT COUNT(*) AS total FROM case_list')
const existingCount = Number(rows[0]?.total || 0)
if (existingCount > 0) {
throw new Error(`case_list 已有 ${existingCount} 条数据,为避免覆盖,已停止导入`)
}
await connection.beginTransaction()
for (const [index, item] of cases.entries()) {
const [result] = await connection.execute(
`INSERT INTO case_list
(name, description, product_id, product_name, files, operator_user_id, operator_name, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
item.name,
item.description,
item.productId,
item.productName,
item.files,
item.operatorUserId,
item.operatorName,
item.createdAt,
item.updatedAt,
],
)
console.log(`[${index + 1}/${cases.length}] → ${result.insertId} ${item.name}`)
}
const [verifyRows] = await connection.query('SELECT COUNT(*) AS total FROM case_list')
const importedCount = Number(verifyRows[0]?.total || 0)
if (importedCount !== cases.length) {
throw new Error(`导入数量校验失败:期望 ${cases.length},实际 ${importedCount}`)
}
await connection.commit()
console.log(`导入完成:${importedCount} 条`)
} catch (error) {
await connection.rollback()
throw error
} finally {
connection.release()
await pool.end()
}
}
main().catch((error) => {
console.error(`导入失败:${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
})
...@@ -16,8 +16,15 @@ const config = { ...@@ -16,8 +16,15 @@ const config = {
password: process.env.MYSQL_PASSWORD || '', password: process.env.MYSQL_PASSWORD || '',
database: process.env.MYSQL_DATABASE || 'com_dms', database: process.env.MYSQL_DATABASE || 'com_dms',
}, },
oss: {
region: process.env.OSS_REGION || 'oss-cn-beijing',
bucket: process.env.OSS_BUCKET || 'webapp-pub',
accessKeyId: process.env.OSS_ACCESS_KEY_ID || '',
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || '',
endpoint: process.env.OSS_ENDPOINT || '',
caseJsonObjectKey: process.env.CASE_JSON_OBJECT_KEY || 'case_library/case.json',
},
dmsAuth: { dmsAuth: {
disabled: process.env.DMS_AUTH_DISABLED === 'true',
ssoUserInfoUrl: ssoUserInfoUrl:
process.env.SSO_USER_INFO_URL || 'https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info', process.env.SSO_USER_INFO_URL || 'https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info',
userCenterBatchUrl: userCenterBatchUrl:
...@@ -26,6 +33,7 @@ const config = { ...@@ -26,6 +33,7 @@ const config = {
permissionApiUrl: process.env.PERMISSION_API_URL || '', permissionApiUrl: process.env.PERMISSION_API_URL || '',
permissionAppId: process.env.PERMISSION_APP_ID || '', permissionAppId: process.env.PERMISSION_APP_ID || '',
permissionAppSecret: process.env.PERMISSION_APP_SECRET || '', permissionAppSecret: process.env.PERMISSION_APP_SECRET || '',
userInfoCacheTtlSeconds: parseInt(process.env.SSO_USER_CACHE_TTL_SECONDS || '180', 10) || 180,
permissionCacheTtlSeconds: parseInt(process.env.PERMISSION_CACHE_TTL_SECONDS || '180', 10) || 180, permissionCacheTtlSeconds: parseInt(process.env.PERMISSION_CACHE_TTL_SECONDS || '180', 10) || 180,
}, },
wechat: { wechat: {
......
import OSS from 'ali-oss'
import config from '../config.js'
let client
const getOssClient = () => {
if (client) return client
const { region, bucket, accessKeyId, accessKeySecret, endpoint } = config.oss
if (!accessKeyId || !accessKeySecret) {
throw new Error('OSS 同步配置不完整,请配置 OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET')
}
client = new OSS({
region,
bucket,
accessKeyId,
accessKeySecret,
...(endpoint ? { endpoint } : {}),
})
return client
}
export const uploadJson = async (objectKey, payload) => {
const content = Buffer.from(JSON.stringify(payload, null, 2), 'utf8')
const result = await getOssClient().put(objectKey, content, {
mime: 'application/json',
headers: { 'Cache-Control': 'no-cache' },
})
return {
objectKey,
url: result.url,
size: content.length,
}
}
...@@ -6,7 +6,8 @@ export const notFound = (req, res) => { ...@@ -6,7 +6,8 @@ export const notFound = (req, res) => {
} }
export const errorHandler = (err, req, res, _next) => { export const errorHandler = (err, req, res, _next) => {
const statusCode = err.statusCode || err.status || 500 const candidate = Number(err.statusCode ?? err.status)
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 const message = process.env.NODE_ENV === 'production' && statusCode === 500 ? 'Internal Server Error' : err.message
logger.error({ err, url: req.url, method: req.method }, 'Request error') logger.error({ err, url: req.url, method: req.method }, 'Request error')
......
...@@ -4,6 +4,8 @@ import { getUserAuthorization } from './permission.client.js' ...@@ -4,6 +4,8 @@ import { getUserAuthorization } from './permission.client.js'
import { hasAnyDmsRouteAccess, normalizeRoute } from './route-access.js' import { hasAnyDmsRouteAccess, normalizeRoute } from './route-access.js'
const roleTag = (role) => role?.tag const roleTag = (role) => role?.tag
const userCache = new Map()
const userRequests = new Map()
export const getTgc = (req) => { export const getTgc = (req) => {
const cookies = req.headers.cookie || '' const cookies = req.headers.cookie || ''
...@@ -22,18 +24,7 @@ const normalizeSsoUser = (payload) => { ...@@ -22,18 +24,7 @@ const normalizeSsoUser = (payload) => {
} }
} }
export const authenticateRequest = async (req) => { const fetchSsoUser = async (tgc) => {
if (config.dmsAuth.disabled) {
return { ssoId: 'local-dev', name: '本地开发用户', mobile: '', roles: ['admin'], routes: [] }
}
const tgc = getTgc(req)
if (!tgc) {
const error = new Error('登录状态已失效')
error.statusCode = 401
throw error
}
let response let response
try { try {
response = await axios.get(config.dmsAuth.ssoUserInfoUrl, { response = await axios.get(config.dmsAuth.ssoUserInfoUrl, {
...@@ -52,6 +43,39 @@ export const authenticateRequest = async (req) => { ...@@ -52,6 +43,39 @@ export const authenticateRequest = async (req) => {
error.statusCode = 401 error.statusCode = 401
throw error throw error
} }
return user
}
const getSsoUser = async (tgc) => {
const cached = userCache.get(tgc)
if (cached && cached.expiresAt > Date.now()) return cached.value
const pending = userRequests.get(tgc)
if (pending) return pending
const request = fetchSsoUser(tgc)
.then((user) => {
userCache.set(tgc, {
value: user,
expiresAt: Date.now() + config.dmsAuth.userInfoCacheTtlSeconds * 1000,
})
return user
})
.finally(() => userRequests.delete(tgc))
userRequests.set(tgc, request)
return request
}
export const authenticateRequest = async (req) => {
const tgc = getTgc(req)
if (!tgc) {
const error = new Error('登录状态已失效')
error.statusCode = 401
throw error
}
const user = await getSsoUser(tgc)
let authorization let authorization
try { try {
......
...@@ -5,6 +5,7 @@ import { createPermissionSignature } from './permission-sign.js' ...@@ -5,6 +5,7 @@ import { createPermissionSignature } from './permission-sign.js'
import { getUserProfiles } from './user-center.client.js' import { getUserProfiles } from './user-center.client.js'
const cache = new Map() const cache = new Map()
const requests = new Map()
const unwrapItems = (payload) => payload?.data?.items ?? [] const unwrapItems = (payload) => payload?.data?.items ?? []
...@@ -40,10 +41,14 @@ export const getUserAuthorization = async (ssoId, tgc) => { ...@@ -40,10 +41,14 @@ export const getUserAuthorization = async (ssoId, tgc) => {
const cached = cache.get(ssoId) const cached = cache.get(ssoId)
if (cached && cached.expiresAt > Date.now()) return cached.value if (cached && cached.expiresAt > Date.now()) return cached.value
const [roles, routes] = await Promise.all([ const pending = requests.get(ssoId)
if (pending) return pending
const requestPromise = Promise.all([
request('/api/v1/user/roles', tgc), request('/api/v1/user/roles', tgc),
request('/api/v1/user/routes', tgc), request('/api/v1/user/routes', tgc),
]) ])
.then(([roles, routes]) => {
const value = { const value = {
roles: Array.isArray(roles) ? roles : [], roles: Array.isArray(roles) ? roles : [],
routes: Array.isArray(routes) ? routes : [], routes: Array.isArray(routes) ? routes : [],
...@@ -53,6 +58,11 @@ export const getUserAuthorization = async (ssoId, tgc) => { ...@@ -53,6 +58,11 @@ export const getUserAuthorization = async (ssoId, tgc) => {
expiresAt: Date.now() + config.dmsAuth.permissionCacheTtlSeconds * 1000, expiresAt: Date.now() + config.dmsAuth.permissionCacheTtlSeconds * 1000,
}) })
return value return value
})
.finally(() => requests.delete(ssoId))
requests.set(ssoId, requestPromise)
return requestPromise
} }
export const getApplicationUsers = async (tgc, params = {}) => { export const getApplicationUsers = async (tgc, params = {}) => {
......
...@@ -8,6 +8,11 @@ export const list = async (req, res) => { ...@@ -8,6 +8,11 @@ export const list = async (req, res) => {
success(res, result) success(res, result)
} }
export const sync = async (_req, res) => {
const result = await casesService.syncCasesToPublic()
success(res, result)
}
export const detail = async (req, res) => { export const detail = async (req, res) => {
const record = await casesService.getCaseById(req.params.id) const record = await casesService.getCaseById(req.params.id)
if (!record) return error(res, '案例不存在', 404) if (!record) return error(res, '案例不存在', 404)
......
import { and, count, desc, eq, like, or } from 'drizzle-orm' import { and, count, desc, eq, like, or } from 'drizzle-orm'
import config from '../../../config.js'
import { httpError } from '../../../lib/http-error.js' import { httpError } from '../../../lib/http-error.js'
import { uploadJson } from '../../../lib/oss.js'
import { db } from '../../../db/index.js' import { db } from '../../../db/index.js'
import { cases } from './schema.js' import { cases } from './schema.js'
import { products } from '../products/schema.js'
const parseFiles = (row) => { const parseFiles = (row) => {
if (!row) return row if (!row) return row
...@@ -12,7 +15,25 @@ const parseFiles = (row) => { ...@@ -12,7 +15,25 @@ const parseFiles = (row) => {
} }
} }
const rowsToList = (rows) => rows.map(parseFiles) const rowsToList = (rows) => rows.map((row) => parseFiles({
...row.caseRecord,
productName: row.productName || '',
}))
const toPublicCase = (record) => ({
id: record.id,
name: record.name,
description: record.description || '',
product_id: record.productId ?? null,
product_name: record.productName || '',
files: record.files || [],
operator: {
id: record.operatorUserId || '',
name: record.operatorName || '',
},
created_at: record.createdAt,
updated_at: record.updatedAt,
})
const resolveOperator = (currentUser = {}) => ({ const resolveOperator = (currentUser = {}) => ({
operatorUserId: currentUser.userId || null, operatorUserId: currentUser.userId || null,
...@@ -39,7 +60,14 @@ export const listCases = async (filters = {}, options = {}) => { ...@@ -39,7 +60,14 @@ export const listCases = async (filters = {}, options = {}) => {
const where = buildFilters(filters) const where = buildFilters(filters)
const [list, totalRows] = await Promise.all([ const [list, totalRows] = await Promise.all([
db.select().from(cases).where(where).orderBy(desc(cases.updatedAt)).limit(limit).offset(offset), db
.select({ caseRecord: cases, productName: products.name })
.from(cases)
.leftJoin(products, eq(cases.productId, products.id))
.where(where)
.orderBy(desc(cases.updatedAt))
.limit(limit)
.offset(offset),
db.select({ total: count() }).from(cases).where(where), db.select({ total: count() }).from(cases).where(where),
]) ])
const total = totalRows[0]?.total || 0 const total = totalRows[0]?.total || 0
...@@ -55,10 +83,37 @@ export const listCases = async (filters = {}, options = {}) => { ...@@ -55,10 +83,37 @@ export const listCases = async (filters = {}, options = {}) => {
} }
} }
export const syncCasesToPublic = async () => {
const rows = await db
.select({ caseRecord: cases, productName: products.name })
.from(cases)
.leftJoin(products, eq(cases.productId, products.id))
.orderBy(desc(cases.id))
const payload = rows.map((row) => toPublicCase(parseFiles({
...row.caseRecord,
productName: row.productName || '',
})))
const upload = await uploadJson(config.oss.caseJsonObjectKey, payload)
return {
count: payload.length,
...upload,
}
}
export const getCaseById = async (id) => { export const getCaseById = async (id) => {
const rows = await db.select().from(cases).where(eq(cases.id, Number(id))).limit(1) const rows = await db
.select({ caseRecord: cases, productName: products.name })
.from(cases)
.leftJoin(products, eq(cases.productId, products.id))
.where(eq(cases.id, Number(id)))
.limit(1)
return parseFiles(rows[0]) || null if (!rows[0]) return null
return parseFiles({
...rows[0].caseRecord,
productName: rows[0].productName || '',
})
} }
export const createCase = async (payload = {}, currentUser = {}) => { export const createCase = async (payload = {}, currentUser = {}) => {
......
...@@ -59,6 +59,7 @@ router.use('/dms/products', requireRouteAccess('/dms/products'), productsManageR ...@@ -59,6 +59,7 @@ router.use('/dms/products', requireRouteAccess('/dms/products'), productsManageR
// ── cases ── // ── cases ──
const casesRouter = Router() const casesRouter = Router()
casesRouter.get('/', validate({ query: casesListQuery }), casesController.list) casesRouter.get('/', validate({ query: casesListQuery }), casesController.list)
casesRouter.post('/sync', casesController.sync)
casesRouter.post('/', validate({ body: caseCreateBody }), casesController.create) casesRouter.post('/', validate({ body: caseCreateBody }), casesController.create)
casesRouter.get('/:id', validate({ params: idParam }), casesController.detail) casesRouter.get('/:id', validate({ params: idParam }), casesController.detail)
casesRouter.put('/:id', validate({ params: idParam, body: caseUpdateBody }), casesController.update) casesRouter.put('/:id', validate({ params: idParam, body: caseUpdateBody }), casesController.update)
......
...@@ -155,7 +155,7 @@ const getProjectByCodeWithDb = async (db, projectCode) => { ...@@ -155,7 +155,7 @@ const getProjectByCodeWithDb = async (db, projectCode) => {
return { return {
...rows[0].project, ...rows[0].project,
productName: rows[0].productName || rows[0].project.productName, productName: rows[0].productName || '',
} }
} }
...@@ -341,7 +341,7 @@ export const listProjects = async (filters = {}, options = {}, currentUser = {}) ...@@ -341,7 +341,7 @@ export const listProjects = async (filters = {}, options = {}, currentUser = {})
&& [PROJECT_STAGES.SOLUTION, PROJECT_STAGES.INITIATION].includes(row.project.stage) && [PROJECT_STAGES.SOLUTION, PROJECT_STAGES.INITIATION].includes(row.project.stage)
&& assignmentsByProjectId.get(row.project.id)?.some((item) => item.role === 'project_manager' && item.ssoId === currentUser.userId) && assignmentsByProjectId.get(row.project.id)?.some((item) => item.role === 'project_manager' && item.ssoId === currentUser.userId)
), ),
productName: row.productName || row.project.productName, productName: row.productName || '',
})) }))
const total = totalRows[0]?.total || 0 const total = totalRows[0]?.total || 0
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论