const Base = require('../base'); module.exports = class extends Base { async indexAction() { this.assign('currentPage', 'hospital'); this.assign('pageTitle', '医院管理'); this.assign('breadcrumb', [{ name: '医院管理' }]); return this.display(); } // 列表(分页) async listAction() { const keyword = this.get('keyword') || ''; const provinceCode = this.get('province_code') || ''; const cityCode = this.get('city_code') || ''; const districtCode = this.get('district_code') || ''; const page = this.get('page') || 1; const pageSize = this.get('pageSize') || 10; const where = { is_deleted: 0 }; if (keyword) where.name = ['like', `%${keyword}%`]; if (provinceCode) where.province_code = provinceCode; if (cityCode) where.city_code = cityCode; if (districtCode) where.district_code = districtCode; const data = await this.model('hospital') .where(where) .order('sort DESC, id ASC') .page(page, pageSize) .countSelect(); return this.json({ code: 0, data }); } // 新增 async addAction() { const { name, province_code, city_code, district_code, sort, is_show } = this.post(); if (!name || !name.trim()) return this.fail('请输入医院名称'); const exists = await this.model('hospital').where({ name: name.trim(), is_deleted: 0 }).find(); if (!think.isEmpty(exists)) return this.fail('该医院已存在'); const regionNames = await this._getRegionNames(province_code, city_code, district_code); await this.model('hospital').add({ name: name.trim(), province_code: province_code || '', city_code: city_code || '', district_code: district_code || '', province_name: regionNames.province, city_name: regionNames.city, district_name: regionNames.district, sort: sort || 0, is_show: is_show === undefined ? 1 : (is_show ? 1 : 0) }); await this.model('hospital').clearTreeCache(); await this.log('add', '医院管理', `新增医院「${name.trim()}」`); return this.success(); } // 编辑 async editAction() { const { id, name, province_code, city_code, district_code, sort, is_show } = this.post(); if (!id) return this.fail('参数错误'); if (!name || !name.trim()) return this.fail('请输入医院名称'); const exists = await this.model('hospital').where({ name: name.trim(), id: ['!=', id], is_deleted: 0 }).find(); if (!think.isEmpty(exists)) return this.fail('该医院已存在'); const regionNames = await this._getRegionNames(province_code, city_code, district_code); await this.model('hospital').where({ id }).update({ name: name.trim(), province_code: province_code || '', city_code: city_code || '', district_code: district_code || '', province_name: regionNames.province, city_name: regionNames.city, district_name: regionNames.district, sort: sort || 0, is_show: is_show ? 1 : 0 }); await this.model('hospital').clearTreeCache(); await this.log('edit', '医院管理', `编辑医院(ID:${id})「${name.trim()}」`); return this.success(); } // 删除 async deleteAction() { const { id } = this.post(); if (!id) return this.fail('参数错误'); await this.model('hospital').where({ id }).update({ is_deleted: 1 }); await this.model('hospital').clearTreeCache(); await this.log('delete', '医院管理', `删除医院(ID:${id})`); return this.success(); } // 切换展示状态 async toggleShowAction() { const { id, is_show } = this.post(); if (!id) return this.fail('参数错误'); await this.model('hospital').where({ id }).update({ is_show: is_show ? 1 : 0 }); await this.model('hospital').clearTreeCache(); await this.log('edit', '医院管理', `${is_show ? '展示' : '隐藏'}医院(ID:${id})`); return this.success(); } // 排序 async sortAction() { const { ids } = this.post(); if (!ids || !ids.length) return this.fail('参数错误'); for (let i = 0; i < ids.length; i++) { await this.model('hospital').where({ id: ids[i] }).update({ sort: ids.length - i }); } await this.model('hospital').clearTreeCache(); return this.success(); } // Excel 导入(表头:关联机构名称/省份/城市/区县) async importAction() { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); let file = this.file('file'); if (!file || !file.path) { await sleep(1000); file = this.file('file'); } if (!file || !file.path) return this.fail('请上传 Excel 文件'); const ExcelJS = require('exceljs'); const workbook = new ExcelJS.Workbook(); try { await workbook.xlsx.readFile(file.path); } catch (e) { return this.fail('文件解析失败,请上传有效的 Excel 文件'); } const sheet = workbook.worksheets[0]; if (!sheet) return this.fail('Excel 内容为空'); // 读取数据行(跳过表头) const rows = []; sheet.eachRow((row, rowNumber) => { if (rowNumber === 1) return; // 表头 const name = this._cellText(row.getCell(1)); const province = this._cellText(row.getCell(2)); const city = this._cellText(row.getCell(3)); const district = this._cellText(row.getCell(4)); if (name) rows.push({ name, province, city, district }); }); if (!rows.length) return this.fail('没有有效的数据行'); // 预加载全部地区,构建名称索引 const regions = await this.model('sys_region').where({ status: 1 }).select(); const regionIndex = this._buildRegionIndex(regions); // 已存在的医院名称 const existRows = await this.model('hospital').where({ is_deleted: 0 }).field('name').select(); const existNames = new Set(existRows.map(r => r.name)); const toInsert = []; const seen = new Set(); const unmatched = []; // 地区未匹配的医院名 let skipped = 0; rows.forEach(r => { if (existNames.has(r.name) || seen.has(r.name)) { skipped++; return; } seen.add(r.name); const codes = this._matchRegion(regionIndex, r.province, r.city, r.district); if ((r.province && !codes.province_code) || (r.city && !codes.city_code) || (r.district && !codes.district_code)) { unmatched.push(r.name); } toInsert.push({ name: r.name, province_code: codes.province_code, city_code: codes.city_code, district_code: codes.district_code, province_name: r.province || '', city_name: r.city || '', district_name: r.district || '', sort: 0, is_show: 1 }); }); if (!toInsert.length) return this.fail('没有新增医院(均已存在)'); // 分批插入 const batchSize = 200; for (let i = 0; i < toInsert.length; i += batchSize) { await this.model('hospital').addMany(toInsert.slice(i, i + batchSize)); } await this.model('hospital').clearTreeCache(); await this.log('import', '医院管理', `导入医院 ${toInsert.length} 条`); return this.success({ count: toInsert.length, skipped, unmatchedCount: unmatched.length, unmatched: unmatched.slice(0, 50) }); } // @private 读取单元格文本 _cellText(cell) { if (!cell || cell.value === null || cell.value === undefined) return ''; const v = cell.value; if (typeof v === 'object') { if (v.text) return String(v.text).trim(); if (v.result !== undefined) return String(v.result).trim(); if (v.richText) return v.richText.map(t => t.text).join('').trim(); return ''; } return String(v).trim(); } // @private 构建地区名称索引:{ provinceName: {code, cities: { cityName: {code, districts: { distName: code }}}}} _buildRegionIndex(regions) { const provinces = {}; const cityByParent = {}; const distByParent = {}; regions.forEach(r => { const code = r.code; if (code.endsWith('0000')) { provinces[r.name] = { code, cities: {} }; } else if (code.endsWith('00')) { const pCode = code.substring(0, 2) + '0000'; if (!cityByParent[pCode]) cityByParent[pCode] = []; cityByParent[pCode].push({ code, name: r.name }); } else { const cCode = code.substring(0, 4) + '00'; if (!distByParent[cCode]) distByParent[cCode] = []; distByParent[cCode].push({ code, name: r.name }); } }); // 组装 Object.values(provinces).forEach(prov => { const cities = cityByParent[prov.code] || []; cities.forEach(c => { prov.cities[c.name] = { code: c.code, districts: {} }; const dists = distByParent[c.code] || []; dists.forEach(d => { prov.cities[c.name].districts[d.name] = d.code; }); }); }); return provinces; } // @private 名称匹配编码(容错:直辖市省市同名、模糊包含) _matchRegion(index, provinceName, cityName, districtName) { const result = { province_code: '', city_code: '', district_code: '' }; if (!provinceName) return result; // 匹配省(容错:去掉"省/市/自治区"等后缀的包含匹配) let prov = index[provinceName]; if (!prov) { const pKey = Object.keys(index).find(k => k.includes(provinceName) || provinceName.includes(k)); if (pKey) prov = index[pKey]; } if (!prov) return result; result.province_code = prov.code; // 匹配市 let city = prov.cities[cityName]; if (!city && cityName) { const cKey = Object.keys(prov.cities).find(k => k.includes(cityName) || cityName.includes(k)); if (cKey) city = prov.cities[cKey]; } // 直辖市:省名=市名,市层可能只有一个 if (!city) { const cityKeys = Object.keys(prov.cities); if (cityKeys.length === 1) city = prov.cities[cityKeys[0]]; } // 省直辖县级行政区、部分直辖市导入数据可能出现「城市=区县」: // 此时城市名匹配不到标准市级节点,用「省 + 区县」在该省下横向查一次。 if (!city && districtName) { const matched = this._findDistrictInProvince(prov, districtName); if (matched) { result.city_code = matched.city.code; result.district_code = matched.districtCode; } return result; } if (!city) return result; result.city_code = city.code; // 匹配区县 if (districtName) { let dCode = city.districts[districtName]; if (!dCode) { const dKey = Object.keys(city.districts).find(k => k.includes(districtName) || districtName.includes(k)); if (dKey) dCode = city.districts[dKey]; } if (!dCode) { const matched = this._findDistrictInProvince(prov, districtName); if (matched) { result.city_code = matched.city.code; dCode = matched.districtCode; } } if (dCode) result.district_code = dCode; } return result; } // @private 在指定省份下按区县名称横向匹配,并返回其所属城市 _findDistrictInProvince(prov, districtName) { if (!prov || !districtName) return null; for (const city of Object.values(prov.cities)) { let dCode = city.districts[districtName]; if (!dCode) { const dKey = Object.keys(city.districts).find(k => k.includes(districtName) || districtName.includes(k)); if (dKey) dCode = city.districts[dKey]; } if (dCode) return { city, districtCode: dCode }; } return null; } // @private 根据编码查地区名称 async _getRegionNames(provinceCode, cityCode, districtCode) { const codes = [provinceCode, cityCode, districtCode].filter(Boolean); const map = {}; if (codes.length) { const regions = await this.model('sys_region').where({ code: ['in', codes] }).select(); regions.forEach(r => { map[r.code] = r.name; }); } return { province: map[provinceCode] || '', city: map[cityCode] || '', district: map[districtCode] || '' }; } };