203 lines
4.9 KiB
TypeScript
203 lines
4.9 KiB
TypeScript
/**
|
||
* 清理 UploadField.optionsStatic 中历史遗留的「数组字符串」脏数据
|
||
*
|
||
* 运行方式:
|
||
* npx ts-node scripts/cleanup-category-options.ts
|
||
*
|
||
* 会遍历所有存在 optionsStatic 的字段,将形如 "[默认店铺]"、
|
||
* "[\"默认店铺\"]"、"[阿猫,阿狗]" 等非规范字符串转换为
|
||
* 纯字符串数组(JSON),并自动去重、去空。
|
||
*/
|
||
import { PrismaClient } from '@prisma/client'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
function isArrayString(str: string): boolean {
|
||
const trimmed = str.trim()
|
||
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
|
||
return false
|
||
}
|
||
try {
|
||
const parsed = JSON.parse(trimmed)
|
||
return Array.isArray(parsed)
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
function normalizeOptionValue(option: string): string {
|
||
let cleaned = option.trim()
|
||
if (!cleaned) return ''
|
||
|
||
try {
|
||
if (
|
||
(cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
||
(cleaned.startsWith("'") && cleaned.endsWith("'"))
|
||
) {
|
||
const parsed = JSON.parse(cleaned)
|
||
if (typeof parsed === 'string') {
|
||
cleaned = parsed.trim()
|
||
}
|
||
}
|
||
} catch {
|
||
// ignore parse errors
|
||
}
|
||
|
||
cleaned = cleaned.replace(/\\+"/g, '"').replace(/\\'/g, "'")
|
||
cleaned = cleaned.replace(/["'`“”]/g, '')
|
||
cleaned = cleaned.trim()
|
||
if (cleaned.startsWith('\\') && cleaned.endsWith('\\')) {
|
||
cleaned = cleaned.slice(1, -1).trim()
|
||
}
|
||
return cleaned.trim()
|
||
}
|
||
|
||
function stripLooseBrackets(value: string): string {
|
||
let result = value.trim()
|
||
let guard = 0
|
||
while (result.startsWith('[') && result.endsWith(']') && guard < 5) {
|
||
const inner = result.slice(1, -1).trim()
|
||
if (!inner || inner === result) {
|
||
break
|
||
}
|
||
result = inner
|
||
guard += 1
|
||
}
|
||
return result
|
||
}
|
||
|
||
function extractLooseArrayOptions(option: string): string[] | null {
|
||
let trimmed = option.trim()
|
||
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
|
||
return null
|
||
}
|
||
|
||
let inner = trimmed
|
||
let safeGuard = 0
|
||
while (inner.startsWith('[') && inner.endsWith(']') && safeGuard < 5) {
|
||
const next = inner.slice(1, -1).trim()
|
||
if (!next || next === inner) {
|
||
break
|
||
}
|
||
inner = next
|
||
safeGuard += 1
|
||
}
|
||
|
||
if (!inner) return []
|
||
|
||
const segments = inner.split(/[,,]/).map(seg => normalizeOptionValue(seg)).filter(Boolean)
|
||
if (segments.length > 1) {
|
||
return segments
|
||
}
|
||
|
||
const single = normalizeOptionValue(inner)
|
||
return single ? [single] : null
|
||
}
|
||
|
||
function sanitizeOptions(optionsStatic: string | null): string[] {
|
||
if (!optionsStatic) return []
|
||
|
||
const pushOption = (target: Set<string>, value: string) => {
|
||
const normalized = stripLooseBrackets(normalizeOptionValue(value))
|
||
if (normalized && !isArrayString(normalized)) {
|
||
target.add(normalized)
|
||
}
|
||
}
|
||
|
||
const result = new Set<string>()
|
||
|
||
try {
|
||
const parsed = JSON.parse(optionsStatic)
|
||
if (Array.isArray(parsed)) {
|
||
parsed.forEach(opt => {
|
||
const optStr = String(opt).trim()
|
||
if (!optStr) return
|
||
|
||
const loose = extractLooseArrayOptions(optStr)
|
||
if (loose && loose.length) {
|
||
loose.forEach(item => pushOption(result, item))
|
||
return
|
||
}
|
||
|
||
if (isArrayString(optStr)) {
|
||
try {
|
||
const nested = JSON.parse(optStr)
|
||
if (Array.isArray(nested)) {
|
||
nested.forEach(item => pushOption(result, String(item)))
|
||
return
|
||
}
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
pushOption(result, optStr)
|
||
})
|
||
return Array.from(result)
|
||
}
|
||
} catch {
|
||
// 非 JSON 字符串,走 fallback
|
||
}
|
||
|
||
optionsStatic
|
||
.split('\n')
|
||
.map(line => line.trim())
|
||
.filter(Boolean)
|
||
.forEach(line => {
|
||
const loose = extractLooseArrayOptions(line)
|
||
if (loose && loose.length) {
|
||
loose.forEach(item => pushOption(result, item))
|
||
} else {
|
||
pushOption(result, line)
|
||
}
|
||
})
|
||
|
||
return Array.from(result)
|
||
}
|
||
|
||
async function main() {
|
||
const fields = await prisma.uploadField.findMany({
|
||
where: {
|
||
optionsStatic: {
|
||
not: null,
|
||
},
|
||
},
|
||
select: {
|
||
id: true,
|
||
key: true,
|
||
label: true,
|
||
optionsStatic: true,
|
||
},
|
||
})
|
||
|
||
let updated = 0
|
||
for (const field of fields) {
|
||
const sanitized = sanitizeOptions(field.optionsStatic)
|
||
const sanitizedJson = JSON.stringify(sanitized)
|
||
const currentTrimmed = (field.optionsStatic ?? '').trim()
|
||
|
||
if (sanitizedJson && sanitizedJson !== currentTrimmed) {
|
||
await prisma.uploadField.update({
|
||
where: { id: field.id },
|
||
data: { optionsStatic: sanitizedJson },
|
||
})
|
||
updated += 1
|
||
console.log(
|
||
`[Cleanup] Updated field ${field.key} (${field.label}) -> ${sanitizedJson}`
|
||
)
|
||
}
|
||
}
|
||
|
||
console.log(`Cleanup finished. Updated ${updated} field(s).`)
|
||
}
|
||
|
||
main()
|
||
.catch((error) => {
|
||
console.error('Cleanup failed:', error)
|
||
process.exitCode = 1
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect()
|
||
})
|
||
|