Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 

325 řádky
12 KiB

  1. const Base = require('../base');
  2. module.exports = class extends Base {
  3. async indexAction() {
  4. this.assign('currentPage', 'hospital');
  5. this.assign('pageTitle', '医院管理');
  6. this.assign('breadcrumb', [{ name: '医院管理' }]);
  7. return this.display();
  8. }
  9. // 列表(分页)
  10. async listAction() {
  11. const keyword = this.get('keyword') || '';
  12. const provinceCode = this.get('province_code') || '';
  13. const cityCode = this.get('city_code') || '';
  14. const districtCode = this.get('district_code') || '';
  15. const page = this.get('page') || 1;
  16. const pageSize = this.get('pageSize') || 10;
  17. const where = { is_deleted: 0 };
  18. if (keyword) where.name = ['like', `%${keyword}%`];
  19. if (provinceCode) where.province_code = provinceCode;
  20. if (cityCode) where.city_code = cityCode;
  21. if (districtCode) where.district_code = districtCode;
  22. const data = await this.model('hospital')
  23. .where(where)
  24. .order('sort DESC, id ASC')
  25. .page(page, pageSize)
  26. .countSelect();
  27. return this.json({ code: 0, data });
  28. }
  29. // 新增
  30. async addAction() {
  31. const { name, province_code, city_code, district_code, sort, is_show } = this.post();
  32. if (!name || !name.trim()) return this.fail('请输入医院名称');
  33. const exists = await this.model('hospital').where({ name: name.trim(), is_deleted: 0 }).find();
  34. if (!think.isEmpty(exists)) return this.fail('该医院已存在');
  35. const regionNames = await this._getRegionNames(province_code, city_code, district_code);
  36. await this.model('hospital').add({
  37. name: name.trim(),
  38. province_code: province_code || '',
  39. city_code: city_code || '',
  40. district_code: district_code || '',
  41. province_name: regionNames.province,
  42. city_name: regionNames.city,
  43. district_name: regionNames.district,
  44. sort: sort || 0,
  45. is_show: is_show === undefined ? 1 : (is_show ? 1 : 0)
  46. });
  47. await this.model('hospital').clearTreeCache();
  48. await this.log('add', '医院管理', `新增医院「${name.trim()}」`);
  49. return this.success();
  50. }
  51. // 编辑
  52. async editAction() {
  53. const { id, name, province_code, city_code, district_code, sort, is_show } = this.post();
  54. if (!id) return this.fail('参数错误');
  55. if (!name || !name.trim()) return this.fail('请输入医院名称');
  56. const exists = await this.model('hospital').where({ name: name.trim(), id: ['!=', id], is_deleted: 0 }).find();
  57. if (!think.isEmpty(exists)) return this.fail('该医院已存在');
  58. const regionNames = await this._getRegionNames(province_code, city_code, district_code);
  59. await this.model('hospital').where({ id }).update({
  60. name: name.trim(),
  61. province_code: province_code || '',
  62. city_code: city_code || '',
  63. district_code: district_code || '',
  64. province_name: regionNames.province,
  65. city_name: regionNames.city,
  66. district_name: regionNames.district,
  67. sort: sort || 0,
  68. is_show: is_show ? 1 : 0
  69. });
  70. await this.model('hospital').clearTreeCache();
  71. await this.log('edit', '医院管理', `编辑医院(ID:${id})「${name.trim()}」`);
  72. return this.success();
  73. }
  74. // 删除
  75. async deleteAction() {
  76. const { id } = this.post();
  77. if (!id) return this.fail('参数错误');
  78. await this.model('hospital').where({ id }).update({ is_deleted: 1 });
  79. await this.model('hospital').clearTreeCache();
  80. await this.log('delete', '医院管理', `删除医院(ID:${id})`);
  81. return this.success();
  82. }
  83. // 切换展示状态
  84. async toggleShowAction() {
  85. const { id, is_show } = this.post();
  86. if (!id) return this.fail('参数错误');
  87. await this.model('hospital').where({ id }).update({ is_show: is_show ? 1 : 0 });
  88. await this.model('hospital').clearTreeCache();
  89. await this.log('edit', '医院管理', `${is_show ? '展示' : '隐藏'}医院(ID:${id})`);
  90. return this.success();
  91. }
  92. // 排序
  93. async sortAction() {
  94. const { ids } = this.post();
  95. if (!ids || !ids.length) return this.fail('参数错误');
  96. for (let i = 0; i < ids.length; i++) {
  97. await this.model('hospital').where({ id: ids[i] }).update({ sort: ids.length - i });
  98. }
  99. await this.model('hospital').clearTreeCache();
  100. return this.success();
  101. }
  102. // Excel 导入(表头:关联机构名称/省份/城市/区县)
  103. async importAction() {
  104. const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  105. let file = this.file('file');
  106. if (!file || !file.path) {
  107. await sleep(1000);
  108. file = this.file('file');
  109. }
  110. if (!file || !file.path) return this.fail('请上传 Excel 文件');
  111. const ExcelJS = require('exceljs');
  112. const workbook = new ExcelJS.Workbook();
  113. try {
  114. await workbook.xlsx.readFile(file.path);
  115. } catch (e) {
  116. return this.fail('文件解析失败,请上传有效的 Excel 文件');
  117. }
  118. const sheet = workbook.worksheets[0];
  119. if (!sheet) return this.fail('Excel 内容为空');
  120. // 读取数据行(跳过表头)
  121. const rows = [];
  122. sheet.eachRow((row, rowNumber) => {
  123. if (rowNumber === 1) return; // 表头
  124. const name = this._cellText(row.getCell(1));
  125. const province = this._cellText(row.getCell(2));
  126. const city = this._cellText(row.getCell(3));
  127. const district = this._cellText(row.getCell(4));
  128. if (name) rows.push({ name, province, city, district });
  129. });
  130. if (!rows.length) return this.fail('没有有效的数据行');
  131. // 预加载全部地区,构建名称索引
  132. const regions = await this.model('sys_region').where({ status: 1 }).select();
  133. const regionIndex = this._buildRegionIndex(regions);
  134. // 已存在的医院名称
  135. const existRows = await this.model('hospital').where({ is_deleted: 0 }).field('name').select();
  136. const existNames = new Set(existRows.map(r => r.name));
  137. const toInsert = [];
  138. const seen = new Set();
  139. const unmatched = []; // 地区未匹配的医院名
  140. let skipped = 0;
  141. rows.forEach(r => {
  142. if (existNames.has(r.name) || seen.has(r.name)) { skipped++; return; }
  143. seen.add(r.name);
  144. const codes = this._matchRegion(regionIndex, r.province, r.city, r.district);
  145. if ((r.province && !codes.province_code) || (r.city && !codes.city_code) || (r.district && !codes.district_code)) {
  146. unmatched.push(r.name);
  147. }
  148. toInsert.push({
  149. name: r.name,
  150. province_code: codes.province_code,
  151. city_code: codes.city_code,
  152. district_code: codes.district_code,
  153. province_name: r.province || '',
  154. city_name: r.city || '',
  155. district_name: r.district || '',
  156. sort: 0,
  157. is_show: 1
  158. });
  159. });
  160. if (!toInsert.length) return this.fail('没有新增医院(均已存在)');
  161. // 分批插入
  162. const batchSize = 200;
  163. for (let i = 0; i < toInsert.length; i += batchSize) {
  164. await this.model('hospital').addMany(toInsert.slice(i, i + batchSize));
  165. }
  166. await this.model('hospital').clearTreeCache();
  167. await this.log('import', '医院管理', `导入医院 ${toInsert.length} 条`);
  168. return this.success({
  169. count: toInsert.length,
  170. skipped,
  171. unmatchedCount: unmatched.length,
  172. unmatched: unmatched.slice(0, 50)
  173. });
  174. }
  175. // @private 读取单元格文本
  176. _cellText(cell) {
  177. if (!cell || cell.value === null || cell.value === undefined) return '';
  178. const v = cell.value;
  179. if (typeof v === 'object') {
  180. if (v.text) return String(v.text).trim();
  181. if (v.result !== undefined) return String(v.result).trim();
  182. if (v.richText) return v.richText.map(t => t.text).join('').trim();
  183. return '';
  184. }
  185. return String(v).trim();
  186. }
  187. // @private 构建地区名称索引:{ provinceName: {code, cities: { cityName: {code, districts: { distName: code }}}}}
  188. _buildRegionIndex(regions) {
  189. const provinces = {};
  190. const cityByParent = {};
  191. const distByParent = {};
  192. regions.forEach(r => {
  193. const code = r.code;
  194. if (code.endsWith('0000')) {
  195. provinces[r.name] = { code, cities: {} };
  196. } else if (code.endsWith('00')) {
  197. const pCode = code.substring(0, 2) + '0000';
  198. if (!cityByParent[pCode]) cityByParent[pCode] = [];
  199. cityByParent[pCode].push({ code, name: r.name });
  200. } else {
  201. const cCode = code.substring(0, 4) + '00';
  202. if (!distByParent[cCode]) distByParent[cCode] = [];
  203. distByParent[cCode].push({ code, name: r.name });
  204. }
  205. });
  206. // 组装
  207. Object.values(provinces).forEach(prov => {
  208. const cities = cityByParent[prov.code] || [];
  209. cities.forEach(c => {
  210. prov.cities[c.name] = { code: c.code, districts: {} };
  211. const dists = distByParent[c.code] || [];
  212. dists.forEach(d => { prov.cities[c.name].districts[d.name] = d.code; });
  213. });
  214. });
  215. return provinces;
  216. }
  217. // @private 名称匹配编码(容错:直辖市省市同名、模糊包含)
  218. _matchRegion(index, provinceName, cityName, districtName) {
  219. const result = { province_code: '', city_code: '', district_code: '' };
  220. if (!provinceName) return result;
  221. // 匹配省(容错:去掉"省/市/自治区"等后缀的包含匹配)
  222. let prov = index[provinceName];
  223. if (!prov) {
  224. const pKey = Object.keys(index).find(k => k.includes(provinceName) || provinceName.includes(k));
  225. if (pKey) prov = index[pKey];
  226. }
  227. if (!prov) return result;
  228. result.province_code = prov.code;
  229. // 匹配市
  230. let city = prov.cities[cityName];
  231. if (!city && cityName) {
  232. const cKey = Object.keys(prov.cities).find(k => k.includes(cityName) || cityName.includes(k));
  233. if (cKey) city = prov.cities[cKey];
  234. }
  235. // 直辖市:省名=市名,市层可能只有一个
  236. if (!city) {
  237. const cityKeys = Object.keys(prov.cities);
  238. if (cityKeys.length === 1) city = prov.cities[cityKeys[0]];
  239. }
  240. // 省直辖县级行政区、部分直辖市导入数据可能出现「城市=区县」:
  241. // 此时城市名匹配不到标准市级节点,用「省 + 区县」在该省下横向查一次。
  242. if (!city && districtName) {
  243. const matched = this._findDistrictInProvince(prov, districtName);
  244. if (matched) {
  245. result.city_code = matched.city.code;
  246. result.district_code = matched.districtCode;
  247. }
  248. return result;
  249. }
  250. if (!city) return result;
  251. result.city_code = city.code;
  252. // 匹配区县
  253. if (districtName) {
  254. let dCode = city.districts[districtName];
  255. if (!dCode) {
  256. const dKey = Object.keys(city.districts).find(k => k.includes(districtName) || districtName.includes(k));
  257. if (dKey) dCode = city.districts[dKey];
  258. }
  259. if (!dCode) {
  260. const matched = this._findDistrictInProvince(prov, districtName);
  261. if (matched) {
  262. result.city_code = matched.city.code;
  263. dCode = matched.districtCode;
  264. }
  265. }
  266. if (dCode) result.district_code = dCode;
  267. }
  268. return result;
  269. }
  270. // @private 在指定省份下按区县名称横向匹配,并返回其所属城市
  271. _findDistrictInProvince(prov, districtName) {
  272. if (!prov || !districtName) return null;
  273. for (const city of Object.values(prov.cities)) {
  274. let dCode = city.districts[districtName];
  275. if (!dCode) {
  276. const dKey = Object.keys(city.districts).find(k => k.includes(districtName) || districtName.includes(k));
  277. if (dKey) dCode = city.districts[dKey];
  278. }
  279. if (dCode) return { city, districtCode: dCode };
  280. }
  281. return null;
  282. }
  283. // @private 根据编码查地区名称
  284. async _getRegionNames(provinceCode, cityCode, districtCode) {
  285. const codes = [provinceCode, cityCode, districtCode].filter(Boolean);
  286. const map = {};
  287. if (codes.length) {
  288. const regions = await this.model('sys_region').where({ code: ['in', codes] }).select();
  289. regions.forEach(r => { map[r.code] = r.name; });
  290. }
  291. return {
  292. province: map[provinceCode] || '',
  293. city: map[cityCode] || '',
  294. district: map[districtCode] || ''
  295. };
  296. }
  297. };