# [Product] 公司架構 [toc] ## 主要產品線 ### :small_blue_diamond: In-Factory: 智慧工廠,工業4.0: ### :small_blue_diamond: In-Park: 智慧園區,物聯網管理、控制、節能等等: ### :small_blue_diamond: 根據場域(例如基隆港、高雄路燈)不同會衍生出類似專案的產品: ## 程式架構 ### 1. ApiGateway - 網關,負責轉發、反向代理、重寫URL等等 - 啟動在80 port,pull下來預設好像在8080 port,如果你起了發現都沒轉導檢查一下 ### 2. Auth-center - 處理登入權限驗證,使用oauth2,發Token給使用者 - 通常是比較新的專案(例如手機APP、MCV2)才會用到 - 啟動在port 9999 ``` server: port: 9999 servlet: context-path: /auth ``` ### 3. CCS - CServer(Device 端:設備跟通訊伺服器互動) - 架構是 裝置 - 閘道器 - CS層 - 平台 ![Snipaste_2022-03-01_11-05-49](https://hackmd.io/_uploads/B12U8lSqa.png) - CS層使用三種傳輸協定 - http - socket (以前用的),也是基於TCP/IP協議,但通常是長連線 - mqtt,透過Broker作為中間件,一種消息訂閱的連線模式,常用於IOT - 這邊的相關服務好像是開在8883 port (MQTT 預設埠為 1883。加密的埠為 8883) - CS層也會將Raw Data直接寫進DB - 他的包叫CServerEms,主要由鐘漁管的,如果需要什麼新功能,請開單 ![image-20221024095756643](https://hackmd.io/_uploads/BkZF8gBcT.png) ### 3. CSTest - 裝置的CS單元測試都在這邊 - 比如我想要手動加入一個裝置(INC4的虛擬gateway),要加在# t-infactory測試機,首先去改CsServerIp=192.168.10.216 ![image-20220624084850953](https://hackmd.io/_uploads/BkYhLxHcT.png) - 然後DB也要對應(因為要去操作devicelist表) ![image-20220624084923123](https://hackmd.io/_uploads/S1MTUgH5a.png) - 接著隨便挑一個裝置種類相近的TEST來改,然後執行單元測試,就可以加入裝置了 ```java package testing.devices.in29; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; import java.io.IOException; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import com.insnergy.db.device.DeviceData; import com.insnergy.db.query.SimpleCriteria; import com.insynerger.def.device.attribute.AttributeIDs; import com.insynerger.def.device.attribute.value.DeviceModel; import core.IgwTestScript; import core.IgwTestScript.Params; import core.db.connector.DBConnector; import core.def.NetType; import core.message.igw.IgwMsg; import core.message.igw.UnknownReq; import core.message.igw.impl.b.B02; import core.message.igw.impl.s.S10; import core.message.igw.impl.s.S28; import core.message.igw.impl.s.S31; import core.message.igw.impl.s.S34; import core.message.sc.impl.BinarySwitch1; import core.scripter.IgwMsgScripter; import core.scripter.Scripter; import core.scripter.SystemCommandScripter; import core.utils.StringUtil; import testing.VirtualGateway; /** * * @see Requirement: * <a href="https://redmine.insynerger.com/issues/9130">CS-9130</a> * */ @ContextConfiguration({ "/db-config.xml" }) public class IN2922Test extends DBConnector { @Autowired private String csIp; @Autowired private int igwPort; @Autowired private int scPort; private String GATEWAY_ID = "INC4--DAPENGBAY---01"; private String GATEWAY_PW = "abc123"; private DeviceModel GATEWAY_MODEL = DeviceModel.IIC4; private String DEVICE_ID = "IN2923DAPENGBAY---01"; private String EXT_TYPE = "2923"; private String NET_TYPE = NetType.RS485.code(); IgwTestScript script; @Before public void setUp() throws Exception { getDataAccessor().write(DeviceData.of(GATEWAY_ID).with(GATEWAY_MODEL)); script = new IgwTestScript(); Params params = script.new Params(); params.csIp = csIp; params.igwPort = igwPort; params.scPort = scPort; params.gateway = new VirtualGateway(GATEWAY_ID, GATEWAY_PW); script.initParams(params); } @After public void tearDown() throws Exception { script.finishTesting(); //getDBUtil().deleteController(DEVICE_ID); //getDBUtil().deleteGateway(GATEWAY_ID); } @Test public void test() throws Exception { Params params = script.getParams(); List<Scripter> scenario = params.scenario; scenario.add(reportDevice(params)); scenario.add(reportSpec(params)); scenario.add(reportConnection(params)); //scenario.add(reportEvent(params)); //scenario.add(testBinarySwitch1(params)); script.doDeviceTest(); } private IgwMsgScripter reportDevice(Params params) { return new IgwMsgScripter(params) { @Override public void doSimulation() throws IOException { S31 s31 = new S31(GATEWAY_ID, DEVICE_ID, NET_TYPE, EXT_TYPE); assertThat(resolveIgwResult(getSender().send(s31.getContent())), is("0")); super.pause(1); validateData(); } private void validateData() { DeviceData data = getDataAccessor().read(SimpleCriteria.of(DEVICE_ID)); assertThat(data.get(AttributeIDs.of(620500L)).getValue(), is(NET_TYPE)); } }; } private IgwMsgScripter reportSpec(Params params) { return new IgwMsgScripter(params) { @Override public void doSimulation() throws IOException { S34 s34 = new S34(GATEWAY_ID, DEVICE_ID); s34.addAttr("600200", StringUtil.encodeUtf8ToHexString("NEC")); assertThat(resolveIgwResult(getSender().send(s34.getContent())), is("0")); super.pause(1); validateData(); } private void validateData() { DeviceData data = getDataAccessor().read(SimpleCriteria.of(DEVICE_ID)); assertThat(data.get(AttributeIDs.of(600200L)).getValue(), is("NEC")); } }; } private IgwMsgScripter reportConnection(Params params) { return new IgwMsgScripter(params) { @Override public void doSimulation() throws IOException { S28 s28 = new S28(GATEWAY_ID, DEVICE_ID); assertThat(resolveIgwResult(getSender().send(s28.getContent())), is("0")); super.pause(1); validateData(); } private void validateData() { DeviceData data = getDataAccessor().read(SimpleCriteria.of(DEVICE_ID)); assertThat(data.get(AttributeIDs.of(100900L)).getValue(), is("1")); } }; } private IgwMsgScripter reportEvent(Params params) { return new IgwMsgScripter(params) { @Override public void doSimulation() throws IOException { S10 s10 = new S10(GATEWAY_ID, DEVICE_ID); s10.setEvent("11", "100020102", "0"); s10.setReportTime(getTimestamp()); assertThat(resolveIgwResult(getSender().send(s10.getContent())), is("0")); super.pause(1); validateData(100020102L, "0"); s10.setEvent("11", "100020102", "1"); s10.setReportTime(getTimestamp()); assertThat(resolveIgwResult(getSender().send(s10.getContent())), is("0")); super.pause(1); validateData(100020102L, "1"); } private void validateData(Long attrId, String expected) { DeviceData data = getDataAccessor().read(SimpleCriteria.of(DEVICE_ID)); assertThat(data.get(AttributeIDs.of(attrId)).getValue(), is(expected)); } }; } private SystemCommandScripter testBinarySwitch1(Params params) { return new SystemCommandScripter(params) { @Override public String processReq(String msg) { String[] msgSplit = msg.split(";"); IgwMsg igwMsg; if (msgSplit[1].equals("B02")) { igwMsg = new B02(msgSplit); } else { igwMsg = new UnknownReq(msgSplit); } return igwMsg.getContent(); } @Override protected void executeScript() throws Exception { BinarySwitch1 cmd = new BinarySwitch1(GATEWAY_ID, DEVICE_ID); cmd.setScValue(BinarySwitch1.PowerStatusValue.OFF); assertThat(resolveScResult(getSender().send(cmd.getWriteReq())), is("0")); super.pause(1); } }; } } ``` ### CS IP或port是null - 在加入裝置的時候,有可能添加的屬性不完全,導致調用sendSystemCommand的時候報錯 - 要去找那個裝置對應的profile表,查出他缺少的屬性,例如我新增裝置的時候沒加到CS IP跟port,先找到他的devicelistid=30020743,拿devicelistid去profile表查 - 根據定義查看attrid,例如620300就是CS IP ![image-20220629142936439](https://hackmd.io/_uploads/S1z5PgB9p.png) - 手動把他改正確,例如台北辦公室的測試環境CS IP=192.168.10.216,port=8500 ![2022-06-29_14-07-24](https://hackmd.io/_uploads/SJBsPgr5p.png) - 虛擬gateway是要設定621100跟621200,注意port有區別cs port=8500 (這個是web服務走的,還會再轉一次) - csScCmd port是8900 (這個是實際指令最後真正去的地方) ![image-20220708164749532](https://hackmd.io/_uploads/HkopvgrcT.png) ### 4. HTTP增加裝置 溝通CS也可以用HTTP,例如想手動觸發一筆裝置回報的資料,參考 https://docs.google.com/document/d/1QnzDL1ZLIiM6kc9YZIO_BQRUQo5IG9qhN2q7eQ8uTMw/edit ![image-20220624085825793](https://hackmd.io/_uploads/SyG4ugB56.png) https://t-infactory.insynerger.com/cs/device/data ```json { "gatewayId": "INC4-DEV-LIGHT------", "requestSn": "sn_{{now}}", "gatewayPw": "YWJjMTIz", "deviceList": [ { "deviceId": "IIL5-DEV-LIGHT----01", "parentId": "INC4-DEV-LIGHT------", "typeId": "L5", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "101000", "value": "1" } ] } }, { "deviceId": "IIL5-DEV-LIGHT----02", "parentId": "INC4-DEV-LIGHT------", "typeId": "L5", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "101000", "value": "0" } ] } }, { "deviceId": "IIL5-DEV-LIGHT----03", "parentId": "INC4-DEV-LIGHT------", "typeId": "L5", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "101000", "value": "1" } ] } }, { "deviceId": "IIL5-DEV-LIGHT----06", "parentId": "INC4-DEV-LIGHT------", "typeId": "L5", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "101000", "value": "0" } ] } }, { "deviceId": "IIL5-DEV-LIGHT----08", "parentId": "INC4-DEV-LIGHT------", "typeId": "L5", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "101000", "value": "1" } ] } } ] } ``` ### 5. data-definition-api - 定義設備的屬性(例如arrtid)、建表,屬於架構層面的API - 首先到裝置整合列表可以看到大分類,DB裡面幾乎都有對應的表(大分類),然後根據attrid找到確切的那個功能 ![image-20220311090734452](https://hackmd.io/_uploads/Ski8FgHqT.png) - devicelist是總表,他有一個ext是補充說明 - profile跟event是週期的紀錄或事件的紀錄 ![image-20220318090232389](https://hackmd.io/_uploads/B1sFtxH5a.png) ### 6. device - listid是流水號主鍵,等於其他子表的devicelistid,相當於身分證號 - deviceid乍看很像,不要搞混,deviceid是具體的一個物品(例如兩個一樣的產品,我為了區分編號1編號2,就有2個deviceid),deviceid就是實際給人看的,相當於綽號(但是不會重複) ![image-20220318093534218](https://hackmd.io/_uploads/BJu15gS96.png) - attr是某種屬性(功能) - 容易混淆的點在於,比如有一個"溫溼度感應計",溫度是一個attrid、濕度又是一個attrid,所以就會有兩筆profileid - 當有了attrId卻不清楚他具體意義時,直接去雲端硬碟搜尋 ![image-20220318093333795](https://hackmd.io/_uploads/S1cE9lrca.png) ![image-20220318093209504](https://hackmd.io/_uploads/SkEHcgSc6.png) - 當有devicelistid跟屬性代碼attrid時就能取得一個唯一辨識 - 週期的回報就是就是profileid - 事件(長的)就會產生eventid ![image-20220318113644752](https://hackmd.io/_uploads/By7BlVv5a.png) ### 7. profile - 可以說device是設備、attr是某種屬性(功能)、profile是"某個設備的某個功能的最新的那筆週期回報紀錄" ![image-20220318090700321](https://hackmd.io/_uploads/By_Pg4vqT.png) - profile中有個attrtype用來表示紀錄的類型,2表示是數字類型(歷史就會記錄在sensornumberinfo),3表示文字類型(就會記錄在sensorstringinfo),1是只會記錄最新的在profile表,例如line或email通知完就沒了,而不寫進歷史(其他專門的表) - 所謂"寫進歷史"根據應用處理那邊的勾選,決定要記錄即時或歷史 - 即時就是寫在profile表 - 歷史就是寫在sensornumberinfo表,如果2種都有勾,gateway收集到裝置的紀錄給C server後會去兩個表都寫入 - 在profile表更新最新這一筆,同時也寫入xxxinfo表(看他是number或string類型) ![image-20220311091216925](https://hackmd.io/_uploads/rk55g4PcT.png) - sensornumberinfo表的歷史紀錄,value就是實際的值(數字類型) ![image-20220317180308173](https://hackmd.io/_uploads/S1XaeEPqT.png) ### 7. event - 可以說device是設備、attr是某種屬性(功能)、profile是"某個設備的某個功能的最新的那筆事件紀錄" - event的attrid通常比較長(位數多) - event沒有attrtype,統一用一個value(text)來存事件,就算是數字也會存成文字,相關的定義一樣要去表看 ![image-20220318114824236](https://hackmd.io/_uploads/rJkyb4Pqp.png) - 同理,最新的那筆存在xxxevent表本身,歷史紀錄存在xxxeventinfo - 因為不分數值類型,所以不會有eventnumberinfo這種東西 - 同理,看到numberinfo或string就知道他必定是來自profile,週期回報 - 最後還有一種表是sensorstatisticinfo,是統計的,很少用到暫時不用管 ### 8. Database相關 - 從`application.yml`找到專案對應連線的DB資訊 - 通常IP 192.168.10.215,這種xxx.215的表示開發測試環境 - 表名大部分是由是設備縮寫開頭,如acxxx是空調的、crtlxxx是控制器 - 表名帶有statistic的,表示統計模組已經分析過的資料,例如: lightstatisticinfo ### 9. ems-api - 所有操作DB都透過它,裡面為了hibernate3、5版的支援問題所以會看到2個分支切來切去 ![image-20220301114440914](https://hackmd.io/_uploads/r1J55BPcT.png) - 這是一個lib,裡面已經封裝好了去DB撈對應資料的方法 - 比如我有devicedId、attrId、時間區間就可以直接拿出歷史紀錄`List<History>`(就是去撈sensornumberinfo這種) - ![image-20220317180234733](https://hackmd.io/_uploads/ryU2qSvcT.png) ### 10. SurveillanceService - 排程服務,在維管中心叫IVS,定時去執行一些東西 ### 11. InFactory - 主要的WEB服務都在這 - 由Group這個抽象層來區分權限 - 又分成地點與建物 - 裝置(device)必須依附於建物之下 ### 12. InEcCommon - InFactory的一些通用項目,只是個library不用啟動服務 - 注意主分支是hyec而非master ### 13. InEcMessages - InFactory的i18n部分,提供多國語言 ![image-20220301115046925](https://hackmd.io/_uploads/Syr6qHwq6.png) - .properties是給spring:message讀的ResourceBundle - 用搜尋檔案會發現有超多重複(很多子模組各有一個),屬於歷史遺留,一般用common的那個就好 ![image-20220301170706572](https://hackmd.io/_uploads/H1cE3SPqa.png) - 也有.json或.js的是給其他前端用的,總之...大雜燴 ### 14. InEms1.0.0 - InFactory的主服務API - 主分支是iniot,而非master - 啟動前需要手動在本地C:\複製一份appdata的資料 ![image-20220307115016938](https://hackmd.io/_uploads/BydI2Svcp.png) ![image-20220301115310986](https://hackmd.io/_uploads/rkUu3Bwc6.png) - 測試帳號: insdev - 密碼: insdev1234 - JSP頁面鑲嵌是在main.layout - 關掉groupTree可以加速啟動 ![Snipaste_2022-04-06_17-26-17](https://hackmd.io/_uploads/B18yarwq6.png) - 登入登很久的話,可以用權限小一點的帳號(下方說明流程) ### 15. InFactoryService - InFactory的導航列部分,啟動在5050 ![image-20220301115628264](https://hackmd.io/_uploads/By-4SvwcT.png) - 如果啟動報錯 CServerFacade循環依賴,先關掉CCS跟data-definition ![image-20220307113924768](https://hackmd.io/_uploads/S1iVSwP9T.png) ### 16. 增加功能到麵包屑 - 在in_functionmenu加入這個功能 - i18n也要加 ![image-20220713123232597](https://hackmd.io/_uploads/r1sHBPP9p.png) - 到in_rolefunction表 - 指派那些role可以控制這個功能,至少要指派給1超級管理員 ![image-20220713123250335](https://hackmd.io/_uploads/rym8HDDq6.png) - 到`https://t-infactory.insynerger.com/ec1503.do` - 選場域、新增角色,指派function權限 - `https://t-infactory.insynerger.com/ec1502.do` - 選場域、新增帳號,指派角色 ### 17. InPark - 一樣有Group這個抽象層來區分權限 - 但是在InPark沒有細分地區與建物,都可以有device - 啟動流程 1. git clone http://git.insynerger.com/esp/InPark 2. 下載 gradle plugin buildship Gradle (綠色大象) 3. 進eclipse 專案右鍵 > configure > add gradle nature 4. 專案右鍵 > Gradle > Refresh Gradle Project 5. Gradle build 6. 產生build/generated/source/apt/main 目錄 (因有使用到mapstruct,需先產生mapper類) 7. 再一次專案右鍵 > Gradle > Refresh Gradle Project 8. 打包 gradle bootRepackage 9. 若啟動專案時發生類似xxxxMapper is not found等錯誤訊息,請先於Gradle Tasks(安裝gradle plugin buildship Gradle後可叫出,或是STS內建)視窗,InPark>build>build 右鍵Run Gradle Tasks 將相關wapper build出來後,在執行第4點refresh即可正常執行 10. ![2022-08-15-17-02-12-image](https://hackmd.io/_uploads/HkciHPv56.png) - mapper生成後,專案的設定應該長這樣 ![2022-08-15-17-04-09-image](https://hackmd.io/_uploads/Syz2Bvvq6.png) - 如果還是有問題,檢查Run/Debug設定 ![2022-08-15-17-05-13-image](https://hackmd.io/_uploads/SJKhHvv5a.png) - 主服務,啟動後訪問 `http://localhost:8085/` ![image-20220225122625318](https://hackmd.io/_uploads/Sy30HDw96.png) - 接下來需要獲取登入的帳號密碼,回到專案從resources找到application.yml - profiles.active: 表示當前選中的啟動設定 - 慣例是使用T開頭的表示測試環境 - include則是都會包含的,相當於基底的意思 ![image-20220225121329477](https://hackmd.io/_uploads/S1ZeUwPqT.png) - 選到`application-tinpark.yml` - 可以看到其中的DB連線資訊 ![image-20220225122335149](https://hackmd.io/_uploads/SJeZUvD56.png) - 使用DBeaver連線,例如想獲取登入帳號資訊就搜尋userinfo表 ![image-20220225122445507](https://hackmd.io/_uploads/HJcbIDD56.png) - SQL debug想印出查詢的語句,更改`logback-spring.xml` ![image-20221116095900319](https://hackmd.io/_uploads/HySz8wDqp.png) - 順便一提,在InPark新增Entity要去`hibernate.cfg.xml`增加Mapping,否則查找半天都會是空的 ![image-20221116100104790](https://hackmd.io/_uploads/HkBXIvDcp.png) ### 18. inpark-api-service - 提供 In-Park 相關API - 他的application.yml也是放在/test之下,所以需要手動指定 - 對專案右鍵>Properties>Java Build Path>Source>把resources資料夾直接link到test的對應位置 ![2022-08-01-11-22-42-image](https://hackmd.io/_uploads/SyBNUwwc6.png) - InsynergerCommonAPI - 負責InPark與InFactory共用的API - 它的設定檔是放在test目錄下,但是有巨坑,改這個active後面的方案沒用,run在本地要改另一個地方 ![image-20220506155358147](https://hackmd.io/_uploads/BkgH8wPqa.png) - 要改這個ICA - STS - Run.launch,這邊的優先級>application.yml ![image-20220506160559770](https://hackmd.io/_uploads/SyjS8DP96.png) - 啟動在port 6060 - 啟動時報錯airScheduleController nest問題,要關掉data-definition-api - 大概是有重複名字的.java造成的 ![image-20220301121128030](https://hackmd.io/_uploads/S1mvUDwqT.png) ### 19. API驗證token - 這邊的Controller分為inner(不用驗證)跟auth(要去auth換token)的, ![2022-08-25-16-20-25-image](https://hackmd.io/_uploads/HJB_Lwv5T.png) - token其實就是用InFactory的使用者的帳號密碼做參數去換,舉例來說請求長這樣 - 幾個參數都是固定的,只有username跟password要換成InFactory的使用者的帳號密碼,並且注意密碼是純MD5(32位小寫)算過的值 - 驗證要問的url寫在 ![2022-08-25-16-24-07-image](https://hackmd.io/_uploads/Hk3FLDP56.png) - 每個環境都有自己的auth server,交換之後就寫在那個環境的auth db,過一段時間後就被刪掉(過期) ![2022-08-25-16-33-17-image](https://hackmd.io/_uploads/HymcUPwqp.png) - 帶著換到的token就能訪問auth的Api ![2022-08-25-16-42-07-image](https://hackmd.io/_uploads/S1u58vwqa.png) ### 20. 啟動問題 #### Gradle的JDK - 因為STS4內建的JVM版本比較新(Eclipse他本身也是java寫的),需要手動指定JDK ![image-20220225102724582](https://hackmd.io/_uploads/H1OTUvD5p.png) - `Gradle wrapper`的意思是依照`build.gradle`腳本裡面指定的Gradle版本,若本機不存在就會直接聯網下載一個暫時的 ### 22. CreateProcess error=206 - 錯誤訊息: ``` CreateProcess error=206, 檔名或副檔名太長 ``` - 原因: Windows命令列支援的字串長度有限制。Linux不會有這個問題 - 解法: 參考官方文檔 https://plugins.gradle.org/plugin/ua.eshepelyuk.ManifestClasspath - 加入插件即可 ``` plugins { id "ua.eshepelyuk.ManifestClasspath" version "1.0.0" } ``` ### 23. Maven 3.8.1 http - 錯誤訊息: ``` Since Maven 3.8.1 http repositories are blocked. ``` - 原因: Maven新版本自己加戲禁用非https連線 - 解法: 找到Maven設定檔把它改了,例如IDEA內嵌的在.\IntelliJ IDEA Community Edition 2021.3.2\plugins\maven\lib\maven3\conf\settings.xml ``` <mirror> <id>maven-default-http-blocker</id> <mirrorOf>external:http:*</mirrorOf> <name>Pseudo repository to mirror external repositories initially using HTTP.</name> <url>http://0.0.0.0/</url> <blocked>true</blocked> ``` ### 24. Choose Main Type - 當跳出這個,沒特別的,選App即可 ![image-20220225113056938](https://hackmd.io/_uploads/BJLEwvv5p.png) ### 25. 上版流程 #### 有腳本(Build Dists.launch)的 例如: InPark、Common Api - 到維管中心 https://mc.insynerger.com/auth/index - 需要先開通權限,選估狗雲端帳號登入(帳號密碼不用打) - 裡面看到可選的版本其實就是來自這台linux主機 ``` FTP 192.168.10.250 2100 port upload / Hk4g4hk4g4123 ``` - 上版之前,注意InPark的yml實際上板套用的是在projects資料夾下面的;下面的那些`application-keelungport.yml`這是方便本地測試用的,不要搞錯 ![image-20220408165542086](https://hackmd.io/_uploads/ryASDww56.png) - 這些yml的來龍去脈去是這樣,這些有腳本的,在build的時候會根據案場產出多個資料夾,每個裡面都把`customize-xxx.yml`重新命名成`customize.yml` ![image-20220905103628015](https://hackmd.io/_uploads/ByALDwD96.png) - 在https://mc.insynerger.com/執行更版的動作時,會做一個重新命名 ![image-20221005093237795](https://hackmd.io/_uploads/HJ3wDvPc6.png) - 但是真正上版丟到那台主機(例如某私雲)的時候,又會改成`application.yml` ![image-20220905104006250](https://hackmd.io/_uploads/HJEdPvvq6.png) - 它的組成是來自`customize.yml`,拼接成完整的`application.yml`(放在下半部),放到`/config`路徑之下。舉例來說,上到大鵬灣環境,真正吃的設定就是這兩項的組合 ![image-20221005093640699](https://hackmd.io/_uploads/r1wYDDDc6.png) - 使用寫好的.launch腳本Build整個War包 ![image-20220301171438892](https://hackmd.io/_uploads/Syt5PwDqp.png) - 然後會跳出彈窗,需要填入版本號(就是當前版本+1)與描述(就是redmine議題追蹤系統上的單號#12893之類的) - https://redmine.insynerger.com/ - 等待建構,最後會在專案的根目錄產生對應的資料夾,用FTP拉去對應的位置(ftp資料夾裡面找尋INPARK/release)即可 ![image-20220408164515594](https://hackmd.io/_uploads/rkvnvwD56.png) - 開啟維管登入(點擊雲端圖示)選擇環境執行換版作業 - 如果loading半天,有可能是FTP那個資料夾裡面項目太多,把舊的放到history ![image-20220330082801559](https://hackmd.io/_uploads/SJChDPD96.png) - 進行到97%會卡特別久,這是正常 ![image-20220408165410899](https://hackmd.io/_uploads/B1VTDPwq6.png) - 可以同時上多個專案的版(例如在大鵬灣環境同時更InPark+InEMS) - 但不能同時在多個地方上同一個專案(例如在測試機上inEMS又在大鵬灣上inEMS),會導致前一個被abort #### InEms(沒腳本的) 區別在於有沒有include: customize - 用gradle build,有三種方法,如果1、2都行不通,就用本地安裝的gradle下命令行去build - 1行不通的原因是gradle版本太新,具體表現是按了會看到快速地閃出錯誤然後又被洗掉,之後無事發生 ![image-20220322144745769](https://hackmd.io/_uploads/HJXRDwPqT.png) - 成功build會在跟目錄下/libs資料夾找到war ![image-20220322145413581](https://hackmd.io/_uploads/SytRDwwqp.png) - 在FTP上,InEMS這邊是用`版本號.場域`來區分 ![image-20220322145004238](https://hackmd.io/_uploads/ryO1OPv96.png) - 先把最新版本的資料夾複製一份,更改版本號 ![image-20220322145311722](https://hackmd.io/_uploads/HyJx_PDcT.png) - readme.txt就相當於用腳本更新到一半跳出來那個視窗,裡面填版本號跟說明 - 最後這些訊息會顯示到line上 ![image-20220322145541074](https://hackmd.io/_uploads/SydeOPw9T.png) - setting的部分,需要手動更改 - 最好的方法是直接把setting資料夾複製舊的過去 ![image-20220322145940733](https://hackmd.io/_uploads/B1eb_Dvc6.png) - 它是由例如application-DBNSA.yml的上半部分加customize.yml的設定組成 ![2022-08-04-08-59-42-image](https://hackmd.io/_uploads/HJKbOvDqp.png) - 最後去到服務主機內看只有一個app.yml,其實道理都一樣,真正放到各案場的都是從`customize.yml`拼接出來的 ![image-20220323104954773](https://hackmd.io/_uploads/S1wGuww96.png) - 上在t-infactory_inems的話,他後續啟動服務要很久(可以長到10分鐘左右) ### 26. 查看主機服務運行 - 用putty連到內網的該主機ip,然後輸入帳號密碼 ![image-20220323103835352](https://hackmd.io/_uploads/H1xQ_DD9T.png) ``` # 查看服務 ps aux|grep inpark # 開啟與關閉服務 systemctl start start_inpark systemctl start stop_inpark systemctl start start_inems systemctl start stop_inems # 查看port lsof -i -P | grep :8500 ``` - 各服務是放在/opt之下 ``` # 切換至/opt cd /opt # 查看樹狀目錄,3層 tree -L 3 ``` - 有要改的用vim開啟,修改完用:wq保存離開 (如果要改很多往下看用FTP copy出來改完再放回去更簡單) - 若服務開不起來,可以去看看啟動的log,進到對應的資料夾(例如/opt/INEMS/INEMS/)下 `sh run.sh`,就能直接spring啟動,若啟動有問題可以直觀的看到 ![image-20220323151319834](https://hackmd.io/_uploads/ryKHODw9a.png) ### 27. 查看Log - 舉例: 看inpark的 ``` /opt/alllogs/inpark/controller.log ``` - Log本身分成好幾種在存,`/opt/alllogs/inpark/`目錄下是這樣 ``` -rw-r--r--. 1 root root 1098380 5月 18 14:08 controller.log -rw-r--r--. 1 root root 722 5月 4 14:50 device-control.log -rw-r--r--. 1 root root 739208 3月 18 16:45 disconn.log -rw-r--r--. 1 root root 933 5月 17 18:11 event.log drwxr-xr-x. 13 root root 183 5月 17 18:11 history -rw-r--r--. 1 root root 148705 3月 18 16:45 lightFault.log -rw-r--r--. 1 root root 187467 3月 18 16:47 mq.log -rw-r--r--. 1 root root 161326 3月 18 16:45 powerAbnormal.log -rw-r--r--. 1 root root 31364 3月 18 16:39 schedule.log -rw-r--r--. 1 root root 1535 5月 18 12:06 security.log -rw-r--r--. 1 root root 0 3月 17 12:21 street-light-control.log -rw-r--r--. 1 root root 33230 5月 18 14:09 system.log -rw-r--r--. 1 root root 490 5月 17 21:22 tomcat.log ``` ### 28. FTP也可以圖形化的看log或改.yaml ``` 主機IP: 192.168.10.211 帳號: root 密碼: Hk4g4hk4g4123 連接埠: 22 ``` - 選信任 ![image-20220519115406692](https://hackmd.io/_uploads/ByMddwP9T.png) ![image-20220519115618143](https://hackmd.io/_uploads/Hyh__DPqT.png) ### 29. ELK查看log - http://192.168.10.221:5601/app/discover - 不是全部的主機都有上ELK,但測試環境幾個常用的都有 ![image-20220712101048769](https://hackmd.io/_uploads/r1OKuwDqp.png) ``` http://192.168.10.221:5601/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(columns:!(message),filters:!(),index:'199eaf80-ff09-11ea-b56c-4f03cdf2c180',interval:auto,query:(language:kuery,query:''),sort:!()) ``` ### 30. 議題單、臭蟲單 - 當單子派到自己身上,修正完之後 - 按右上角的編輯 - 狀態改為QA,填寫自測結果 ### 31. 遠端桌面連線 1. 先下載openvpn 軟體 (請用2.X版的 ,3.X版的可能無法使用),https://openvpn.net/community-downloads/ 2. 匯入ins401office.ovpn comp-lzo reneg-sec 0 cipher AES-256-CBC auth SHA512 auth-user-pass -----BEGIN CERTIFICATE----- MIIDdTCCAl2gAwIBAgIJAP/Jd/r39bHlMA0GCSqGSIb3DQEBCwUAMFExCzAJBgNV BAYTAlRXMQ8wDQYDVQQHDAZUYWlwZWkxFjAUBgNVBAoMDVN5bm9sb2d5IEluYy4x GTAXBgNVBAMMEFN5bm9sb2d5IEluYy4gQ0EwHhcNMTkwMTE0MDI1MTAyWhcNMzgx MDAxMDI1MTAyWjBRMQswCQYDVQQGEwJUVzEPMA0GA1UEBwwGVGFpcGVpMRYwFAYD VQQKDA1TeW5vbG9neSBJbmMuMRkwFwYDVQQDDBBTeW5vbG9neSBJbmMuIENBMIIB IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxjDnEFw8lGZWmBJGHUitH1Yl WH1Xg2MiJFuMF6SjDaY3TEetxaPQsrRm+xGBwKU0JhZs32XUTZiHkRMj85I1YZVY 9aGbImDKd1kfwgB95YGzHuB/QLdd+32CLmYdINjaJmV/1simPegXjHNUO9OhhaxQ 4sr7dyNQarM3LyFWZ6uziPkx93onjeSbBOIAavelVupFuFaBnH0C6iiuTzHQPVrc vr/bh91zuhsdq2Vr+F1nWcYrY2Fo/fI0xFIA5Yqllid2NEUMTspgOxPdaFDvsmuv RPoG5QEAlPYQvBWFFESbROcWK5tdzpGIz9HPY6HLwRp7lu4/w6dVIdfTHcvzkQID AQABo1AwTjAdBgNVHQ4EFgQUJCFeETuLPwioHEJEcVxozMZkftcwHwYDVR0jBBgw FoAUJCFeETuLPwioHEJEcVxozMZkftcwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B AQsFAAOCAQEAX9RXZiMAQsnNdB+arNOFe0ejb9IBzo9UFaOGshHP5HciZdndFbI3 FSbCVoMo0BWJ3roInCYHB1eUcpoWq8fBBU2NmQbpPjDlpvXt6HAFVw90SwSHz6Aq c4on3ny6qXhMXoDovoV+N+1pmZX/4pSFBTDxyql4jyE8sPsl94xKC+UeZ1zX0IWH yJXPlaO9DmROamIq1ymb8JO/D5byEMvScOILYQuA91DcvCGTdmS99oSvwdfTnthD gvCSsmuAQbsWo+0pAKC4Ac1po0ylXDldQ8rBLGDvKJ0SCTO5EzhZaFv1UjP2Q/D5 d7POtnVGF3MVH1i43khnjaCsTDuffjI58A== -----END CERTIFICATE----- ``` 3. 連上VPN之後,使用遠端連線,電腦=IP位置,不清楚要請辦公室的同事幫忙看或問東堅 4. 接著注意,如果是新員工的使用者名稱前面要加`INSYNERGER\` 3. 顯示要憑證點`是` ![image-20220905102256591](image-20220905102256591.png) ``` ### 32. Postman建立 - 參考文件: 佳典API.pdf https://docs.google.com/presentation/d/1zjbKZ16TNQjW1d_xz22NLdULAQ_LwuXz0rgYc65fRyo/edit#slide=id.g1739527d92a_0_367 1. 下載Postman。https://www.postman.com/downloads/ 3. 進行身分驗證。 (1)請輸入Headers,key:Authorization、 value: (2)輸入POST請求網址:請洽佑誠,索取相關文件,文件第8頁下方,填入後會得到相關參數(左圖),可直接send後,取得結果(右圖),主要使用`access_token`進入步驟2.。 2.取得佳典資訊。 (1)輸入GET請求網址:https://t-infactory.insynerger.com/api/incommon/v2/users/me/ (2)填入key:Authorization、value:Bearer +步驟1.(2)取得的`access_token`,再send。 (3)成功讀取畫面,主要使用`groundId`進入步驟3。 3.取得groups清單 (1)輸入GET請求網址:https://t-infactory.insynerger.com/api/incommon/v2/groups/{groundId}/ (2)填入key:Authorization、value:Bearer +步驟1. (3)取得的`access_token`,再send。 (4)成功讀取畫面。 - 帳號建立: - http://localhost/ec1503.do進行角色管理 - http://localhost/ec1502.do了解使用者清單 - http://localhost/ec1501.do場域管理(新增單位(ex.某公司某廠區)、新增建物(ex.末端偵測儀器)) - 執行環境: - ApiGateWay:netty。 - InEms1.0.0:Tomcat 。 - InFactoryService:Tomcat 。 ### 32. Netty和Tomcat的區別 >Netty和Tomcat最大的區別就在於通信協議,Tomcat是基於Http協議的,他的實質是一個基於http協議的web容器,但是Netty不一樣,他能通過編程自定義各種協議,因為netty能夠通過codec自己來編碼/解碼字節流,完成類似redis訪問的功能,這就是netty和tomcat最大的不同。 >有人說netty的性能就一定比tomcat性能高,其實不然,tomcat從6.x開始就支持了nio模式,並且後續還有arp模式——一種通過jni調用apache網路庫的模式,相比於舊的bio模式,並發性能得到了很大提高,特別是arp模式,而netty是否比tomcat性能更高,則要取決於netty程序作者的技術實力了。為什麼Netty受歡迎? netty是一款收到大公司青睞的框架,在我看來,netty能夠受到青睞的原因有三: 併發高 傳輸快 封裝好,Netty為什麼並發高 Netty是一款基於NIO(Nonblocking I/O,非阻塞IO)開發的網絡通信框架,對比於BIO(Blocking I/O,阻塞IO),他的並發性能得到了很大提高。 NIO 2.0裡終於有AIO了,Linux上用AIO,Windows上用IOCP,都支持了概念上的最後一種IOasynchronous I/O 1. 就IO而言:概念上有5中模型:blocking I/O,nonblocking I/O,I/O multiplexing (select and poll),signal driven I/O (SIGIO),asynchronous I/O (the POSIX aio_functions)。 2. 然後呢不同的操作系統對上述模型支持不同: unix支持io多路復用,不同系統叫法不同:freebsd裡面叫kqueue;linux 是epoll。而windows: 2000的時候就誕生了IOCP支持最後一種異步I/O 3. java是一種跨平台語言,為了支持異步IO,誕生了nio,Java1.4引入的NIO 1.0是基於I/O復用的。在各個平台上會選擇不同的複用方式。Linux用的epoll,BSD上用kqueue,Windows上應該是重疊I/O(肯定不是IOCP),但是nio直接使用比較難用,所以有了mina,netty這些針對網絡io部分(tcp/udp-傳輸層)的封裝(nio也有非網絡io部分),為了使nio更易用。http是應用層的協議。 4. servlet3.0則是另外一種東西,不是對協議的封裝,javaee6眾多規範中的一個,但凡javaee6的實現(或者像tomcat這種web容器部分的實現),都會支持servlet3.0,servlet理論上可以支持多種應用層協議(不單單只是http),而servlet3.0以後提供的異步特性與javase提供的nio或aio無直接關係,就是使用bio一樣可以實現servlet3.0中提供的異步特性。異步只是一種概念,異步與否要看,上層使用的異步,而支持的下層完全可能是阻塞的。 - tomcat就是針對http層的,所以我建議http還是選擇tomcat(或者其他成熟的http-server),並不是說netty不好,而是你的選擇問題。 - netty是一個網絡組件,tcp,udp,http都可以弄,但是官方文檔都是些hello wolrd級別的。如果你非常了解http結構,完全可以基於netty搞出一個比tomcat牛的http server。如果做tcp開發,netty不二之選! 現在高並發分佈式網站架構一般採用nginx(前端負載均衡)+ Netty/Tomcat(HTTP) Netty是基於Java NIO開發的,而Tomcat是Apache下的針對HTTP的服務器項目,前者更像一個中間件框架,後者更像一個工具 ### 33. AJAX ``` url位址:ems.parseUrl("file") 1.json $.getJSON(ems.parseUrl("file"), { go: 'getTreeData', }).done(function(treeData) {} ``` ### 34. GIT Commit使用sourceTree 1. 盡量保持InEms、InEcCommon、InEcMessages最新版,需時常Pull。 2. commit不要加入.project檔、WebMvcConfig檔(因我們有將@PostConstruct註解)。 3. push上版。 ==注意:需更新到最新版才能push。== ### 35. Tiles框架 1. 介紹: Tiles 是一種JSP布局框架,主要目的是為了將復雜的jsp頁面作為一個的頁面的部分機能,然後用來組合成一個最終表示用頁面用的,這樣的話,便於對頁面的各個機能的變更及維護。 Tiles使得struts在頁面的處理方面多了一種選擇。並且更容易實現代碼的重用。 2. Struts-Tiles頁面布局: Tiles增加了layout的概念,其實就是把一個頁面劃分為幾塊。通常的來說一個頁面大概可以劃分為如下幾塊: head頁面頭部:存放一個運用的公共信息:logo等,如果是網站可能是最上面的一塊。 menu頁面菜單:放置一個運用中需要使用的菜單,或者在每一個頁面都使用的連接。 footer頁面尾部:如版權信息等。 body頁面主題內容:每個頁面相對獨立的內容。 如果按上面的劃分那對每一個頁面我們只要寫body裏面的內容,其他的就可以共享重用。 如果大多數頁面的布局基本相同我們甚至可以使用一個jsp文件根據不同的參數調用不同的body。 3. 便利性: * 美工界面工作可以讓更懂用戶感受的商務人員與美工交流; * 技術人員專註於業務模型的實現; * 使WBS做得可以更細,測試也可以做得很細; * 以上所有工作可以並行 * 有效的組織項目Views層文件管理,方便團隊協作。 4. 範例:ec6404為例: 1. 開啟src/main/resources/ec/tiles-definition.xml 2. extends需依照各自版型不同去做選擇,此例為複製ec2912版型,因此為newEC2Layout。 ### 36. Wi-Fi、藍牙、ZigBee比較 >談到Wi-Fi、藍牙、ZigBee各自的優缺點,分析:「以發展順序來看,Wi-Fi最早有,再來是藍牙,然後才有ZigBee,這個順序也造成他們聯盟組織的規模、資源有所差異,從而影響各自的發展局面。」 >Wi-Fi無疑是三大無線技術標準中,聯盟規範最嚴謹、詳細,投入的廠商最多,市面上應用基礎最廣,整合、更新上最有企圖的一個,其涵蓋的發展面最廣,從低頻的2.4GHz到高頻的5GHz,甚至是超高頻的60GHz,此外,傳輸距離長,傳送速率高是其特長,但是高耗電一直是其致命傷。不過近年來Wi-Fi也開始發展低耗能的技術,像去年推出能在不具無線網路的環境中探索、連結鄰近設備的Wi-Fi Aware技術即是一例。 >對於發展略晚於Wi-Fi的藍牙,由於它能承載的傳輸量每秒可達1MB,同時安全性高,可設定加密保護,每分鐘能換頻率一千六百次,但有效的傳輸距離較短,傳統的版本大約在10公尺左右,因此比較多投入在個人化的感官體驗的資料傳輸應用上,因而聲音應用領域的配套發展的最多也最成熟。與Wi-Fi相較,藍牙的低功耗、低成本和高度安全是其優勢,而藍牙技術聯盟也很明顯以此為主打,目前發展到4.2的版本,除了提升傳輸距離和傳輸速率,更加降低其功耗,並新增隱私權的功能,強化安全性。 >至於ZigBee,因為其一開始被發展出來就是以自動控制為目的,因此是最適合於智慧工廠、智慧家庭、智慧建築等智慧化控制應用領域的無線通訊技術。在控制應用領域,ZigBee的裝置種類豐富,配套的協議語言也被較完整的定義,而且在基礎通訊協定的設計上,ZigBee一開始就考慮一對千以上的溝通,採用循多重路徑的跳點通訊(multi-hop),因此比起Wi-Fi和藍牙在設備自動化的應用上更為成熟。由於ZigBee的溝通只專注在感應和控制,因此其傳遞的訊息量是很小的,並且由於設備不用一直常開,在這樣的應用方式下也導致其與Wi-Fi和藍牙相較,是最為省電的一種無線通訊技術,另外ZigBee的硬體裝置也是三者中最為廉價的,因此成本也最低,而安全性的部分,由於支援先進加密標準AES (Advanced Encryption Standard),因此只要設置得宜也有極高的安全性。可以說,低傳輸速率、低成本、省電、安全、專用於設備間的溝通(Machine to Machine,M2M)是ZigBee的基本特點。 >然而,也提醒,ZigBee的問題是除了設備自動化外的應用十分薄弱,幾乎只能固守自動控制領域,在當前強調與手機等行動裝置結合的智慧聯網趨勢,因為行動裝置使用的無線技術只有Wi-Fi和藍牙,因此就必須依靠能轉換各種通訊協定的Gateway(閘道器)扮演關鍵的中介角色。「這也是為什麼現在很多智慧家庭方案的廠商都要積極去做Gateway的原因。」 那市面上這麼多號稱擁有Wi-Fi、藍牙、ZigBee技術的產品,是否有認證規範作為依據呢? ![24983-3](https://hackmd.io/_uploads/Sk4BU_v9a.jpg) ### 37. ModelAndView和Model的區別 :::info ModelAndView是方法返回值,例項是使用者手動建立.Model是方法引數,例項是springMVC自動建立並作為控制器方法引數傳入,無需使用者建立.Model和ModelMap返回的是頁面的指定路徑,ModelAndView返回的是物件. ::: ### 38. 上測試機問題 :::info {infactory_backend_url}===http://localhost/infactory-backend/api== ::: 1. Infactory Api - 本機: <br> String infactoryApiUrl = serviceProtocol+"://"+ "<font color="red">localhost/infactory-backend</font>"; <br> String apiUrl = infactoryApiUrl+"/api/devicemanager/v2/gatewayList?userGroupId="+ groupId; - 測試機: <br> String infactoryApiUrl = serviceProtocol + "://"+ <font color="red">settingService.innerIfserviceIP+":"+settingService.innerIfservicePort</font>; http://192.168.10.216:5050 <br> {infactoryApiUrl }/api/devicemanager/v2/gatewayList?userGroupId={groupId} 2. commonApi - 本機: String commonApiUrl = "http://"+ "<font color="red">localhost:6060</font>"; <br> String apiUrl = commonApiUrl + "/api/incommon/v2/inner/group/" + groupId + "/type/"+"?device_type="+DEVICE_TYPE+"&decent="+DECENT; - 測試機: String commonApiUrl = serviceProtocol + "://"+ <font color="red">settingService.innerServiceIP+":"+settingService.innerServicePort</font>; <br> http://192.168.10.216:6060 <br> String apiUrl = commonApiUrl + "/api/incommon/v2/inner/group/" + groupId + "/type/"+"?device_type="+DEVICE_TYPE+"&decent="+DECENT; ### 39. Eclipse的快捷鍵 1. Ctrl+shift+O <br> 可以將文件中不必要的import去掉 ### 40. 查表 1. 屬性代碼可查machineprofile的attrid欄位 <br> 例:602500 綁定的電表裝置代碼 2. 電表查法 - devicelistext可查裝置名稱與listId。 - machineprofile可用attrid查綁定電表ID devicelistid - deviceconfig可用deviceid查dcid。 - metercircuit用dcid可查circuitid。 <br> 注:loadid 為電表後2碼,例:II12000D6F0003BBA289|01,可知loadid=01。 - meterprofile用circuitid查meterid。 - meterinfo電表歷史紀錄,可用meterid查whplus累積用電。 ### 41. 使用本地端測試正式機時須更動資料庫的連接路徑: 1. InFactoryService、InEms1.0.0 - 找到src/test/resources中的application.yml中是哪一個profiles - 至該application-xxx.yml修改資料庫路徑 ### 42. 新增C-Server資料(postman) 1. 增加正式機資料,先在Pre-request Script增加環境時間 - https://infactory.insynerger.com/cs/device/data/ - var now = new Date(); postman.setEnvironmentVariable("now", now.getTime()); ![image](https://hackmd.io/_uploads/rJarPK8IA.png) 2. 在Body貼上下面這段要新增的資料內容 ```json { "gatewayId": "INC7-00073285626C---", "requestSn": "sn_{{now}}", "gatewayPw": "YWJjMTIz", "deviceList": [ { "deviceId": "IN21-00073285626C-61", "parentId": "IN21-00073285626C---", "typeId": "2114", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "401000", "value": "60.74" }, { "attrId": "401600", "value": "2.15" }, { "attrId": "401700", "value": "31907.62" }, { "attrId": "400101", "value": "17.74" }, { "attrId": "400102", "value": "16.9" } ] } }, { "deviceId": "IN11-00073285626C-27", "parentId": "IN11-00073285626C---", "typeId": "1116", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "500800", "value": "0.95" }, { "attrId": "501900", "value": "60.9" }, { "attrId": "500500", "value": "0" }, { "attrId": "501800", "value": "382.4" }, { "attrId": "501200", "value": "21473.1" }, { "attrId": "500900", "value": "33021" }, { "attrId": "500700", "value": "60.03" } ] } }, { "deviceId": "IN11-00073285626C-31", "parentId": "IN11-00073285626C---", "typeId": "1154", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "500800", "value": "0.8" }, { "attrId": "501200", "value": "6070.3" }, { "attrId": "500900", "value": "140000" }, { "attrId": "500700", "value": "0" }, { "attrId": "500500", "value": "0" } ] } }, { "deviceId": "IN11-00073285626C-32", "parentId": "IN11-00073285626C---", "typeId": "1154", "reportBlock": { "reportTime": {{now}}, "attrList": [ { "attrId": "500800", "value": "0.9" }, { "attrId": "501200", "value": "63418.4" }, { "attrId": "500900", "value": "59781" }, { "attrId": "500700", "value": "0" }, { "attrId": "500500", "value": "0" } ] } } ] } ``` ### 43. 公司資料表分類 1. DeviceList找裝置,Profile找即時資訊,Event找事件資訊,Info找歷史資訊(數字Number、文字String、事件Event) 2. 屬性對照表ATTRID_MAPPING 3. 若要顯示在ec1502,QUERYTYPE要有4的資料 :::info QUERYTYPE: RealTime (1) History (2) 統計 (3) 公版(資訊設備彙總)即時顯示(4) ::: ### 44. bootstrap衝突(造成menu底線) 解:在CSS中加入以下去除底線 ```css a { text-decoration: none; } ``` ### 45. HTTP 1. t-infactory.insynerger.com前端使用需要加s,postman沒差 https://t-infactory.insynerger.com/api/incommon/v2/inner/sendMail?recipient=jimmybear@insynerger.com&subject=測試subject&content=123456 2. localhost不用加s ### 46. CSS 1. footer置底 ```css html { min-height: 100%; position: relative; } ``` ```css .footer { background-color: #252525; position: absolute; Width: 100%; bottom: 0; left: 0; } ``` 2.置中 ```css justify-content: center; text-align:center; ``` 3.flex垂直製中 ```css display: flex; height: 300px; align-items: center; ``` ### 47.資料庫 [冰水主機資料庫參考](https://docs.google.com/document/d/1CKojzmpkgE96IZWXupYT7d3GcwwJ_X82Tir10d5-nAo/edit#heading=h.muznqax3j05x) ![image](https://hackmd.io/_uploads/BkodZqUUA.png) 要把回報時間後面的毫秒弄成0 ### 48. 程式檢測 1. 資料庫數值全部null。 2. 資料庫全刪除。 ### 49. 虛擬電表(virtualdevice and virtualdeviceattrid兩表) 會有排程(計算30分前)去對虛擬電錶綁訂的電錶做加總 # 專案架構或設定 ## 架構 ### 1. 各階層通訊架構 ![image](https://hackmd.io/_uploads/SkQlX5L8R.png) ### 2. 設備方法(參考Ins-schedule專案的waterSystemScheduler) 1. 取得設備類別 ``` DeviceCategory devCategory = DeviceCategory.fromId(device.getDeviceId()); ``` 2. 取得設備數據-根據設備類型查詢不同張表 ```java AbstractDeviceProfilesService profileService = deviceProfileService.getProfileService(devCategory); Map<Long, Map<String, Object>> deviceProfileAttrMap = profileService.getDeviceProfile(String.join(",", ConvertUtils.convertToStringSet(attrIds)), Arrays.asList(listId)); ``` 3. AIOT商品正式機 4. AIOT2其他正式機例:排程.. ### 3. 前端方法 1. 主要在main.js透過mainLayoutNew引用,例alertMessage 2. 初始場域grouptree->initSelect(nodeId, 'sf-groupid','appendSystem()'); 3. application cookie 查看處,引用場域ID$.cookie('curNodeId') ### 4. 資料庫 參考:[裝置整合](https://docs.google.com/document/d/1uec4tWBCtnNF5a0YKIBHQTjtNVVT6_CSZBfv188CMKM/edit) 1. xxxprofile即時資料 2. xxxnumberinfo歷史資料 3. meterextprofile新的電錶即時資料表 4. xxxstatisticinfo以結束時間為準-統計表 5. newmeterstatistics以開始時間為準-統計表 ### 5. Yamiconfigurer 配置Bean ```java List<ClassPathResource> list = new ArrayList<>(); list.add(new ClassPathResource("mail.yml")); list.add(new ClassPathResource("notify.yml")); properties.setResources(list.toArray(new ClassPathResource[]{})); configurer.setProperties(properties.getObject()); ``` @ConfigurationProperties(prefix="notify") 使用 ### 6. springMail 1. 先在SettingService設定好物件 ```java @Value("${common.smtp.from}") String mailFromInsnergy; ``` 2. 郵件設定: - 正確寄件人:Carbon Management System <noreply@notif.insynerger.com> - 錯誤寄件人:Carbon Management System <noreply@insynerger.com> 錯誤訊息:郵件無法確認是否由本人寄出。 > 郵件伺服器設定 ``` common: smtp: host: notif.insynerger.com port: 2525 protocol: smtp username: noreply password: Insynerger@n0reply from: InSynerger Notification <noreply@notif.insynerger.com> to: mc-alert.idv@notif.insynerger.com ``` ## 專案設定 ### 1. 自訂yaml設定檔 需更換或放入ftp ->setting裡的customize,測試機上才能使用 ### 2. 登入處理 - webmvcConfig - DynamicWebViewPreparer(動態版面 ### 3. 網頁Title - DynamicWebViewPreparer-execute方法 - 資料庫domininfo-用來設定首頁 ### 4. 改functionmenu順序,需開單 ![1686022445270](https://hackmd.io/_uploads/H1s_n58LR.jpg) ### 5. SessionTimeoutRefreshFilter-登入時間限制 ![image](https://hackmd.io/_uploads/H19Y2qULR.png) ### 6. SecurityConfig-單一登入 ![image](https://hackmd.io/_uploads/ByzohcU8C.png) ```java @Value("${customize.singleLogin:false}") private boolean singleLogin; @Override protected void configure(HttpSecurity http) throws Exception { http .headers().frameOptions().disable(); // https://redmine.insynerger.com/issues/14322 if(singleLogin) { @SuppressWarnings("rawtypes") ConcurrencyControlConfigurer configurer = http.sessionManagement().maximumSessions(1); configurer.expiredUrl("/index.do"); // true一個瀏覽器登入成功後,另外一個瀏覽器就登入不了了。 configurer.maxSessionsPreventsLogin(true); } http .csrf().disable() .authorizeRequests() .antMatchers( "/index.html", "/index.do", "/getPW.do", "/apply.do", "/domain/**", "/favicon.ico", "/html/**", "/images/**", "/scripts/**", "/styles/**", "/upload/**", "/api/**", "/redirects/**" ).permitAll() .anyRequest().authenticated() .and().formLogin() .loginPage("/index.do").permitAll() .failureHandler(formLoginFailureHandler()) .successHandler(formLoginSuccessHandler()) .and() .logout().permitAll() .deleteCookies("JSESSIONID") .invalidateHttpSession(true) .logoutSuccessHandler(logoutSuccessHandler()) .and().cors().and().csrf().disable().addFilterBefore(new AutoRedirectFilter(http), UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(new HttpMethodFilter(), UsernamePasswordAuthenticationFilter.class); } ``` ### 7. TAG使用,tld/ems.tld ```javascript var groupId = "${ems:getUserGroupId()}" ? "${ems:getUserGroupId()}" : null; ``` ### 8. 純xml配置controller,例:1503 (1)CR1503Controller ```java public ModelAndView getRolees(HttpServletRequest req, HttpServletResponse res) throws Exception { Long groupId = SecurityUtils.getUserGroupId(); String apiUrl = serviceProtocol + "://"+ settingService.ifserviceIP + "/api/role/v2/rolelist?groupid="+groupId; StringBuilder sb = new StringBuilder(apiUrl); List<RoleVo> roleList = new ArrayList<>(); try { log.info("send to: {}", sb.toString()); ResponseEntity<ResponseList<RoleVo>> result = new RestTemplate().exchange(sb.toString(), HttpMethod.GET, null, new ParameterizedTypeReference<ResponseList<RoleVo>>() {}); log.info("result: {}", result); if (result != null && HttpStatus.OK.equals(result.getStatusCode())) { roleList = result.getBody().getData(); } } catch (Exception e) { log.error(e.getMessage()); } Map<String, Object> model = new HashMap<String, Object>(); model.put("json", JSONUtils.listToJsonArray(roleList)); return new ModelAndView("jsonData", "model", model); } ``` (2)/ec/controller-config.xml 透過method解決器拆解傳入參數 ``` <!-- CR1503 帳號權限設定 --> <bean name="/cr1503.do" class="org.iii.ems.ec.web.cr15.CR1503Controller"> <property name="methodNameResolver" ref="paraMethodResolver" /> </bean> ``` (3)controller-config.xml方法拆解器定義 ``` <bean id="paraMethodResolver" class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver"> <property name="paramName" value="go" /> <property name="defaultMethodName" value="list" /> </bean> ``` (4)JSP ```javascript $.ajax({ //url: serviceProtocol +'://'+backurl+'/api/role/v2/rolelist?groupid=' + groupid, url: window.location.pathname+'?go=getRolees', type: 'GET', dataType: 'json', // context: document.body, data: '', success: function (res) { roleViewData = res; console.log('687:', res); userData = res.data; var roleViewTable = ''; var checkSelfId = ''; res.forEach(function(item){ //xxxxxxxxxx} ``` ### 9. index.do登入轉導EctuaryIndexLayoutController,domininfo.getLoginIndexUrl()轉導到哪個頁面 ```java protected ModelAndView handleRequestInternal(HttpServletRequest req, HttpServletResponse res) throws Exception { HttpSession httpSession = req.getSession(); HttpSession session = req.getSession(); Domaininfo domaininfo = (Domaininfo)httpSession.getAttribute("domaininfo"); Map<String, Object> model = new LinkedHashMap<String, Object>(); boolean authenticated = SecurityUtils.isAuthenticated(); Userinfo userinfo = null; if(authenticated) {//登入 userinfo = SecurityUtils.getUserinfo(); //有登入才能取得userinfo } if(domaininfo == null) { domaininfo = domaininfoDao.find(req.getServerName()); if(domaininfo == null){ // 若找不到對應的domain 直接設定預設domaininfo domaininfo = ECConstants.domaininfo; } } if(userinfo == null) { domaininfo.setLoginIndexUrl(ECConstants.domaininfo.getLoginIndexUrl()); }else { if(userinfo.getLoginPage() == null) { domaininfo.setLoginIndexUrl(ECConstants.domaininfo.getLoginIndexUrl()); }else { //有設定登入頁面就優先使用 domaininfo.setLoginIndexUrl(userinfo.getLoginPage()); } } model.put("domain", domaininfo); . . . } ``` ### 10. LoginSuccessHandler,登入處理頁面,順序在EctuaryIndexLayoutController之後 ```java //可以獲得此帳號有權限的URI Set<String> grantedUriSet = SecurityUtils.getGrantedUri(); //userinfo可以獲得loginPageUrl Userinfo userinfo = SecurityUtils.getUserinfo(); if(userinfo!=null) { userinfo.setLastlogintime(lastlogintime); userService.updateUserinfo(userinfo); } String loginPageUrl = userinfo.getLoginPage(); if(grantedUriSet.contains(loginPageUrl)) { loginIndexUrl = loginPageUrl; }else if(grantedUriSet.contains(ECConstants.domaininfo.getLoginIndexUrl())){ //角色如有loginIndexUri就優先使用 loginIndexUrl = ECConstants.domaininfo.getLoginIndexUrl(); }else { //當角色無該功能頁面 則導向角色有的功能權限的第一個功能 loginIndexUrl = new ArrayList<String>(grantedUriSet).get(0); userinfo.setLoginPage(null); userService.updateUserinfo(userinfo); } ``` ### 11. for資安,前端傳入場域groupId,需多做判斷,確認傳入的場域為自己或子場域 ```java public ModelAndView getRoles(HttpServletRequest req, HttpServletResponse res) throws Exception { Long groupid = WebUtils.getLongParam(req, "groupid"); //判斷登入帳號下的場域 List<Long> descendantGroupidList = groupService.findDescendantGroupidsByGroupidAndMyself(SecurityUtils.getUserGroupId()); List<RoleVo> roleList = new ArrayList<>(); if(descendantGroupidList.contains(groupid)) { String apiUrl = getInfactoryApiUrl() + "/api/role/v2/rolelist?groupid="+groupid; StringBuilder sb = new StringBuilder(apiUrl); try { log.info("send to: {}", sb.toString()); ResponseEntity<ResponseList<RoleVo>> result = new RestTemplate().exchange(sb.toString(), HttpMethod.GET, null, new ParameterizedTypeReference<ResponseList<RoleVo>>() {}); log.info("result: {}", result); if (result != null && HttpStatus.OK.equals(result.getStatusCode())) { roleList = result.getBody().getData(); } } catch (Exception e) { log.error(e.getMessage()); } } Map<String, Object> model = new HashMap<String, Object>(); model.put("json", JSONUtils.listToJsonArray(roleList)); return new ModelAndView("jsonData", "model", model); } ``` ### 12. for資安,阻止手動修改URI跳轉畫面 1. FilterConfig ```java @Configuration public class FilterConfig { @Autowired private GrantedUriFilter grantedUriFilter; @Bean public FilterRegistrationBean functionUriFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(grantedUriFilter); bean.addUrlPatterns("/*"); bean.setName("grantedUriFilter"); bean.setOrder(0); return bean; } } ``` 2. GrantedUriFilter ```java @Slf4j @Component public class GrantedUriFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest)request; HttpServletResponse resp = (HttpServletResponse)response; String functionURI = req.getRequestURI(); //判斷有crxx.do或ecxx.do的網址 if(functionURI.matches("/cr\\d+\\.do")||functionURI.matches("/ec\\d+\\.do")) { Long groupid = SecurityUtils.getUserGroupId(); Set<String> grantedUriSet = SecurityUtils.getGrantedUri();//查看有權限的uri 好用 if(!CollectionUtils.isEmpty(grantedUriSet)) { // 爲了抵禦手動輸入functionName跳轉至其他無權限功能 if (!grantedUriSet.contains(functionURI)) { log.error("Return from filter,groupid:"+groupid +",unsupported functionUri: " + functionURI); resp.setStatus(HttpStatus.SC_NOT_FOUND); RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/404Error"); dispatcher.forward(req, resp); return; } } } chain.doFilter(request, response); } } ``` ### 13. customize自訂檔打包位置:InemsCarbon/src/main/resources/projects/** ```yaml // Copy and rename customize.yml copy { from "${projectDir}/src/main/resources/projects" into distName + "/setting" include "customize-${env}.yml" rename "customize-${env}.yml", "customize.yml" } ``` ## 程式方法 ### 1.後端多語系方法 ```java LocaleUtils1.getI18nMessage( "jsp.common.menu.locale."+l , httpServletRequest)。 ``` ### 2.ems.parseUrl("file") - 前端網址解析:cr0203.do ### 3.陣列轉換JSON ```java JSONUtils.listToJsonArray(list)。 ``` ### 4.JAVA 8 stream 方法 ```java //普通篩選 if(!buyerStatus.equals(0) && buyerStatus != null) { list = list.stream().filter(carbon -> buyerStatus.equals(carbon.getBuyerStatus())).collect(Collectors.toList()); } //各時間段運轉狀態為0的筆數 Map<Long, Long> collectCoolingCapacityOffCountMap = watersystemStatisticinfoList.stream().filter(s->ENABLE_OFF.equals(s.getEnableStatus())) .collect(Collectors.groupingBy(CompositeWaterSystemStatisticInfoVo::getStatisticTime, Collectors.counting())); //各時間段運轉狀態為1的加總 collectCoolingCapacityMap = watersystemStatisticinfoList.stream().collect(Collectors.groupingBy(CompositeWaterSystemStatisticInfoVo::getStatisticTime, Collectors.reducing(new BigDecimal("0"), c->{ if(c.getCoolingCapacity() != null) { if(ENABLE_ON.equals(c.getEnableStatus())){ return c.getCoolingCapacity(); } if(CollectionUtils.isNotEmpty(coolingFullOffTimeList)) { if(coolingFullOffTimeList.contains(c.getStatisticTime())) { return c.getCoolingCapacity(); } } } return new BigDecimal("0"); }, (v1, v2) -> v1.add(v2)))); } ``` ### 5.stream使用 ```java //----------------------------------------------- //讀取CSV InputStreamReader reader = new InputStreamReader(file.getInputStream()); CSVReader csvReader = new CSVReaderBuilder(reader).build(); List<String[]> allData = csvReader.readAll(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); List<String[]> listByModel = allData.stream().filter(cell->cell[carModelCol].contains(carModel)).collect(Collectors.toList()); //篩選年份後,按月份分組求和 monthlyCarDataMap = listByModel.stream().filter(cell->cell[dateCol].contains(year.toString())) .collect(Collectors.groupingBy((cell -> LocalDate.parse(cell[dateCol],formatter).getMonthValue()), Collectors.mapping(cell -> new BigDecimal(cell[fillingCol]), // string 轉 BigDecimal Collectors.reducing(BigDecimal.ZERO, BigDecimal::add) // 求和 ))); ------------------------------------------------ //篩選年份後,按月份分組求數量 Map<Integer, Long> monthlyCarCountLongMap = listByModel.stream().filter(cell->cell[dateCol].contains(year.toString())) .collect(Collectors.groupingBy((cell -> LocalDate.parse(cell[dateCol],formatter).getMonthValue()),Collectors.counting())); //Array使用 Arrays.stream(cookies).filter(cookie -> "username".equals(cookie.getName())) .forEach(cookie ->{ // String name = cookie.getName(); String value = cookie.getValue(); model.put("username", value); }); ``` ### 6.JS 序列化 ```javascript var applyData = $('#applyForm').serializeArray(); var content = "<table style='vertical-align:middle; border:1px #658FA7 solid; margin-bottom:30px; border-spacing:0px;'> <tbody>"; $(applyData).each(function(){ if(this.name != "inputFile" && this.name != "go" && this.name != "content"){ content += "<tr style='border:1px #658FA7 solid; color:#000000; background- color:#d1eef3;'>"+ "<td style='width:130px; border:1px #658FA7 solid; line-height:25px; font-weight:bold; text-align:left;padding-left:10px;'>"+this.name+"</td>"+ "<td style='border:1px #658FA7 solid; line-height:25px; font-weight: bold;text-align:left; width:240px; padding-left:10px;'>"+ this.value + "</td>" } }) ``` ### 7.防止submit ```javascript $("form#applyForm").submit(function(e) { e.preventDefault();//**防止 var formData = new FormData(this); $.ajax({ url: window.location.pathname, type: 'POST', data: formData, success:function (data){ if(data.success){ $('#inputReset').click(); successMessageWithButton("<spring:message code='jsp.apply.message.sendSuccess'/>" ,null ,function(){ window.location.replace('${ctx}/index.do'); }); }else{ alertMessage("<spring:message code='jsp.apply.message.sendFail'/>");//寄送失敗,請聯繫管理員 } }, error: function (res) { alertMessage("<spring:message code='ec1502.0034'/>");<%--上傳失敗--%> }, cache: false, contentType: false, processData: false }); } ``` ### 8.webSocket 1. jsp-js ```javascript <script src="scripts/bootsLayout/stomp.js"></script> <script src="scripts/socket/sockjs.min.js"></script> <script> var stompClient = null; function connect() { var groupId = $.cookie('curNodeId') if(!groupId){ return; } var host = window.location.host; var path = window.location.pathname; var protocol = window.location.protocol; var webCtx = path.substring(0, path.indexOf('/', 1)); var endPointURL = "ws://" + host + "/carbon/event"; if(protocol == 'https:'){ endPointURL = "wss://" + host + "/carbon/event"; } console.log("endPointURL"+endPointURL) stompClient = Stomp.client(endPointURL); // var socket = new SockJS('/carbon/event'); // stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { console.log('Connected: ' + frame); stompClient.subscribe('/notify/'+groupId, function(message) { console.log("message"+message); getMessageCount(); //var output = document.getElementById("output"); //output.innerHTML += "<p>" + message.body + "</p>"; }); }); } </script> ``` 2. WebSocketConfig ```java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{ @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic","/notify");// 配置消息代理前缀(發送給有訂閱的人) config.setApplicationDestinationPrefixes("/app");// 配置應用程序前缀,發送這網址會去MessageMapping } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/carbon/event").setAllowedOrigins("*"); registry.addEndpoint("/carbon/event").setAllowedOrigins("*").withSockJS(); } } ``` 3. WebSocketController ```java @Slf4j @Controller public class WebSocketController { private final SimpMessagingTemplate messagingTemplate; public WebSocketController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } public void sendMessage(Long groupId, String message) { log.info("sendMessage"+groupId); messagingTemplate.convertAndSend("/notify/"+groupId, message);///發給訂閱的人 } // @MessageMapping("/sendMessage") // public void sendMessage(@Payload String message) { // // 处理接收到的消息 // // 可以根据需要将消息发送给特定用户 // messagingTemplate.convertAndSend("/topic/messages", message); // } } ``` 4. https設定(apache設定):/etc/httpd/conf/httpd.config note:wss預設443 Port,https必須用wss ws預設80 Port ```java <VirtualHost *:80> ServerName nz-carbon.com Redirect / https://nz-carbon.com/ # RewriteEngine on # RewriteCond %{SERVER_NAME}=nz-carbon.com # RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] </VirtualHost> <VirtualHost *:443> ServerName nz-carbon.com RewriteEngine On RewriteCond %{HTTP:Upgrade} =websocket [NC] RewriteRule /(.*) ws://localhost:8080/$1 [P,L] RewriteCond %{HTTP:Upgrade} !=websocket [NC] RewriteRule /(.*) http://localhost:8080/$1 [P,L] Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains" ProxyPassReverse / http://localhost:8080/ SSLEngine On SSLCACertificateFile /etc/letsencrypt/live/nz-carbon.com/fullchain.pem SSLCertificateFile /etc/letsencrypt/live/nz-carbon.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/nz-carbon.com/privkey.pem # Include /etc/letsencrypt/options-ssl-apache.conf </VirtualHost> ``` ### 9.Optional -java.util [optional](http://java.atguigu.com/news/6270.html) ```java user = Optional.ofNullable(user).orElse(createUser()); user = Optional.ofNullable(user).orElseGet(() -> createUser()); //這兩個函數的區別:當user值不為null時,orElse函數依然會執行createUser()方法,而orElseGet函數並不會執行createUser()方法 //Optional.ofNullable(物件),null會返回空對象 Optional.of(物件),null時拋null point excetion orElseThrow,直接拋異常。Optional.ofNullable(user).orElseThrow(()->new Exception(“用户不存在”)); ``` ### 10.[多執行緒](http://java.atguigu.com/news/6034.html) ### 11.JS前端篩選Array ```javascript var data = [{ checkInfo: { checkId: "item1" } }, { checkInfo: { checkId: "item2" } } ]; //filter方式 data.filter(list => list.checkInfo.checkId === searchString)[0] : data[0]; //find方式直接找地一個符合條件的 let match = data.find(list => list.checkInfo.checkId === searchString) ``` ### 12.dataTable應用 ```javascript <link rel="stylesheet" type="text/css" href="styles/inems/jquery.dataTables.css"> <script type="text/javascript" src="scripts/bootstrap-4.6.0/js/bootstrap.bundle.min.js"></script> <script src="scripts/jquery/plugin/dataTables-1.12.1/jquery.dataTables.min.js"></script> <script type="text/javascript" src="scripts/jquery/plugin/dataTables-buttons-2.2.3/dataTables.buttons.min.js"></script> <script type="text/javascript" src="scripts/jquery/plugin/dataTables-buttons-2.2.3/buttons.html5.min.js"></script> <!-- <script type="text/javascript" src="scripts/jquery/plugin/dataTables-buttons-2.2.3/buttons.print.min.js"></script> --> /** * DataTable初始 * */ function initDataTable(dataTableId, serchOption, method, dataJson, columnArray){ /*https://datatables.net/extensions/buttons/examples/html5/simple.html*/ return dataTable = $("#"+dataTableId).dataTable({ dom: 'B<l<"pageDetails"i>>tp', buttons: [{ extend: 'csvHtml5',//csv也行 text: 'CSV', bom: true, title: "車輛填報"+"輸出資料", exportOptions:{ columns:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] } }], initComplete: function() { //使用dataTable內建匯出按鈕並隱藏 var $buttons = $('.dt-buttons').hide(); //輸出按鈕傾聽 $('#exportCSV').on('click', function() { var btnClass = ".buttons-csv"; if (btnClass) $buttons.find(btnClass).click(); }) }, searching: serchOption, fnDrawCallback: function(oSettings) { // if (oSettings._iDisplayLength >= oSettings.fnRecordsDisplay()) { if (oSettings.fnRecordsDisplay() <= 0) { $('#dataListTable_paginate').hide(); }else{ $('#dataListTable_paginate').show(); } }, language: { lengthMenu: "<spring:message code='ec380201.0012' /><%--每頁顯示--%> _MENU_ <spring:message code='ec380201.0013' /><%--項--%>", info: "<spring:message code='jsp.common.pageIndex.desc' arguments='_PAGE_,_PAGES_, _TOTAL_' />", <%--頁次 {0}/{1}, 共 {2} 筆資料--%> infoEmpty: "<spring:message code='jsp.common.pageIndex.desc' arguments='0,0,0' />", search: "<spring:message code='ec380201.0015' /><%--搜尋--%>", infoFiltered: "(<spring:message code='ec380201.0014' arguments='_MAX_' />)", emptyTable: "沒有符合的商品", zeroRecords: "<spring:message code='ec.common.jqgridsearchnorecords' /><%--未能找到匹配的記錄--%>", paginate: { first: "<spring:message code='jsp.common.pageIndex.first' /><%--第一頁--%>", previous: "<spring:message code='jsp.common.pageIndex.prev' /><%--上頁--%>", next: "<spring:message code='jsp.common.pageIndex.next' /><%--下頁--%>", last: "<spring:message code='jsp.common.pageIndex.last' /><%--最後一頁--%>" } }, ajax: function (data, callback, settings) { $.ajax({ type: method, url: window.location.pathname, data : dataJson, dataType: "json", success: function (result) { if(result){ var returnData = {}; returnData.data = result.list;//返回的資料列表 callback(returnData); } } }); }, //列表表頭欄位 columns: columnArray, }).api(); } ``` ## 錯誤處理 ### 1. dominxxx無法開啟專案,使用build only.launch 編譯 ### 2. API豎線處理|->%7C ``` http://localhost:6060/api/incommon/v2/inner/statistics/powerMeter/II12000D6F0003BBA289%7C03/?stats_type=5&start_time=1638288000000&end_time=1675180800000&attr_ids=501200 ``` >前端轉換例子(ec7101) ```javascript=488 var ApiDeviceId=deviceId.replace("|", "%7C");//轉成api使用的deviceId $.ajax({ url: window.location.pathname +"?go=getMeterStatisticWMY&deviceId="+ApiDeviceId+"&startTime="+startTime+"&endTime="+endTime+"&demo="+demo+"&lastYearStartTime="+lastYearStartTime+"&lastYearEndTime="+lastYearEndTime, type: 'GET', dataType: 'json', success: function (result) { // console.log(result) if(result.thisYear.devicesStatistics.length>0){ //傳device的所有值及bindingType去重整資料 if(result.lastYear.devicesStatistics!= null && result.lastYear.devicesStatistics.length>0){ addArray(result.thisYear.devicesStatistics[0].statisticsValues,result.lastYear.devicesStatistics[0].statisticsValues,bindingType);//拿到對應的deviceId每個月分的電表資料 }else{ addArray(result.thisYear.devicesStatistics[0].statisticsValues,null,bindingType);//拿到對應的deviceId每個月分的電表資料 } } // appendElectricityInformationTable();//加資料到表格中 getHighchart();//將表格的資料放在圖上 }, error: function (result) { console.error(result); } }); ``` >後端轉換例子(EvDeviceAPIService-getTradeRecordByApi) ```java=152 String apiUrl = incommonUrl + "/api/incommon/v2/inner/ev/trade/records?deviceId=" + deviceId.replace("|", "%7C") + "&startTime=" + start + "&endTime=" + end; ``` - API位置->InnerDeviceController->deviceStatistic() ### 3.新電表meterextprofile ### 4.meterstatisticinfo統計方式為15分鐘到才統計。例:00:15等於00:00~00:15資料 newmeterstatistics為到之前就統計為15分。例:00:15等於00:15~00:30資料 ### 5.bootstrap.css會預設Ul margin-left:2rem導致title跑版 解:ol, ul { padding-left: 0 !important; } ### 6.styles/inems/jquery.dataTables.css要在styles/inems/commonStyle.css前面,才能透過前後順序覆蓋為想要的樣式。 ### 7.meterNumberInfo捨棄歷史資料沿用meterInfo2 ### 8.本機名稱:INSYNERGER\jimmybear ### 9.datatable Cannot read property 'style' of undefined - 解:確認initDataTable欄位數相同 ### 10.有父類別的class要轉成JSON,不能直接用new JSONObject (可能才可以看到父類別屬性) - 解:json.put("carbonHolding",new JSONObject(new Gson().toJson(carbonHoldingVo))); ### 11.三元運算式坑:傳入錯誤的資料型態,編譯器無法偵測錯誤,但包版會錯。 ==例:Long test = object.get("test2") ? object.get("test2") : 0 錯誤原因:將int傳入Long== ### 12. 測試機是Linux環境,斜線不同 ```java new File(FilePath);//本機可以,測試機不行,絕對路徑、file無法用在測試機, new FileInputStream(FilePath);//本機可以,測試機不行,絕對路徑無法用在測試機, new ClassPathResource(filePath+fileName).getFile();//本機可以,測試機不行,因為測試機為war檔 new ClassPathResource(filePath+fileName).getInputStream();//本機可以,測試機可以,war檔的解決方法 ``` [可參考](https://stackoverflow.com/questions/36371748/spring-boot-access-static-resources-missing-scr-main-resources) ### 13.正式機設定檔不能設為application-prod,優先級高於東堅自己替換的yml黨,不然測試機環境會去連正式機資料庫。 ### 14.本地端連正式資料庫,須將api網址換成127.0.0.1,不然localhost會自動轉導首頁 ### 15.引用@PropertySource("classpath:application.yml")的service裡不能寫方法,不然裡面都無法讀取設定檔,例:settingService ### 16.gmail附件名稱亂碼處理,增加[sdk上版說明](https://docs.google.com/document/d/1YRdmQP2yXG8XvOmNw7BvK7Tp4c2N6lBCgcYuagDSTag/edit#heading=h.uxn169u7xql9) ```java System.getProperties().setProperty("mail.mime.splitlongparameters", "false"); ``` 或 可以測試看看? ```yaml spring: application: name: YourApplicationName mail: mime: splitlongparameters: false ``` ### 17.上版read.me文件上傳後亂碼,InemsCarbon的bulid.gradle 因:編譯文件時輸出並非utf-8 解: ```java new File(distName,"readme.txt").withWriter("UTF-8") { writer -> writer.write("${releaseNote}") } ``` ### 18.如前端JSP沒導入<%@ include file="/WEB-INF/jsp/common/includes.jsp"%>,會發生無法讀取controller傳來的值,EL語法讀不到。 ### 19.發信發生未驗證問題,要注意上板環境的smtp設定是否與發信mail server設定的mailform相同,否則發生以下錯誤 ![image](https://hackmd.io/_uploads/Bkbl138LA.png) ![image](https://hackmd.io/_uploads/BktgyhUU0.png) ![image](https://hackmd.io/_uploads/Bksz12ULR.png) ### 18.JPQL不能使用 ```java //錯誤 SELECT * FROM CarbonCreditInfo c WHERE c.projectInfo.pjId = ?1 and c.sellerStatus = ?2 //正確 SELECT c FROM CarbonCreditInfo c WHERE c.projectInfo.pjId = ?1 and c.sellerStatus = ?2 FROM CarbonCreditInfo c WHERE c.projectInfo.pjId = ?1 and c.sellerStatus = ?2 ``` ### 19.W11工作列消失,重啟檔案總管 ### 20.httpMethodFilter 寫入header cache-controller會被蓋過問題 原因:security.web.header.HeaderWriterFilter在httpMethodFilter 運行之前,所以會發生被覆蓋情形 解:可於springSecurity中加下來程式,便可於HeaderWriterFilter新增。但自產生的max-age,不知如何消除 ```java //Insecure Policy ( 11306 ) http .headers() .addHeaderWriter(new StaticHeadersWriter("Cache-Control", "no-cache, no-store, max-age=0, private")) .addHeaderWriter(new StaticHeadersWriter("Pragma", "no-cache")); ``` ### 21.dataTable樣式問題 沒對齊原因在dataTable 裡scrollx = true,改用外部overflow:auto就可以 ![image](https://hackmd.io/_uploads/r1hvds8IA.png)