|
- 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] || ''
- };
- }
- };
|