asyncExport.service.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import { Config, Inject, Provide } from '@midwayjs/core';
  2. import { Demand } from '../entity/platform/demand.entity';
  3. import { Admin } from '../entity/system/admin.entity';
  4. import { Dept } from '../entity/system/dept.entity';
  5. import { DictData } from '../entity/system/dictData.entity';
  6. import { DictType } from '../entity/system/dictType.entity';
  7. import { Menus } from '../entity/system/menus.entity';
  8. import { Message } from '../entity/system/message.entity';
  9. import { Region } from '../entity/system/region.entity';
  10. import { Role } from '../entity/system/role.entity';
  11. import { Tags } from '../entity/system/tags.entity';
  12. import { User } from '../entity/system/user.entity';
  13. import { UserMenus } from '../entity/system/userMenus.entity';
  14. import { ApplyCompany } from '../entity/users/applyCompany.entity';
  15. import { Association } from '../entity/users/association.entity';
  16. import { Cirelation } from '../entity/users/cirelation.entity';
  17. import { Company } from '../entity/users/company.entity';
  18. import { CompanyYear } from '../entity/users/companyYear.entity';
  19. import { Competition } from '../entity/users/competition.entity';
  20. import { ContactApply } from '../entity/users/contactApply.entity';
  21. import { Expert } from '../entity/users/expert.entity';
  22. import { Incubator } from '../entity/users/incubator.entity';
  23. import { IncubatorYear } from '../entity/users/incubatorYear.entity';
  24. import { Investment } from '../entity/users/investment.entity';
  25. import { School } from '../entity/users/school.entity';
  26. import { State } from '../entity/users/state.entity';
  27. import { Unit } from '../entity/users/unit.entity';
  28. import { Achievement } from '../entity/platform/achievement.entity';
  29. import { Collection } from '../entity/platform/collection.entity';
  30. import { Design } from '../entity/platform/design.entity';
  31. import { Directory } from '../entity/platform/directory.entity';
  32. import { Footplate } from '../entity/platform/footplate.entity';
  33. import { Friend } from '../entity/platform/friend.entity';
  34. import { Journal } from '../entity/platform/journal.entity';
  35. import { Match } from '../entity/platform/match.entity';
  36. import { MatchPath } from '../entity/platform/matchPath.entity';
  37. import { News } from '../entity/platform/news.entity';
  38. import { Notes } from '../entity/platform/notes.entity';
  39. import { Project } from '../entity/platform/project.entity';
  40. import { Score } from '../entity/platform/score.entity';
  41. import { Sector } from '../entity/platform/sector.entity';
  42. import { Sign } from '../entity/platform/sign.entity';
  43. import { Supply } from '../entity/platform/supply.entity';
  44. import { Support } from '../entity/platform/support.entity';
  45. import { InjectEntityModel } from '@midwayjs/typeorm';
  46. import { Repository } from 'typeorm';
  47. import { ExportMission } from '../entity/exportMission.entity';
  48. import { divide, get, isFinite, isObject, isString, upperFirst } from 'lodash';
  49. import { ExportConfigService } from './exportConfig.service';
  50. import { completeBuilderCondition } from '../frame/conditionBuilder';
  51. import * as dayjs from 'dayjs';
  52. import * as Excel from 'exceljs';
  53. import * as Path from 'path';
  54. import * as fs from 'fs';
  55. import { ErrorCode, ServiceError } from '../error/service.error';
  56. import { ExportConfig } from '../entity/exportConfig.entity';
  57. @Provide()
  58. export class AsyncExportService {
  59. @InjectEntityModel(ExportMission)
  60. model: Repository<ExportMission>;
  61. @InjectEntityModel(ExportConfig)
  62. ecService: Repository<ExportConfig>;
  63. //数据步进值
  64. limit = 50;
  65. async checkHaveDataInQuery(table, query = {}) {
  66. const model: any = get(this, `_model_${table}`);
  67. // 抛出异常: 该数据未经过导出设置,无法导出
  68. if (!model) throw new ServiceError(ErrorCode.NO_EXPORT_SETTING);
  69. const builder = model.createQueryBuilder();
  70. const nq = this.checkColumnInTable(model, query);
  71. completeBuilderCondition(builder, nq);
  72. const total = await builder.getCount();
  73. // 抛出异常: 未查询到可导出的数据
  74. if (total <= 0) throw new ServiceError(ErrorCode.NO_DATA_IN_EXPORT_QUERY);
  75. }
  76. async execute(id) {
  77. const mission = await this.model.createQueryBuilder().where('"id" = :id', { id }).getOne();
  78. // 没有任务,不需要执行
  79. if (!mission) return false;
  80. // 没有配置,不需要执行
  81. const config = get(mission, 'config');
  82. if (!config) return;
  83. /**表名 */
  84. let table: string = get(config, 'table');
  85. // 没有表,不需要执行
  86. if (!table) return;
  87. table = upperFirst(table);
  88. /**用户选择的字段 */
  89. const cc: Array<string> = get(config, 'config');
  90. // 没有导出配置,不需要执行
  91. if (!cc) return;
  92. /**根据表名找到完整的配置 */
  93. const configData = await this.ecService.createQueryBuilder().where('"table" =:table', { table }).getOne();
  94. // 需要通过cc换正常的配置
  95. // 未找到配置,不需要执行
  96. if (!configData) return;
  97. const colConfigs: Array<any> = get(configData, 'config');
  98. if (!colConfigs) return;
  99. /**任务存储的导出字段完整配置 */
  100. const selectCols = colConfigs.filter(f => cc.includes(f.column));
  101. // 计算数据总量
  102. /**查询范围 */
  103. const query = get(config, 'query', {});
  104. const model: any = get(this, `_model_${table}`);
  105. // 没找到model,不需要执行
  106. if (!model) return;
  107. const builder = model.createQueryBuilder();
  108. const nq = this.checkColumnInTable(model, query);
  109. completeBuilderCondition(builder, nq);
  110. /**数据总数,计算进度用 */
  111. const total = await builder.getCount();
  112. // 找数据,处理数据,写入数据
  113. let skip = 0;
  114. const fileName = `${table}-${dayjs().format('YYYYMMDDHHmmss')}.xlsx`;
  115. let downloadPath;
  116. /**循环标志 */
  117. let whileContinue = true;
  118. const head = selectCols.map(i => i.zh);
  119. while (whileContinue) {
  120. const writeInExcel = [];
  121. if (skip === 0) writeInExcel.push(head);
  122. const list = await builder.skip(skip).take(this.limit).getMany();
  123. if (list.length <= 0) {
  124. // 没有数据就不需要继续循环了
  125. whileContinue = false;
  126. break;
  127. }
  128. skip = skip + this.limit;
  129. /**处理数据 */
  130. const excelData = await this.dealData(list, selectCols);
  131. writeInExcel.push(...excelData);
  132. /**写入数据 */
  133. downloadPath = await this.writeExcel(fileName, writeInExcel);
  134. try {
  135. // 计算,更新进度
  136. let per = Math.ceil(divide(skip, total) * 100);
  137. if (per > 100) per = 100;
  138. this.model.update({ id }, { progress: per });
  139. } catch (error) {
  140. console.log(`mission id:${id}; skip:${skip}`);
  141. }
  142. }
  143. await this.model.update({ id }, { progress: 100, uri: downloadPath });
  144. }
  145. async dealData(list, cols) {
  146. const returnData = [];
  147. for (const i of list) {
  148. const excelData = [];
  149. for (const c of cols) {
  150. const column = get(c, 'column');
  151. // 取值
  152. let value = get(i, column);
  153. // 空值下一个
  154. if (!value || value === null) {
  155. // 空字符串站位,否则列对不上
  156. excelData.push('');
  157. continue;
  158. }
  159. // 数据类型验证
  160. const type = get(c, 'type');
  161. value = this.checkValueType(value, type);
  162. // 验证失败或空字符串,直接下一个
  163. if (value === '') {
  164. excelData.push(value);
  165. continue;
  166. }
  167. const source = get(c, 'source');
  168. const from = get(c, 'from');
  169. const from_column = get(c, 'from_column');
  170. value = await this.getValueFromSource(value, source, from, from_column);
  171. excelData.push(value);
  172. }
  173. returnData.push(excelData);
  174. }
  175. return returnData;
  176. }
  177. /**
  178. * 验证数据是否匹配数据类型,不匹配的返回空字符串
  179. * @param value 数据库中的值
  180. * @param type 数据类型
  181. * @returns 空字符串 ''
  182. */
  183. checkValueType(value, type) {
  184. let returnData = '';
  185. switch (type) {
  186. case 'string':
  187. if (isString(value)) returnData = value;
  188. break;
  189. case 'integer':
  190. if (isFinite(value)) returnData = value;
  191. break;
  192. case 'text':
  193. if (isString(value)) returnData = value;
  194. break;
  195. case 'date':
  196. if (dayjs(value).isValid()) returnData = value;
  197. break;
  198. case 'jsonb':
  199. if (isObject(value)) returnData = JSON.stringify(value);
  200. break;
  201. default:
  202. break;
  203. }
  204. return returnData;
  205. }
  206. /**
  207. * 根据来源,转换值
  208. * @param value 数据库中的值
  209. * @param source 数据来源
  210. * @param from 字典/关联表
  211. * @param from_column 具体使用的字段
  212. */
  213. async getValueFromSource(value, source, from, from_column) {
  214. if (source === 'dict') {
  215. // 查出字典数据
  216. const dictData = await this._model_DictData.createQueryBuilder().where('"code" =:code', { code: from }).getMany();
  217. // 没有字典数据,直接返回原值
  218. if (dictData.length <= 0) return value;
  219. let dict_column = from_column;
  220. if (!dict_column) dict_column = 'value';
  221. const dict = dictData.find(f => get(f, dict_column) === value);
  222. // 没找到,返回原值
  223. if (!dict) return value;
  224. // 找到了,返回label
  225. return get(dict, 'label');
  226. } else if (source === 'relation') {
  227. const table = upperFirst(from);
  228. const model = this[`_model_${table}`];
  229. if (!model) return value;
  230. const relationData = await model.createQueryBuilder().where('"id" =:id', { id: value }).getOne();
  231. // 没找到关联的数据
  232. if (!relationData) return value;
  233. // 找到了,就取指定字段
  234. return get(relationData, from_column);
  235. } else return value;
  236. }
  237. getModelName(name) {
  238. const model = this[`_model_${upperFirst(name)}`];
  239. if (!model) return;
  240. return get(model, 'metadata.comment');
  241. }
  242. getDict_tables() {
  243. const keys = Object.keys(this).filter(f => f.includes('_model_'));
  244. const tableList = keys.map(i => {
  245. const model = this[i];
  246. const label = get(model, 'metadata.comment');
  247. const table = get(model, 'metadata.name');
  248. return { label, table };
  249. });
  250. return tableList;
  251. }
  252. @Config('PathConfig.path')
  253. path;
  254. async writeExcel(fileName, data) {
  255. const path = this.path;
  256. if (!path) {
  257. throw new ServiceError('服务端没有设置存储路径');
  258. }
  259. if (!fs.existsSync(path)) {
  260. // 如果不存在文件夹,就创建
  261. this.mkdir(path);
  262. }
  263. const fullPath = Path.resolve(path, fileName);
  264. const has_file = fs.existsSync(fullPath);
  265. const workbook = new Excel.Workbook();
  266. let sheet;
  267. if (!has_file) {
  268. // 没有文件,第一次,需要生成
  269. sheet = workbook.addWorksheet('sheet');
  270. } else {
  271. await workbook.xlsx.readFile(fullPath);
  272. sheet = workbook.getWorksheet('sheet');
  273. }
  274. sheet.addRows(data);
  275. await workbook.xlsx.writeFile(fullPath);
  276. return `/files/cxyy/export/${fileName}`;
  277. }
  278. // 创建文件夹
  279. mkdir(dirname) {
  280. if (fs.existsSync(dirname)) {
  281. return true;
  282. }
  283. if (this.mkdir(Path.dirname(dirname))) {
  284. fs.mkdirSync(dirname);
  285. return true;
  286. }
  287. }
  288. /**检查查询条件是否在表中有字段 */
  289. checkColumnInTable(model, query) {
  290. const columns = get(model, 'metadata.columns', []);
  291. const nq = {};
  292. for (const key in query) {
  293. const column = columns.find(f => get(f, 'propertyName') === key);
  294. if (!column) continue;
  295. const val = query[key];
  296. nq[key] = val;
  297. }
  298. return nq;
  299. }
  300. @InjectEntityModel(Admin)
  301. _model_Admin: Repository<Admin>;
  302. @InjectEntityModel(Dept)
  303. _model_Dept: Repository<Dept>;
  304. @InjectEntityModel(DictData)
  305. _model_DictData: Repository<DictData>;
  306. @InjectEntityModel(DictType)
  307. _model_DictType: Repository<DictType>;
  308. @InjectEntityModel(Menus)
  309. _model_Menus: Repository<Menus>;
  310. @InjectEntityModel(Message)
  311. _model_Message: Repository<Message>;
  312. @InjectEntityModel(Region)
  313. _model_Region: Repository<Region>;
  314. @InjectEntityModel(Role)
  315. _model_Role: Repository<Role>;
  316. @InjectEntityModel(Tags)
  317. _model_Tags: Repository<Tags>;
  318. @InjectEntityModel(User)
  319. _model_User: Repository<User>;
  320. @InjectEntityModel(UserMenus)
  321. _model_UserMenus: Repository<UserMenus>;
  322. @InjectEntityModel(ApplyCompany)
  323. _model_ApplyCompany: Repository<ApplyCompany>;
  324. @InjectEntityModel(Association)
  325. _model_Association: Repository<Association>;
  326. @InjectEntityModel(Cirelation)
  327. _model_Cirelation: Repository<Cirelation>;
  328. @InjectEntityModel(Company)
  329. _model_Company: Repository<Company>;
  330. @InjectEntityModel(CompanyYear)
  331. _model_CompanyYear: Repository<CompanyYear>;
  332. @InjectEntityModel(Competition)
  333. _model_Competition: Repository<Competition>;
  334. @InjectEntityModel(ContactApply)
  335. _model_ContactApply: Repository<ContactApply>;
  336. @InjectEntityModel(Expert)
  337. _model_Expert: Repository<Expert>;
  338. @InjectEntityModel(Incubator)
  339. _model_Incubator: Repository<Incubator>;
  340. @InjectEntityModel(IncubatorYear)
  341. _model_IncubatorYear: Repository<IncubatorYear>;
  342. @InjectEntityModel(Investment)
  343. _model_Investment: Repository<Investment>;
  344. @InjectEntityModel(School)
  345. _model_School: Repository<School>;
  346. @InjectEntityModel(State)
  347. _model_State: Repository<State>;
  348. @InjectEntityModel(Unit)
  349. _model_Unit: Repository<Unit>;
  350. @InjectEntityModel(Achievement)
  351. _model_Achievement: Repository<Achievement>;
  352. @InjectEntityModel(Collection)
  353. _model_Collection: Repository<Collection>;
  354. @InjectEntityModel(Demand)
  355. _model_Demand: Repository<Demand>;
  356. @InjectEntityModel(Design)
  357. _model_Design: Repository<Design>;
  358. @InjectEntityModel(Directory)
  359. _model_Directory: Repository<Directory>;
  360. @InjectEntityModel(Footplate)
  361. _model_Footplate: Repository<Footplate>;
  362. @InjectEntityModel(Friend)
  363. _model_Friend: Repository<Friend>;
  364. @InjectEntityModel(Journal)
  365. _model_Journal: Repository<Journal>;
  366. @InjectEntityModel(Match)
  367. _model_Match: Repository<Match>;
  368. @InjectEntityModel(MatchPath)
  369. _model_MatchPath: Repository<MatchPath>;
  370. @InjectEntityModel(News)
  371. _model_News: Repository<News>;
  372. @InjectEntityModel(Notes)
  373. _model_Notes: Repository<Notes>;
  374. @InjectEntityModel(Project)
  375. _model_Project: Repository<Project>;
  376. @InjectEntityModel(Score)
  377. _model_Score: Repository<Score>;
  378. @InjectEntityModel(Sector)
  379. _model_Sector: Repository<Sector>;
  380. @InjectEntityModel(Sign)
  381. _model_Sign: Repository<Sign>;
  382. @InjectEntityModel(Supply)
  383. _model_Supply: Repository<Supply>;
  384. @InjectEntityModel(Support)
  385. _model_Support: Repository<Support>;
  386. }