SupplierInitImpl.java 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. package com.sdtool.business.service;
  2. import com.sdtool.business.dao.*;
  3. import com.sdtool.common.api.*;
  4. import com.sdtool.common.base.BaseService;
  5. import com.sdtool.common.datas.ERPModule;
  6. import com.sdtool.common.datas.ESKeys;
  7. import com.sdtool.common.datas.HttpCode;
  8. import com.sdtool.common.datas.RedisKeys;
  9. import com.sdtool.common.entity.base.BusinessBookPeriod;
  10. import com.sdtool.common.entity.base.DataBaseMultiItemEx;
  11. import com.sdtool.common.entity.base.ProcessMapItem;
  12. import com.sdtool.common.entity.doc.BusinessScript;
  13. import com.sdtool.common.entity.site.ERPTokenUser;
  14. import com.sdtool.common.entity.system.ConfigValue;
  15. import com.sdtool.common.entity.system.KeyValuePair;
  16. import com.sdtool.common.tool.ERPUtils;
  17. import com.sdtool.common.tool.ParticularTimeTasker;
  18. import com.dySweetFishPlugin.elasticsearch.ESClient;
  19. import com.dySweetFishPlugin.redis.RedisService;
  20. import com.dySweetFishPlugin.sql.DBService;
  21. import com.dySweetFishPlugin.sql.DataBaseMultiItem;
  22. import com.dySweetFishPlugin.sql.RMapUtils;
  23. import com.dySweetFishPlugin.sql.TableIdService;
  24. import com.dySweetFishPlugin.sql.dao.TunaService;
  25. import com.dySweetFishPlugin.tool.lang.ThreadFactoryWithNamePrefix;
  26. import com.sweetfish.convert.json.JsonConvert;
  27. import com.sweetfish.service.Local;
  28. import com.sweetfish.service.RetResult;
  29. import com.sweetfish.util.AnyValue;
  30. import com.sweetfish.util.AutoLoad;
  31. import com.sweetfish.util.ResourceType;
  32. import com.sweetfish.util.Utility;
  33. import groovy.lang.Binding;
  34. import groovy.util.GroovyScriptEngine;
  35. import groovy.util.ResourceException;
  36. import groovy.util.ScriptException;
  37. import org.apache.commons.io.FilenameUtils;
  38. import org.apache.commons.lang3.StringUtils;
  39. import org.beetl.core.Configuration;
  40. import org.beetl.core.GroupTemplate;
  41. import org.beetl.core.Template;
  42. import org.beetl.core.resource.FileResourceLoader;
  43. import org.rex.DB;
  44. import org.rex.RMap;
  45. import org.rex.db.exception.DBException;
  46. import javax.annotation.Priority;
  47. import javax.annotation.Resource;
  48. import java.io.File;
  49. import java.io.IOException;
  50. import java.net.URLEncoder;
  51. import java.nio.charset.StandardCharsets;
  52. import java.nio.file.Files;
  53. import java.nio.file.Path;
  54. import java.nio.file.Paths;
  55. import java.time.Duration;
  56. import java.time.LocalDateTime;
  57. import java.time.format.DateTimeFormatter;
  58. import java.util.*;
  59. import java.util.concurrent.*;
  60. import java.util.stream.Collectors;
  61. import java.util.stream.IntStream;
  62. import java.util.stream.Stream;
  63. /**
  64. * Created by 81460 on 2020-03-23
  65. * 增加网站初始化服务
  66. *
  67. * @author 81460
  68. */
  69. @AutoLoad(false)
  70. @Local
  71. @ResourceType(SupplierInitService.class)
  72. @Priority(7999)
  73. public class SupplierInitImpl extends BaseService implements SupplierInitService {
  74. @Resource
  75. ESClient esClient;
  76. @Resource
  77. TunaService tunaService;
  78. @Resource
  79. TableIdService tableIdService;
  80. @Resource
  81. RedisService redisService;
  82. @Resource
  83. JsonConvert jsonConvert;
  84. @Resource
  85. DBService dbService;
  86. @Resource
  87. NoSqlKeysService keysService;
  88. @Resource
  89. SystemService systemService;
  90. @Resource
  91. PermissionService permissionService;
  92. @Resource
  93. ConfigService configService;
  94. @Resource(name = "APP_HOME")
  95. private String appHome;
  96. @Resource(name = "property.bucketFileRoot")
  97. private String bucketFileRoot;
  98. @Resource(name = "property.sysRunMode")
  99. private String sysRunMode;
  100. @Resource(name = "property.bookStartYear")
  101. private int bookStartYear;
  102. @Resource(name = "property.bookEndYear")
  103. private int bookEndYear;
  104. @Resource(name = "property.bookSplitKind")
  105. private int bookSplitKind;
  106. @Resource(name = "property.fileDownloadUrl")
  107. private String downloadUrl;
  108. @Resource(name = "property.outFileDownloadUrl")
  109. private String outDownloadUrl;
  110. @Resource(name = "property.materialStockShards")
  111. private int materialStockShards;
  112. private DocDao docDao;
  113. private SystemDao systemDao;
  114. private MallDao mallDao;
  115. private DesignDao designDao;
  116. private ExecutorService initInfoExecutor;
  117. private final List<ScheduledThreadPoolExecutor> scheduleThreadList = new ArrayList<>();
  118. private final List<ParticularTimeTasker> particularTimeTaskList = new ArrayList<>();
  119. private final Map<String, String[]> esIndexMap = new ConcurrentHashMap<>();
  120. private final ConcurrentHashMap<String, String> sysDataBaseItem = new ConcurrentHashMap<>();
  121. private String redisHashListGet;
  122. private String runEnvironment;
  123. private boolean resetClientInfo;
  124. private String initScriptPathName;
  125. private boolean resetAccountTradeBill;
  126. private boolean resetPaperInfo;
  127. @Override
  128. public void init(AnyValue config) {
  129. initInfoExecutor = Executors.newFixedThreadPool(4, new ThreadFactoryWithNamePrefix("[初始化线程池]"));
  130. //数据库更新
  131. boolean master = false;
  132. AnyValue masterValue = config.getAnyValue("master");
  133. if (masterValue != null) {
  134. master = masterValue.getBoolValue("value");
  135. }
  136. AnyValue resetClientValue = config.getAnyValue("resetClientInfo");
  137. if (resetClientValue != null) {
  138. resetClientInfo = resetClientValue.getBoolValue("value");
  139. } else {
  140. resetClientInfo = true;
  141. }
  142. AnyValue resetAccountTradeBillValue = config.getAnyValue("resetAccountTradeBill");
  143. if (resetAccountTradeBillValue != null) {
  144. resetAccountTradeBill = resetAccountTradeBillValue.getBoolValue("value");
  145. } else {
  146. resetAccountTradeBill = true;
  147. }
  148. AnyValue resetPaperInfoValue = config.getAnyValue("resetPaperInfo");
  149. if (resetPaperInfoValue != null) {
  150. resetPaperInfo = resetPaperInfoValue.getBoolValue("value");
  151. } else {
  152. resetPaperInfo = true;
  153. }
  154. AnyValue initScriptPathNameValue = config.getAnyValue("initScriptPathName");
  155. if (initScriptPathNameValue != null) {
  156. initScriptPathName = initScriptPathNameValue.getValue("value");
  157. } else {
  158. initScriptPathName = "init";
  159. }
  160. try {
  161. //无论是否产品中心都需要初始化全部
  162. // String dataSql = ("ProductCenter".equalsIgnoreCase(sysRunMode)) ?
  163. // "select * from sys_DataBaseMultiItem where shardingKey = 1000 and voidFlag = 0 order by id" :
  164. // "select * from sys_DataBaseMultiItem where voidFlag = 0 order by id";
  165. String dataSql = "select * from sys_DataBaseMultiItem where voidFlag = 0 order by id";
  166. List<DataBaseMultiItemEx> dataList = DB.getList(dataSql, DataBaseMultiItemEx.class);
  167. dataList.forEach((x) -> sysDataBaseItem.put(x.getDataBaseAlias(), x.getShardingKey()));
  168. if (master) {
  169. //读取数据源,进行初始化操作
  170. //region 更新sql语句
  171. //一定要注意:更新的sql语句开发者应该在sql更新目录下保存一份,因为正式执行在sqlupdate目录中,执行后会删除文件
  172. Path sqlPath = Paths.get(appHome + File.separator + "conf" + File.separator + "sqlupdate");
  173. if (Files.exists(sqlPath) && (Files.isDirectory(sqlPath))) {
  174. try (Stream<Path> paths = Files.walk(sqlPath, 1).filter(Files::isRegularFile)) {
  175. FileResourceLoader resourceLoader = new FileResourceLoader(appHome + File.separator + "conf" + File.separator + "sqlupdate", "utf-8");
  176. Configuration cfg = Configuration.defaultConfiguration();
  177. GroupTemplate gt = new GroupTemplate(resourceLoader, cfg);
  178. paths.forEach((x) -> {
  179. dataList.forEach((d) -> {
  180. //https://gitee.com/xiandafu/beetl/issues/I1A0XZ
  181. //这里本来想放在循环上面,但不知道为什么报错 Queue full,以前测试的时候还可以,不知道啥毛病
  182. Template t = gt.getTemplate(FilenameUtils.getBaseName(x.toFile().getName()) + "." + FilenameUtils.getExtension(x.toFile().getName()));
  183. logger.info(d.getDataBaseAlias() + "执行系统初始化sql");
  184. t.binding("supplyCode", d.getShardingKey());
  185. String content = t.render();
  186. if (StringUtils.isNotEmpty(content)) {
  187. try {
  188. content = ERPUtils.replaceEscape(content);
  189. dbService.batchUpdate(d.getDataBaseAlias(), content.split(";"));
  190. logger.info(d.getDataBaseAlias() + "执行初始化sql成功");
  191. } catch (DBException e) {
  192. logger.error("执行初始化sql失败: " + e.getMessage(), e);
  193. }
  194. }
  195. });
  196. try {
  197. Files.deleteIfExists(x);
  198. } catch (IOException e) {
  199. logger.error("执行初始化sql失败: " + e.getMessage(), e);
  200. }
  201. });
  202. resourceLoader.close();
  203. } catch (IOException e) {
  204. logger.error("执行初始化sql失败: " + e.getMessage(), e);
  205. }
  206. }
  207. //endregion
  208. //region 更新es
  209. //es的更新有些复杂,没法用文件名表示,暂定为用rest服务手动更新
  210. //endregion
  211. }
  212. } catch (DBException e) {
  213. logger.error("执行sql出错: " + e.getMessage(), e);
  214. }
  215. }
  216. @Override
  217. public void start(AnyValue config) {
  218. systemDao = tunaService.generate(SystemDao.class);
  219. docDao = tunaService.generate(DocDao.class);
  220. mallDao = tunaService.generate(MallDao.class);
  221. designDao = tunaService.generate(DesignDao.class);
  222. AnyValue environmentValue = config.getAnyValue("environment");
  223. if (environmentValue != null) {
  224. runEnvironment = environmentValue.getValue("value");
  225. } else {
  226. runEnvironment = "dev";
  227. }
  228. boolean master = false;
  229. AnyValue masterValue = config.getAnyValue("master");
  230. if (masterValue != null) {
  231. master = masterValue.getBoolValue("value");
  232. }
  233. if (master) {
  234. Path path = Paths.get(appHome + File.separator + "conf" + File.separator + "redisscript" + File.separator + "lget_ldel.lua");
  235. this.redisHashListGet = redisService.scriptLoad(readUTF8File(path, false));
  236. //读取数据源,进行初始化操作
  237. try {
  238. //把所有分库写入到redis中
  239. redisService.del(keysService.getRedisKey(RedisKeys.KEY_SUPPLIER_PLATFORM));
  240. redisService.del(keysService.getRedisKey(RedisKeys.KEY_SUPPLIER_SHARDING_KEY_PLATFORM));
  241. redisService.del(keysService.getRedisKey(RedisKeys.KEY_SUPPLIER_ITEM_CODE_PLATFORM));
  242. //商户中心只允许一个源执行,免得初始化一大堆
  243. //无论是否产品中心都需要初始化全部
  244. // String dataSql = ("ProductCenter".equalsIgnoreCase(sysRunMode)) ?
  245. // "select * from sys_DataBaseMultiItem where shardingKey = 1000 and voidFlag = 0 order by id" :
  246. // "select * from sys_DataBaseMultiItem where voidFlag = 0 order by id";
  247. String dataSql = "select * from sys_DataBaseMultiItem where voidFlag = 0 order by id";
  248. List<DataBaseMultiItemEx> dataList = DB.getList(dataSql, DataBaseMultiItemEx.class);
  249. if (!dataList.isEmpty()) {
  250. //检查数据有效性
  251. for (DataBaseMultiItemEx item : dataList) {
  252. long supplierCode = Long.parseLong(item.getShardingKey());
  253. if ((supplierCode <= 0L) || (supplierCode > 9999999L)) {
  254. //加入此条件的限制主要用来对supplierCode进行62进制编码
  255. //因为有的在线支付在异步通知中不传递attach参数,为了统一管理,对商户订单号头四位进行处理,
  256. //实际是不能大于62^4=14,776,336的,为方便记忆,1千万足够用了
  257. throw new RuntimeException("商户分厂的分表id超出范围,不能小于0或者大于9999999");
  258. }
  259. }
  260. //写入到redis,方便前端获取
  261. Map<String, String> supplierClientMap = dataList.stream().collect(
  262. Collectors.toMap(x -> String.valueOf(x.getId()), x -> jsonConvert.convertTo(x))
  263. );
  264. Map<String, String> supplierShardingClientMap = dataList.stream().collect(
  265. Collectors.toMap(DataBaseMultiItem::getShardingKey, x -> jsonConvert.convertTo(x))
  266. );
  267. Map<String, String> supplierItemCodeClientMap = dataList.stream().collect(
  268. Collectors.toMap(DataBaseMultiItemEx::getItemCode, x -> jsonConvert.convertTo(x))
  269. );
  270. if (!supplierClientMap.isEmpty()) {
  271. redisService.hmset(keysService.getRedisKey(RedisKeys.KEY_SUPPLIER_PLATFORM), supplierClientMap);
  272. }
  273. if (!supplierShardingClientMap.isEmpty()) {
  274. redisService.hmset(keysService.getRedisKey(RedisKeys.KEY_SUPPLIER_SHARDING_KEY_PLATFORM), supplierShardingClientMap);
  275. }
  276. if (!supplierItemCodeClientMap.isEmpty()) {
  277. redisService.hmset(keysService.getRedisKey(RedisKeys.KEY_SUPPLIER_ITEM_CODE_PLATFORM), supplierItemCodeClientMap);
  278. }
  279. }
  280. dataList.parallelStream().forEach((x) -> initSupplier(x.getDataBaseAlias(), Long.parseLong(x.getShardingKey()), x.getItemType()));
  281. } catch (DBException e) {
  282. logger.error(e.getMessage(), e);
  283. }
  284. }
  285. //操作日志,写一个key为0的,某些属于平台操作,无sullier key
  286. esClient.checkIndexEx(ESKeys.ESOPLOG_INDEX + "_0", ESKeys.INDEX_CONFIG,
  287. Utility.ofMap(ESKeys.ESERPDEFAULT_TYPE, "oplog.json"));
  288. }
  289. @Override
  290. public void destroy(AnyValue config) {
  291. scheduleThreadList.forEach(ScheduledThreadPoolExecutor::shutdown);
  292. if (initInfoExecutor != null) {
  293. initInfoExecutor.shutdown();
  294. }
  295. particularTimeTaskList.forEach(ParticularTimeTasker::stop);
  296. }
  297. @SuppressWarnings("unchecked")
  298. public List<String> redisListGetAndDel(String key, int range) {
  299. Object eval = redisService.evalsha(this.redisHashListGet,
  300. 1,
  301. key,
  302. String.valueOf(range));
  303. return (List<String>) eval;
  304. }
  305. private void initSupplier(String dataSourceId, long supplierCode, String initScriptRoot, Binding binding) {
  306. //获取脚本目录下的所有文件,并按照文件名排序
  307. File dir = new File(initScriptRoot);
  308. File[] scriptFiles = dir.listFiles();
  309. if (scriptFiles != null) {
  310. GroovyScriptEngine gse;
  311. try {
  312. gse = new GroovyScriptEngine(initScriptRoot, this.getClass().getClassLoader());
  313. //region 0、语言扩展方法,先执行,比如扩展一些Groovy方法
  314. //SupplierInitImpl是不允许运行脚本的,因为它本身需要初始化脚本代码,所以这里执行扩展主要是为了下面的初始化脚本使用
  315. //而业务的脚本代码如果需要Groovy扩展方法,则由BaseService启动执行即可
  316. File[] firstFiles = Arrays.stream(scriptFiles)
  317. .filter(file -> file.getName().startsWith("First_"))
  318. .toArray(File[]::new);
  319. //按前面的数字排下序,防止有些需要先执行
  320. Arrays.sort(firstFiles, Comparator.comparingInt(f -> Integer.parseInt(f.getName().split("_")[1])));
  321. for (File file : firstFiles) {
  322. gse.run(file.getName(), binding);
  323. }
  324. //endregion
  325. //region 1、初始化脚本,先处理,因为要阻塞等待执行完成
  326. //过滤掉Ignore_开头的文件,文件为公共方法
  327. File[] initFiles = Arrays.stream(scriptFiles)
  328. .filter(file -> file.getName().startsWith("Init_"))
  329. .toArray(File[]::new);
  330. List<CompletableFuture<Void>> futureList = new ArrayList<>();
  331. //按前面的数字排下序,防止有些需要先执行
  332. Arrays.sort(initFiles, Comparator.comparingInt(f -> Integer.parseInt(f.getName().split("_")[1])));
  333. for (File file : initFiles) {
  334. //加载并执行Groovy脚本,获取返回的Runnable对象,这里如果用createScript,然后调用invokeMethod需要传递两次参数,并且invokeMethod传递参数还是一个固定的,
  335. Runnable runnable = (Runnable) gse.run(file.getName(), binding);
  336. if (runnable != null) {
  337. //将Runnable对象添加到线程池
  338. futureList.add(CompletableFuture.runAsync(runnable, initInfoExecutor));
  339. }
  340. }
  341. CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join();
  342. //endregion
  343. //region 2、间隔多少秒干活
  344. //经过测试,groovy的好处就是Runnable里面出现异常了,还能继续下个间隔干活
  345. File[] scheduleFiles = Arrays.stream(scriptFiles)
  346. .filter(file -> file.getName().startsWith("Schedule_"))
  347. .toArray(File[]::new);
  348. //按前面的数字排下序,防止有些需要先执行
  349. Arrays.sort(scheduleFiles, Comparator.comparingInt(f -> Integer.parseInt(f.getName().split("_")[1])));
  350. for (File file : scheduleFiles) {
  351. //加载并执行Groovy脚本,获取返回的Runnable对象
  352. Runnable runnable = (Runnable) gse.run(file.getName(), binding);
  353. ScheduledThreadPoolExecutor cycleExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryWithNamePrefix("[周期任务线程池" + FilenameUtils.removeExtension(file.getName()) + supplierCode + "]"));
  354. long initialDelay = Integer.parseInt(file.getName().split("_")[2]);
  355. long delay = Integer.parseInt(file.getName().split("_")[3]);
  356. cycleExecutor.scheduleWithFixedDelay(runnable, initialDelay, delay, TimeUnit.SECONDS);
  357. scheduleThreadList.add(cycleExecutor);
  358. }
  359. //endregion
  360. //region 3、定时处理业务
  361. File[] timeFiles = Arrays.stream(scriptFiles)
  362. .filter(file -> file.getName().startsWith("Time_"))
  363. .toArray(File[]::new);
  364. //按前面的数字排下序,防止有些需要先执行
  365. Arrays.sort(timeFiles, Comparator.comparingInt(f -> Integer.parseInt(f.getName().split("_")[1])));
  366. for (File file : timeFiles) {
  367. //加载并执行Groovy脚本,获取返回的Runnable对象
  368. Runnable runnable = (Runnable) gse.run(file.getName(), binding);
  369. //完整的文件名 Time_1(序号)_D/M(按天、按月执行)_12(日期,12号,按天时为0)_15(小时)_00(分钟)_00(秒)_脚本说明.groovy
  370. String[] timeArray = file.getName().split("_");
  371. String timeKind = timeArray[2];//执行类型 D按天 M按月
  372. int day = Integer.parseInt(timeArray[3]);//日期,按天执行为统一为0即可
  373. int hour = Integer.parseInt(timeArray[4]);//小时
  374. int minutes = Integer.parseInt(timeArray[5]);
  375. int seconds = Integer.parseInt(timeArray[6]);
  376. ParticularTimeTasker timeTasker = new ParticularTimeTasker(
  377. runnable,
  378. () -> {
  379. //计算下一次执行的时间,此处代码应该简化一下,暂时为了验证和测试,先分开写
  380. //当前时间
  381. LocalDateTime now = LocalDateTime.now();
  382. if (StringUtils.equalsIgnoreCase("D", timeKind)) {
  383. LocalDateTime nextExecutionDate = LocalDateTime.of(now.getYear(), now.getMonth(), now.getDayOfMonth(), hour, minutes, seconds);
  384. if (now.isAfter(nextExecutionDate)) {
  385. //如果当前时间已经过了今天的hour点minutes分seconds秒,那么任务的开始时间设置为明天的同一时间
  386. nextExecutionDate = nextExecutionDate.plusDays(1);
  387. }
  388. logger.info("按天执行:下一次执行时间" + nextExecutionDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
  389. Duration duration = Duration.between(now, nextExecutionDate);
  390. //此处不加1秒有问题,体现在如果脚本里面的执行特别快,duration.toSeconds()不是很精确,导致会在预定时间之前(大概200毫秒,一般不超过1秒)执行完成,然后由于时间还没到,会多次执行,所以统一加1秒
  391. //主要是这里+1秒会不会导致每次都顺延多一秒钟,没法测试:(,但理论上应该是拿下一次执行的时间计算的,应该不会错
  392. return duration.toSeconds() + 1;
  393. } else if (StringUtils.equalsIgnoreCase("M", timeKind)) {
  394. //每个月的day日hour点minutes分seconds秒执行
  395. LocalDateTime nextExecutionDate = LocalDateTime.of(now.getYear(), now.getMonth(), day, hour, minutes, seconds);
  396. if (now.isAfter(nextExecutionDate)) {
  397. nextExecutionDate = nextExecutionDate.plusMonths(1);
  398. }
  399. logger.info("按月执行:下一次执行时间" + nextExecutionDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
  400. Duration duration = Duration.between(now, nextExecutionDate);
  401. //ParticularTimeTasker中的定义的按秒间隔执行,查看ParticularTimeTasker的start方法
  402. return duration.toSeconds() + 1;
  403. }
  404. return -1L;
  405. });
  406. timeTasker.start();
  407. particularTimeTaskList.add(timeTasker);
  408. }
  409. //endregion
  410. //region 4、一次性执行的脚本,比如创建目录等无需在线程池中执行的方法
  411. File[] onceFiles = Arrays.stream(scriptFiles)
  412. .filter(file -> file.getName().startsWith("Run_"))
  413. .toArray(File[]::new);
  414. //按前面的数字排下序,防止有些需要先执行
  415. Arrays.sort(timeFiles, Comparator.comparingInt(f -> Integer.parseInt(f.getName().split("_")[1])));
  416. for (File file : onceFiles) {
  417. gse.run(file.getName(), binding);
  418. }
  419. //endregion
  420. } catch (IOException | ScriptException | ResourceException e) {
  421. throw new RuntimeException(e);
  422. }
  423. }
  424. }
  425. /**
  426. * 商户初始化
  427. * 2024-01-01 tt修改为按脚本执行,避免每个业务都来一遍
  428. * 脚本规范:脚本按文件名顺序执行,所以文件名前面以 执行类型_顺序(数字)_ 开头,如果文件名以Ignore_开头(用于放置公共方法),则不执行
  429. * 执行脚本传入所有的参数binding,当前还是固定的,原因见注释,编写规范见10_Inquire.groovy脚本(用的参数多可查看实现方式)
  430. * 最简单读数据库写redis的代码参考Init_19_ProductType.groovy,最简单写ES的代码参考Init_20_ProductImage.groovy
  431. * tt:我们的业务需求当前大概就碰到四种初始化脚本
  432. * 1、查数据库初始化redis或者es,文件名为Init_开头
  433. * 2、每隔多少分钟干活,可以抽象为间隔多少秒处理数据,文件名为 Schedule_序号_第一次执行秒_间隔秒_xxx
  434. * 3、定时任务,当前遇到的业务基本集中在按天,按月固定时间执行,文件名为Time_开头,具体见代码注释
  435. * 其它的暂时没碰到,所以代码没必要写的像Quartz那样太通用
  436. * 4、同步执行的业务,可能是创建目录之类的操作,以Run_开头执行算了(这类脚本看之前的代码理论上可以放在第一种任务中执行,为兼容加一种)
  437. *
  438. * @param dataSourceId 分库
  439. * @param supplierCode 分表
  440. * @param supplierType 商户类型
  441. */
  442. private void initSupplier(String dataSourceId, long supplierCode, int supplierType) {
  443. // 创建一个新的Binding对象,并设置变量
  444. Binding binding = new Binding(Utility.ofMap(
  445. "redisService", redisService,
  446. "esClient", esClient,
  447. "keysService", keysService,
  448. "systemService", systemService,
  449. "permissionService", permissionService,
  450. "tableIdService", tableIdService,
  451. "supplierService", this,
  452. //其它参数都好说,就这一堆dao,如果放在脚本里面初始化,没测试过,脚本里面初始化了,其它Service在初始化能否正常使用,
  453. //暂时先放在java里面初始化dao并都传递过去
  454. "systemDao", systemDao,
  455. "docDao", docDao,
  456. "mallDao", mallDao,
  457. "designDao", designDao,
  458. "jsonConvert", jsonConvert,
  459. "dataSourceId", dataSourceId,
  460. "supplierCode", supplierCode,
  461. "runEnvironment", runEnvironment,
  462. "resetClientInfo", resetClientInfo,
  463. "sysRunMode", sysRunMode,
  464. "appHome", appHome,
  465. "bucketFileRoot", bucketFileRoot,
  466. "logger", logger,
  467. "materialStockShards", materialStockShards,
  468. "resetAccountTradeBill", resetAccountTradeBill,
  469. "resetPaperInfo", resetPaperInfo
  470. ));
  471. //首先初始化公共代码
  472. String initCommonScriptRoot = appHome + File.separator +
  473. "conf" + File.separator +
  474. "script" + File.separator +
  475. 0 + File.separator +
  476. initScriptPathName;
  477. initSupplier(dataSourceId, supplierCode, initCommonScriptRoot, binding);
  478. // 指定Groovy脚本的位置
  479. // String initScriptRoot = appHome + File.separator +
  480. // "conf" + File.separator +
  481. // "script" + File.separator +
  482. // supplierCode + File.separator +
  483. // initScriptPathName;
  484. //
  485. // initSupplier(dataSourceId, supplierCode, initScriptRoot, binding);
  486. }
  487. /**
  488. * 增加业务脚本
  489. *
  490. * @param businessScript 脚本
  491. * @param dataSourceId 分库
  492. * @param supplierCode 分表
  493. */
  494. public void addBusinessScriptLocal(BusinessScript businessScript, String dataSourceId, long supplierCode) {
  495. if (redisService.hexists(keysService.getRedisKey(RedisKeys.KEY_BUSINESSSCRIPT, supplierCode), businessScript.getBusinessCode())) {
  496. return;
  497. }
  498. businessScript.setId(tableIdService.getTableIdMulti("tbBusinessScript.id", 1, dataSourceId, String.valueOf(supplierCode)));
  499. businessScript.setVoidFlag(0);
  500. BusinessScript.create(businessScript, 0L);
  501. redisService.hset(keysService.getRedisKey(RedisKeys.KEY_BUSINESSSCRIPT, supplierCode), businessScript.getBusinessCode(), jsonConvert.convertTo(businessScript));
  502. try {
  503. docDao.addBusinessScript(businessScript, dataSourceId, supplierCode);
  504. } catch (Exception e) {
  505. logger.error(e.getMessage(), e);
  506. }
  507. }
  508. public void addSupplier(DataBaseMultiItemEx dataItem) {
  509. if (sysDataBaseItem.containsKey(dataItem.getDataBaseAlias())) {
  510. return;
  511. }
  512. sysDataBaseItem.put(dataItem.getDataBaseAlias(), dataItem.getShardingKey());
  513. //初始化
  514. initSupplier(dataItem.getDataBaseAlias(), Long.parseLong(dataItem.getShardingKey()), dataItem.getItemType());
  515. }
  516. /**
  517. * 增加商户
  518. *
  519. * @param dataSourceId 数据库id
  520. * @param supplierCode 商户code,对应商户id即可
  521. * @param supplierType 商户类别,如印刷厂,外协厂等
  522. * @param driverClassName 数据库连接驱动名称
  523. * @param dataConnStr 数据库连接字符串
  524. * @param userName 数据库登录账号
  525. * @param password 数据库密码
  526. * @return 操作信息
  527. */
  528. @SuppressWarnings({"unchecked", "rawtypes"})
  529. public RMap addSupplier(String dataSourceId, String supplierCode, int supplierType,
  530. String driverClassName, String dataConnStr,
  531. String userName, String password) {
  532. if (sysDataBaseItem.containsKey(dataSourceId)) {
  533. return RMapUtils.error(HttpCode.CONFLICT.value(), "数据源已经存在");
  534. }
  535. sysDataBaseItem.put(dataSourceId, supplierCode);
  536. RMap dataParams = new RMap();
  537. dataParams.put("dataBaseAlias", dataSourceId);
  538. dataParams.put("shardingKey", supplierCode);
  539. dataParams.put("supplierType", supplierType);
  540. try {
  541. //写入主表
  542. DB.update("insert into sys_DataBaseMultiItem (dataBaseAlias, shardingKey, itemType) values(#{dataBaseAlias}, #{shardingKey}, #{supplierType})", dataParams);
  543. //增加数据源
  544. dbService.addDataSource(dataSourceId, driverClassName, dataConnStr, userName, password);
  545. //初始化
  546. initSupplier(dataSourceId, Long.parseLong(supplierCode), supplierType);
  547. } catch (DBException e) {
  548. logger.error(e.getMessage(), e);
  549. }
  550. return RMapUtils.success();
  551. }
  552. @SuppressWarnings({"rawtypes", "unused"})
  553. public RMap updateSupplierDataBase() {
  554. //todo
  555. return null;
  556. }
  557. /**
  558. * 获取某个supplier对应的es全部索引
  559. *
  560. * @param supplierCode 分表
  561. * @param esKey key
  562. * @return key
  563. */
  564. public String[] getDateYearESIndexArray(long supplierCode, String esKey) {
  565. if (bookSplitKind == 1) {
  566. return new String[]{keysService.getESKey(esKey, supplierCode) + "_current"};
  567. } else {
  568. return esIndexMap.computeIfAbsent(supplierCode + "_" + esKey, (k) -> IntStream.rangeClosed(bookStartYear, bookEndYear).mapToObj((x) -> keysService.getESKey(esKey, supplierCode) + "_" + x).toArray(String[]::new));
  569. }
  570. }
  571. /**
  572. * 获取某个supplier对应的es单个年份索引
  573. *
  574. * @param supplierCode 分表
  575. * @param esKey key
  576. * @param dataYear 年份
  577. * @return es索引
  578. */
  579. public String getDateYearESIndex(long supplierCode, String esKey, int dataYear) {
  580. if (bookSplitKind == 1) {
  581. return keysService.getESKey(esKey, supplierCode) + "_current";
  582. } else {
  583. return keysService.getESKey(esKey, supplierCode) + "_" + dataYear;
  584. }
  585. }
  586. /**
  587. * 获取账套列表,维护使用
  588. *
  589. * @return KeyValuePair的key为dataSourceId,value为supplierCode
  590. */
  591. public List<KeyValuePair> getDataBaseItemList() {
  592. return sysDataBaseItem.entrySet().stream().map((x) -> new KeyValuePair(x.getKey(), x.getValue())).collect(Collectors.toList());
  593. }
  594. /**
  595. * 从supplierCode获取数据源
  596. * 分表一般是唯一的
  597. *
  598. * @param supplierCode 分表
  599. * @return dataSourceId
  600. */
  601. public String getDataSourceIdFromSupplierCode(long supplierCode) {
  602. return sysDataBaseItem.entrySet().stream()
  603. .filter(entry -> entry.getValue().equalsIgnoreCase(String.valueOf(supplierCode)))
  604. .map(Map.Entry::getKey)
  605. .findFirst()
  606. .orElse("");
  607. }
  608. /**
  609. * 业务数据结转
  610. * 结转主要过程为
  611. * 1、查找上一期结束时间
  612. * 2、根据当期结束时间
  613. * 3、创建对应的业务成套数据表
  614. * 4、数据库中在新表中插入数据,并在旧表中删除数据(有个问题需考虑,实际测试mysql的delete速度非常慢,需要解决)
  615. * 5、从es中删除结转期对应的业务数据
  616. *
  617. * @param dataSourceId 分库
  618. * @param supplierCode 分表
  619. * @return 操作信息
  620. */
  621. @SuppressWarnings({"rawtypes", "unchecked", "unused"})
  622. public RetResult<BusinessBookPeriod> closedPeriodSupplier(String closedDateTime, ERPTokenUser currentUser, String dataSourceId, long supplierCode) {
  623. return handleScript("ClosedPeriod", ERPModule.SYSTEM, dataSourceId, supplierCode,
  624. () -> {
  625. RMap params = new RMap();
  626. params.put("closedDateTime", closedDateTime);
  627. return ProcessMapItem.newBuilder()
  628. .itemData(params)
  629. .currentUser(currentUser)
  630. .dataSourceId(dataSourceId)
  631. .supplierCode(supplierCode)
  632. .build();
  633. });
  634. }
  635. /**
  636. * 获取下载根地址
  637. *
  638. * @param dataSourceId 分库
  639. * @param supplierCode 分表
  640. * @return 下载根目录地址
  641. */
  642. public String getDownloadRootUrl(String dataSourceId, long supplierCode) {
  643. ConfigValue downUrlConfig = configService.getRedisConfigValue("OrderDownloadServer", dataSourceId, supplierCode);
  644. return (downUrlConfig == null) ? downloadUrl : downUrlConfig.getConfigValue1();
  645. }
  646. public String getWebDownloadRootUrl(String dataSourceId, long supplierCode) {
  647. ConfigValue downUrlConfig = configService.getRedisConfigValue("OrderDownloadServer", dataSourceId, supplierCode);
  648. return (downUrlConfig == null) ? outDownloadUrl : downUrlConfig.getConfigValue2();
  649. }
  650. /**
  651. * 获取文件内网下载地址
  652. * 由于直接存入拼接好的下载地址不利于后期修改地址,而且存储字段过长,改为实时获取下载地址
  653. * 下面链接未做跨域处理,如需跨域,可参考 <a href="https://note.youdao.com/s/3WoWIIj3">...</a>
  654. *
  655. * @param bucketFileName 文件名,包含目录,比如 productImage/abc.jpg
  656. * @param srcFileName 原文件名,比如订单的文件名,用于下载文件时可以更方便的命名
  657. * @param dataSourceId 分库
  658. * @param supplierCode 分表
  659. * @return 下载地址
  660. */
  661. public String getFileDownloadUrl(String bucketFileName, String srcFileName, String dataSourceId, long supplierCode) {
  662. if (StringUtils.isEmpty(bucketFileName)) {
  663. return "";
  664. }
  665. ConfigValue downUrlConfig = configService.getRedisConfigValue("OrderDownloadServer", dataSourceId, supplierCode);
  666. String httpUrl = (downUrlConfig == null) ? downloadUrl : downUrlConfig.getConfigValue1();
  667. String url = httpUrl + supplierCode + "/" +
  668. bucketFileName;
  669. if (StringUtils.isNotBlank(srcFileName)) {
  670. url = url + "?filename=" + URLEncoder.encode(srcFileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
  671. }
  672. return url;
  673. }
  674. /**
  675. * 获取文件外网下载地址
  676. * 由于直接存入拼接好的下载地址不利于后期修改地址,而且存储字段过长,改为实时获取下载地址
  677. *
  678. * @param bucketFileName 文件名,包含目录,比如 productImage/abc.jpg
  679. * @param srcFileName 原文件名,比如订单的文件名,用于下载文件时可以更方便的命名
  680. * @param dataSourceId 分库
  681. * @param supplierCode 分表
  682. * @return 下载地址
  683. */
  684. public String getFileWebDownloadUrl(String bucketFileName, String srcFileName, String dataSourceId, long supplierCode) {
  685. if (StringUtils.isEmpty(bucketFileName)) {
  686. return "";
  687. }
  688. ConfigValue downUrlConfig = configService.getRedisConfigValue("OrderDownloadServer", dataSourceId, supplierCode);
  689. String httpUrl = (downUrlConfig == null) ? downloadUrl : downUrlConfig.getConfigValue2();
  690. String url = httpUrl + supplierCode + "/" +
  691. bucketFileName;
  692. if (StringUtils.isNotBlank(srcFileName)) {
  693. url = url + "?filename=" + URLEncoder.encode(srcFileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
  694. }
  695. return url;
  696. }
  697. /**
  698. * 获取订单文件内网下载地址
  699. *
  700. * @param bucketFileName 文件名,包含目录,比如 productImage/abc.jpg
  701. * @param dataSourceId 分库
  702. * @param supplierCode 分表
  703. * @return 下载地址
  704. */
  705. public String getOrderFileDownloadUrl(String bucketFileName, String dataSourceId, long supplierCode) {
  706. if (StringUtils.isEmpty(bucketFileName)) {
  707. return "";
  708. }
  709. ConfigValue downUrlConfig = configService.getRedisConfigValue("ERPOrderDownloadCenter", dataSourceId, supplierCode);
  710. String httpUrl = (downUrlConfig == null) ? downloadUrl : downUrlConfig.getConfigValue1();
  711. return httpUrl + "oofile/" + bucketFileName;
  712. }
  713. /**
  714. * 获取订单文件外网下载地址
  715. *
  716. * @param bucketFileName 文件名,包含目录,比如 productImage/abc.jpg
  717. * @param dataSourceId 分库
  718. * @param supplierCode 分表
  719. * @return 下载地址
  720. */
  721. public String getOrderFileWebDownloadUrl(String bucketFileName, String dataSourceId, long supplierCode) {
  722. if (StringUtils.isEmpty(bucketFileName)) {
  723. return "";
  724. }
  725. ConfigValue downUrlConfig = configService.getRedisConfigValue("ERPOrderDownloadCenter", dataSourceId, supplierCode);
  726. String httpUrl = (downUrlConfig == null) ? downloadUrl : downUrlConfig.getConfigValue2();
  727. return httpUrl + "oofile/" + bucketFileName;
  728. }
  729. public String getRunEnvironment() {
  730. return runEnvironment;
  731. }
  732. }