466 lines
19 KiB
TypeScript
466 lines
19 KiB
TypeScript
import { Request, Response } from 'express'
|
||
import pool from '../utils/db'
|
||
import { RowDataPacket, ResultSetHeader } from 'mysql2'
|
||
import { syncProductMinPrice } from '../utils/syncPrice'
|
||
|
||
// 所有字段(用于数据库读写)
|
||
const ALL_DB_HEADERS = [
|
||
'model_name', 'barcode', 'fineness', 'main_stone', 'sub_stone', 'ring_size',
|
||
'gold_total_weight', 'gold_net_weight', 'loss', 'gold_loss',
|
||
'gold_price', 'gold_value',
|
||
'main_stone_count', 'main_stone_weight', 'main_stone_unit_price', 'main_stone_amount',
|
||
'side_stone_count', 'side_stone_weight', 'side_stone_unit_price', 'side_stone_amount',
|
||
'accessory_amount', 'processing_fee', 'setting_fee', 'total_labor_cost', 'total_price',
|
||
]
|
||
|
||
// CSV 手动输入列(不含自动计算字段)
|
||
const CSV_INPUT_HEADERS = [
|
||
'style_no',
|
||
'model_name', 'barcode', 'fineness', 'main_stone', 'sub_stone', 'ring_size',
|
||
'gold_total_weight', 'loss',
|
||
'main_stone_count', 'main_stone_weight', 'main_stone_unit_price',
|
||
'side_stone_count', 'side_stone_weight', 'side_stone_unit_price',
|
||
'accessory_amount', 'processing_fee', 'setting_fee',
|
||
]
|
||
|
||
// 全部中文表头映射
|
||
const CSV_HEADERS_CN: Record<string, string> = {
|
||
style_no: '款号',
|
||
model_name: '规格名称', barcode: '条型码', fineness: '成色', main_stone: '主石', sub_stone: '副石', ring_size: '手寸',
|
||
gold_total_weight: '金料总重', gold_net_weight: '金料净重', loss: '损耗', gold_loss: '金损',
|
||
gold_price: '金价', gold_value: '金料价值',
|
||
main_stone_count: '主石数', main_stone_weight: '主石重', main_stone_unit_price: '主石单价', main_stone_amount: '主石金额',
|
||
side_stone_count: '副石数', side_stone_weight: '副石重', side_stone_unit_price: '副石单价', side_stone_amount: '副石金额',
|
||
accessory_amount: '配件费', processing_fee: '工费', setting_fee: '镶石费', total_labor_cost: '工费合计', total_price: '总价',
|
||
}
|
||
|
||
// 中文→英文反向映射(导入用)
|
||
const CN_TO_EN: Record<string, string> = Object.fromEntries(
|
||
Object.entries(CSV_HEADERS_CN).map(([en, cn]) => [cn, en])
|
||
)
|
||
|
||
// 根据手动输入字段自动计算派生字段
|
||
function calcSpecFields(row: Record<string, number>) {
|
||
const n = (v: any) => Number(v) || 0
|
||
const goldNetWeight = +(n(row.gold_total_weight) - n(row.main_stone_weight) * 0.2 - n(row.side_stone_weight) * 0.2).toFixed(4)
|
||
const safeGoldNetWeight = goldNetWeight < 0 ? 0 : goldNetWeight
|
||
const goldLoss = +(safeGoldNetWeight * n(row.loss)).toFixed(4)
|
||
const goldValue = +(goldLoss * n(row.gold_price)).toFixed(2)
|
||
const mainStoneAmount = +(n(row.main_stone_weight) * n(row.main_stone_unit_price)).toFixed(2)
|
||
const sideStoneAmount = +(n(row.side_stone_weight) * n(row.side_stone_unit_price)).toFixed(2)
|
||
const totalLaborCost = +(n(row.accessory_amount) + n(row.processing_fee) + n(row.setting_fee)).toFixed(2)
|
||
const totalPrice = +(goldValue + mainStoneAmount + sideStoneAmount + totalLaborCost).toFixed(2)
|
||
return {
|
||
gold_net_weight: safeGoldNetWeight,
|
||
gold_loss: goldLoss,
|
||
gold_value: goldValue,
|
||
main_stone_amount: mainStoneAmount,
|
||
side_stone_amount: sideStoneAmount,
|
||
total_labor_cost: totalLaborCost,
|
||
total_price: totalPrice,
|
||
}
|
||
}
|
||
|
||
function escapeCSVField(value: string | number): string {
|
||
const str = String(value)
|
||
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
||
return `"${str.replace(/"/g, '""')}"`
|
||
}
|
||
return str
|
||
}
|
||
|
||
export function parseCSV(content: string): Record<string, string>[] {
|
||
const lines = content.split(/\r?\n/).filter((l) => l.trim())
|
||
if (lines.length < 2) return []
|
||
|
||
const rawHeaders = parseCSVLine(lines[0])
|
||
// 支持中文表头:自动转为英文字段名
|
||
const headers = rawHeaders.map((h) => {
|
||
const trimmed = h.trim()
|
||
return CN_TO_EN[trimmed] || trimmed
|
||
})
|
||
const rows: Record<string, string>[] = []
|
||
|
||
for (let i = 1; i < lines.length; i++) {
|
||
const values = parseCSVLine(lines[i])
|
||
const row: Record<string, string> = {}
|
||
headers.forEach((h, idx) => {
|
||
row[h] = (values[idx] || '').trim()
|
||
})
|
||
rows.push(row)
|
||
}
|
||
return rows
|
||
}
|
||
|
||
function parseCSVLine(line: string): string[] {
|
||
const result: string[] = []
|
||
let current = ''
|
||
let inQuotes = false
|
||
|
||
for (let i = 0; i < line.length; i++) {
|
||
const ch = line[i]
|
||
if (inQuotes) {
|
||
if (ch === '"' && line[i + 1] === '"') {
|
||
current += '"'
|
||
i++
|
||
} else if (ch === '"') {
|
||
inQuotes = false
|
||
} else {
|
||
current += ch
|
||
}
|
||
} else {
|
||
if (ch === '"') {
|
||
inQuotes = true
|
||
} else if (ch === ',') {
|
||
result.push(current)
|
||
current = ''
|
||
} else {
|
||
current += ch
|
||
}
|
||
}
|
||
}
|
||
result.push(current)
|
||
return result
|
||
}
|
||
|
||
export function generateCSV(rows: Record<string, any>[], headers: string[] = CSV_INPUT_HEADERS): string {
|
||
const headerLine = headers.map((h) => escapeCSVField(CSV_HEADERS_CN[h] || h)).join(',')
|
||
const dataLines = rows.map((row) =>
|
||
headers.map((h) => escapeCSVField(row[h] ?? '')).join(',')
|
||
)
|
||
return [headerLine, ...dataLines].join('\n')
|
||
}
|
||
|
||
// GET /api/admin/products/:id/spec-data - 获取规格数据列表
|
||
export async function adminGetSpecData(req: Request, res: Response): Promise<void> {
|
||
try {
|
||
const { id } = req.params
|
||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||
`SELECT id, product_id AS productId, model_name AS modelName, barcode, fineness, main_stone AS mainStone,
|
||
sub_stone AS subStone, ring_size AS ringSize, gold_total_weight AS goldTotalWeight, gold_net_weight AS goldNetWeight,
|
||
loss, gold_loss AS goldLoss, gold_price AS goldPrice, gold_value AS goldValue,
|
||
main_stone_count AS mainStoneCount, main_stone_weight AS mainStoneWeight,
|
||
main_stone_unit_price AS mainStoneUnitPrice, main_stone_amount AS mainStoneAmount,
|
||
side_stone_count AS sideStoneCount, side_stone_weight AS sideStoneWeight,
|
||
side_stone_unit_price AS sideStoneUnitPrice, side_stone_amount AS sideStoneAmount,
|
||
accessory_amount AS accessoryAmount, processing_fee AS processingFee,
|
||
setting_fee AS settingFee, total_labor_cost AS totalLaborCost, total_price AS totalPrice
|
||
FROM spec_data WHERE product_id = ? ORDER BY id ASC`,
|
||
[id]
|
||
)
|
||
res.json({ code: 0, data: rows })
|
||
} catch (err) {
|
||
console.error('adminGetSpecData error:', err)
|
||
res.status(500).json({ code: 500, message: '获取规格数据失败' })
|
||
}
|
||
}
|
||
|
||
// POST /api/admin/products/:id/spec-data - 新增单条规格数据
|
||
export async function adminCreateSpecData(req: Request, res: Response): Promise<void> {
|
||
try {
|
||
const { id } = req.params
|
||
const d = req.body
|
||
if (d.barcode) {
|
||
const [dup] = await pool.execute<RowDataPacket[]>('SELECT id FROM spec_data WHERE barcode = ?', [d.barcode])
|
||
if (dup.length > 0) { res.status(400).json({ code: 400, message: `条型码 "${d.barcode}" 已存在` }); return }
|
||
}
|
||
const [result] = await pool.execute<ResultSetHeader>(
|
||
`INSERT INTO spec_data (product_id, model_name, barcode, fineness, main_stone, sub_stone, ring_size,
|
||
gold_total_weight, gold_net_weight, loss, gold_loss, gold_price, gold_value,
|
||
main_stone_count, main_stone_weight, main_stone_unit_price, main_stone_amount,
|
||
side_stone_count, side_stone_weight, side_stone_unit_price, side_stone_amount,
|
||
accessory_amount, processing_fee, setting_fee, total_labor_cost, total_price)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||
[id, d.modelName||'', d.barcode||null, d.fineness||'', d.mainStone||'', d.subStone||'', d.ringSize||'',
|
||
d.goldTotalWeight||0, d.goldNetWeight||0, d.loss||0, d.goldLoss||0,
|
||
d.goldPrice||0, d.goldValue||0,
|
||
d.mainStoneCount||0, d.mainStoneWeight||0, d.mainStoneUnitPrice||0, d.mainStoneAmount||0,
|
||
d.sideStoneCount||0, d.sideStoneWeight||0, d.sideStoneUnitPrice||0, d.sideStoneAmount||0,
|
||
d.accessoryAmount||0, d.processingFee||0, d.settingFee||0, d.totalLaborCost||0, d.totalPrice||0]
|
||
)
|
||
await syncProductMinPrice(Number(id))
|
||
res.json({ code: 0, data: { id: result.insertId } })
|
||
} catch (err) {
|
||
console.error('adminCreateSpecData error:', err)
|
||
res.status(500).json({ code: 500, message: '新增规格数据失败' })
|
||
}
|
||
}
|
||
|
||
// DELETE /api/admin/products/:productId/spec-data/:specId
|
||
export async function adminDeleteSpecData(req: Request, res: Response): Promise<void> {
|
||
try {
|
||
const { productId, specId } = req.params
|
||
await pool.execute('DELETE FROM spec_data WHERE id = ?', [specId])
|
||
await syncProductMinPrice(Number(productId))
|
||
res.json({ code: 0, message: '删除成功' })
|
||
} catch (err) {
|
||
console.error('adminDeleteSpecData error:', err)
|
||
res.status(500).json({ code: 500, message: '删除规格数据失败' })
|
||
}
|
||
}
|
||
|
||
// PUT /api/admin/products/:productId/spec-data/:specId - 编辑规格数据
|
||
export async function adminUpdateSpecData(req: Request, res: Response): Promise<void> {
|
||
try {
|
||
const { productId, specId } = req.params
|
||
const d = req.body
|
||
if (d.barcode) {
|
||
const [dup] = await pool.execute<RowDataPacket[]>('SELECT id FROM spec_data WHERE barcode = ? AND id != ?', [d.barcode, specId])
|
||
if (dup.length > 0) { res.status(400).json({ code: 400, message: `条型码 "${d.barcode}" 已存在` }); return }
|
||
}
|
||
await pool.execute(
|
||
`UPDATE spec_data SET model_name=?, barcode=?, fineness=?, main_stone=?, sub_stone=?, ring_size=?,
|
||
gold_total_weight=?, gold_net_weight=?, loss=?, gold_loss=?, gold_price=?, gold_value=?,
|
||
main_stone_count=?, main_stone_weight=?, main_stone_unit_price=?, main_stone_amount=?,
|
||
side_stone_count=?, side_stone_weight=?, side_stone_unit_price=?, side_stone_amount=?,
|
||
accessory_amount=?, processing_fee=?, setting_fee=?, total_labor_cost=?, total_price=?
|
||
WHERE id=?`,
|
||
[d.modelName||'', d.barcode||null, d.fineness||'', d.mainStone||'', d.subStone||'', d.ringSize||'',
|
||
d.goldTotalWeight||0, d.goldNetWeight||0, d.loss||0, d.goldLoss||0,
|
||
d.goldPrice||0, d.goldValue||0,
|
||
d.mainStoneCount||0, d.mainStoneWeight||0, d.mainStoneUnitPrice||0, d.mainStoneAmount||0,
|
||
d.sideStoneCount||0, d.sideStoneWeight||0, d.sideStoneUnitPrice||0, d.sideStoneAmount||0,
|
||
d.accessoryAmount||0, d.processingFee||0, d.settingFee||0, d.totalLaborCost||0, d.totalPrice||0,
|
||
specId]
|
||
)
|
||
await syncProductMinPrice(Number(productId))
|
||
res.json({ code: 0, message: '更新成功' })
|
||
} catch (err) {
|
||
console.error('adminUpdateSpecData error:', err)
|
||
res.status(500).json({ code: 500, message: '更新规格数据失败' })
|
||
}
|
||
}
|
||
|
||
// GET /api/admin/products/:id/spec-data/export
|
||
export async function exportSpecData(req: Request, res: Response): Promise<void> {
|
||
try {
|
||
const { id } = req.params
|
||
const [productRows] = await pool.execute<RowDataPacket[]>(
|
||
'SELECT style_no FROM products WHERE id = ?', [id]
|
||
)
|
||
const styleNo = productRows.length > 0 ? productRows[0].style_no : ''
|
||
|
||
const inputDbCols = CSV_INPUT_HEADERS.filter(h => h !== 'style_no')
|
||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||
`SELECT ${inputDbCols.join(', ')} FROM spec_data WHERE product_id = ?`,
|
||
[id]
|
||
)
|
||
const rowsWithStyleNo = rows.map(r => ({ style_no: styleNo, ...r }))
|
||
|
||
const csv = generateCSV(rowsWithStyleNo, CSV_INPUT_HEADERS)
|
||
res.setHeader('Content-Type', 'text/csv; charset=utf-8')
|
||
res.setHeader('Content-Disposition', `attachment; filename=spec_data_${id}.csv`)
|
||
res.send('\uFEFF' + csv)
|
||
} catch (err) {
|
||
console.error('exportSpecData error:', err)
|
||
res.status(500).json({ code: 500, message: '导出规格数据失败' })
|
||
}
|
||
}
|
||
|
||
// POST /api/admin/spec-data/lookup - 批量条形码查询商品+规格信息
|
||
export async function lookupByBarcodes(req: Request, res: Response): Promise<void> {
|
||
try {
|
||
const { barcodes } = req.body
|
||
if (!barcodes || !Array.isArray(barcodes) || barcodes.length === 0) {
|
||
res.status(400).json({ code: 400, message: '请提供条形码列表' })
|
||
return
|
||
}
|
||
|
||
const uniqueBarcodes = [...new Set(barcodes.map((b: string) => b.trim()).filter(Boolean))]
|
||
if (uniqueBarcodes.length === 0) {
|
||
res.status(400).json({ code: 400, message: '条形码列表为空' })
|
||
return
|
||
}
|
||
|
||
const placeholders = uniqueBarcodes.map(() => '?').join(',')
|
||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||
`SELECT sd.id AS specDataId, sd.product_id AS productId, sd.barcode,
|
||
sd.model_name AS modelName, sd.fineness, sd.main_stone AS mainStone,
|
||
sd.sub_stone AS subStone, sd.ring_size AS ringSize,
|
||
sd.gold_total_weight AS goldTotalWeight, sd.gold_net_weight AS goldNetWeight,
|
||
sd.loss, sd.gold_loss AS goldLoss, sd.gold_price AS goldPrice, sd.gold_value AS goldValue,
|
||
sd.main_stone_count AS mainStoneCount, sd.main_stone_weight AS mainStoneWeight,
|
||
sd.main_stone_unit_price AS mainStoneUnitPrice, sd.main_stone_amount AS mainStoneAmount,
|
||
sd.side_stone_count AS sideStoneCount, sd.side_stone_weight AS sideStoneWeight,
|
||
sd.side_stone_unit_price AS sideStoneUnitPrice, sd.side_stone_amount AS sideStoneAmount,
|
||
sd.accessory_amount AS accessoryAmount, sd.processing_fee AS processingFee,
|
||
sd.setting_fee AS settingFee, sd.total_labor_cost AS totalLaborCost,
|
||
sd.total_price AS totalPrice,
|
||
p.name AS productName, p.style_no AS styleNo, p.thumb
|
||
FROM spec_data sd
|
||
LEFT JOIN products p ON sd.product_id = p.id
|
||
WHERE sd.barcode IN (${placeholders})`,
|
||
uniqueBarcodes
|
||
)
|
||
|
||
const itemsMap: Record<string, any> = {}
|
||
for (const row of rows) {
|
||
itemsMap[row.barcode] = row
|
||
}
|
||
|
||
const notFound = uniqueBarcodes.filter(b => !itemsMap[b])
|
||
|
||
res.json({ code: 0, data: { items: itemsMap, notFound } })
|
||
} catch (err) {
|
||
console.error('lookupByBarcodes error:', err)
|
||
res.status(500).json({ code: 500, message: '条形码查询失败' })
|
||
}
|
||
}
|
||
|
||
// POST /api/admin/products/:id/spec-data/import
|
||
export async function importSpecData(req: Request, res: Response): Promise<void> {
|
||
const conn = await pool.getConnection()
|
||
try {
|
||
if (!req.file) {
|
||
res.status(400).json({ code: 400, message: '请上传 CSV 文件' })
|
||
return
|
||
}
|
||
|
||
let content = req.file.buffer.toString('utf-8')
|
||
if (content.charCodeAt(0) === 0xFEFF) {
|
||
content = content.slice(1)
|
||
}
|
||
|
||
if (content.includes('<27>') || content.includes('\ufffd')) {
|
||
try {
|
||
const iconv = require('iconv-lite')
|
||
content = iconv.decode(req.file.buffer, 'gbk')
|
||
if (content.charCodeAt(0) === 0xFEFF) {
|
||
content = content.slice(1)
|
||
}
|
||
} catch { /* fallback utf-8 */ }
|
||
}
|
||
|
||
const rows = parseCSV(content)
|
||
if (rows.length === 0) {
|
||
res.status(400).json({ code: 400, message: 'CSV 文件为空或格式错误' })
|
||
return
|
||
}
|
||
|
||
// 校验必填的手动输入列
|
||
const firstRow = rows[0]
|
||
const requiredHeaders = CSV_INPUT_HEADERS
|
||
const missingHeaders = requiredHeaders.filter((h) => !(h in firstRow))
|
||
if (missingHeaders.length > 0) {
|
||
const missingCN = missingHeaders.map(h => CSV_HEADERS_CN[h] || h).join(', ')
|
||
res.status(400).json({ code: 400, message: `缺少必填列: ${missingCN}。请确保CSV表头包含:款号、规格名称、条型码等,编码为UTF-8` })
|
||
return
|
||
}
|
||
|
||
// 获取最新金价和铂金价格
|
||
const [gpRows] = await pool.execute<RowDataPacket[]>(
|
||
'SELECT price FROM gold_price_logs ORDER BY id DESC LIMIT 1'
|
||
)
|
||
const goldPrice = gpRows.length > 0 ? Number(gpRows[0].price) : 0
|
||
const [ppRows] = await pool.execute<RowDataPacket[]>(
|
||
'SELECT price FROM platinum_price_logs ORDER BY id DESC LIMIT 1'
|
||
)
|
||
const platinumPrice = ppRows.length > 0 ? Number(ppRows[0].price) : 0
|
||
|
||
// 按款号分组,批量查询对应的 product_id
|
||
const styleNos = [...new Set(rows.map(r => (r.style_no || '').trim()).filter(Boolean))]
|
||
if (styleNos.length === 0) {
|
||
res.status(400).json({ code: 400, message: 'CSV 中未找到有效的"款号"列数据' })
|
||
return
|
||
}
|
||
|
||
const [productRows] = await pool.execute<RowDataPacket[]>(
|
||
`SELECT id, style_no FROM products WHERE style_no IN (${styleNos.map(() => '?').join(',')})`,
|
||
styleNos
|
||
)
|
||
const styleNoToProductId: Record<string, number> = {}
|
||
for (const p of productRows) {
|
||
styleNoToProductId[p.style_no] = p.id
|
||
}
|
||
|
||
const notFoundStyleNos = styleNos.filter(s => !styleNoToProductId[s])
|
||
if (notFoundStyleNos.length > 0) {
|
||
res.status(400).json({ code: 400, message: `以下款号未找到对应商品: ${notFoundStyleNos.join(', ')}` })
|
||
return
|
||
}
|
||
|
||
const textFields = ['model_name', 'barcode', 'fineness', 'main_stone', 'sub_stone', 'ring_size']
|
||
const numericInputFields = CSV_INPUT_HEADERS.filter(h => h !== 'style_no' && !textFields.includes(h))
|
||
const errors: string[] = []
|
||
let imported = 0
|
||
let skipped = 0
|
||
|
||
await conn.beginTransaction()
|
||
|
||
for (let i = 0; i < rows.length; i++) {
|
||
const row = rows[i]
|
||
const styleNo = (row.style_no || '').trim()
|
||
const productId = styleNoToProductId[styleNo]
|
||
if (!productId) {
|
||
errors.push(`第 ${i + 2} 行:款号 "${styleNo}" 未找到对应商品,已跳过`)
|
||
skipped++
|
||
continue
|
||
}
|
||
|
||
// 条型码去重:如果已存在则跳过
|
||
const barcode = (row.barcode || '').trim()
|
||
if (barcode) {
|
||
const [dup] = await conn.execute<RowDataPacket[]>(
|
||
'SELECT id FROM spec_data WHERE barcode = ?', [barcode]
|
||
)
|
||
if (dup.length > 0) {
|
||
errors.push(`第 ${i + 2} 行:条型码 "${barcode}" 已存在,已跳过`)
|
||
skipped++
|
||
continue
|
||
}
|
||
}
|
||
|
||
// 根据成色决定使用金价还是铂金价格
|
||
const fineness = (row.fineness || '').trim()
|
||
const rowPrice = fineness === '铂金PT950' ? platinumPrice : goldPrice
|
||
|
||
// 解析手动输入的数值字段
|
||
const numericVals: Record<string, number> = { gold_price: rowPrice }
|
||
for (const h of numericInputFields) {
|
||
const num = Number(row[h])
|
||
if (row[h] !== '' && isNaN(num)) {
|
||
errors.push(`第 ${i + 2} 行 "${CSV_HEADERS_CN[h] || h}" 列不是有效数字`)
|
||
}
|
||
numericVals[h] = isNaN(num) ? 0 : num
|
||
}
|
||
|
||
// 自动计算派生字段
|
||
const calc = calcSpecFields(numericVals)
|
||
|
||
const insertCols = ALL_DB_HEADERS
|
||
const insertVals = insertCols.map(h => {
|
||
if (textFields.includes(h)) return (row[h] || '').trim()
|
||
if (h in calc) return (calc as any)[h]
|
||
if (h === 'gold_price') return rowPrice
|
||
return numericVals[h] ?? 0
|
||
})
|
||
|
||
await conn.execute(
|
||
`INSERT INTO spec_data (product_id, ${insertCols.join(', ')})
|
||
VALUES (?, ${insertCols.map(() => '?').join(', ')})`,
|
||
[productId, ...insertVals]
|
||
)
|
||
imported++
|
||
}
|
||
|
||
await conn.commit()
|
||
|
||
// 同步所有涉及商品的最低价
|
||
const affectedProductIds = [...new Set(Object.values(styleNoToProductId))]
|
||
for (const pid of affectedProductIds) {
|
||
await syncProductMinPrice(pid)
|
||
}
|
||
|
||
res.json({
|
||
code: 0,
|
||
data: { imported, skipped, warnings: errors.length > 0 ? errors : undefined },
|
||
})
|
||
} catch (err) {
|
||
await conn.rollback()
|
||
console.error('importSpecData error:', err)
|
||
res.status(500).json({ code: 500, message: '导入规格数据失败' })
|
||
} finally {
|
||
conn.release()
|
||
}
|
||
}
|