# [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層 - 平台

- CS層使用三種傳輸協定
- http
- socket (以前用的),也是基於TCP/IP協議,但通常是長連線
- mqtt,透過Broker作為中間件,一種消息訂閱的連線模式,常用於IOT
- 這邊的相關服務好像是開在8883 port (MQTT 預設埠為 1883。加密的埠為 8883)
- CS層也會將Raw Data直接寫進DB
- 他的包叫CServerEms,主要由鐘漁管的,如果需要什麼新功能,請開單

### 3. CSTest
- 裝置的CS單元測試都在這邊
- 比如我想要手動加入一個裝置(INC4的虛擬gateway),要加在# t-infactory測試機,首先去改CsServerIp=192.168.10.216

- 然後DB也要對應(因為要去操作devicelist表)

- 接著隨便挑一個裝置種類相近的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

- 手動把他改正確,例如台北辦公室的測試環境CS IP=192.168.10.216,port=8500

- 虛擬gateway是要設定621100跟621200,注意port有區別cs port=8500 (這個是web服務走的,還會再轉一次)
- csScCmd port是8900 (這個是實際指令最後真正去的地方)

### 4. HTTP增加裝置
溝通CS也可以用HTTP,例如想手動觸發一筆裝置回報的資料,參考
https://docs.google.com/document/d/1QnzDL1ZLIiM6kc9YZIO_BQRUQo5IG9qhN2q7eQ8uTMw/edit

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找到確切的那個功能

- devicelist是總表,他有一個ext是補充說明
- profile跟event是週期的紀錄或事件的紀錄

### 6. device
- listid是流水號主鍵,等於其他子表的devicelistid,相當於身分證號
- deviceid乍看很像,不要搞混,deviceid是具體的一個物品(例如兩個一樣的產品,我為了區分編號1編號2,就有2個deviceid),deviceid就是實際給人看的,相當於綽號(但是不會重複)

- attr是某種屬性(功能)
- 容易混淆的點在於,比如有一個"溫溼度感應計",溫度是一個attrid、濕度又是一個attrid,所以就會有兩筆profileid
- 當有了attrId卻不清楚他具體意義時,直接去雲端硬碟搜尋


- 當有devicelistid跟屬性代碼attrid時就能取得一個唯一辨識
- 週期的回報就是就是profileid
- 事件(長的)就會產生eventid

### 7. profile
- 可以說device是設備、attr是某種屬性(功能)、profile是"某個設備的某個功能的最新的那筆週期回報紀錄"

- profile中有個attrtype用來表示紀錄的類型,2表示是數字類型(歷史就會記錄在sensornumberinfo),3表示文字類型(就會記錄在sensorstringinfo),1是只會記錄最新的在profile表,例如line或email通知完就沒了,而不寫進歷史(其他專門的表)
- 所謂"寫進歷史"根據應用處理那邊的勾選,決定要記錄即時或歷史
- 即時就是寫在profile表
- 歷史就是寫在sensornumberinfo表,如果2種都有勾,gateway收集到裝置的紀錄給C server後會去兩個表都寫入
- 在profile表更新最新這一筆,同時也寫入xxxinfo表(看他是number或string類型)

- sensornumberinfo表的歷史紀錄,value就是實際的值(數字類型)

### 7. event
- 可以說device是設備、attr是某種屬性(功能)、profile是"某個設備的某個功能的最新的那筆事件紀錄"
- event的attrid通常比較長(位數多)
- event沒有attrtype,統一用一個value(text)來存事件,就算是數字也會存成文字,相關的定義一樣要去表看

- 同理,最新的那筆存在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個分支切來切去

- 這是一個lib,裡面已經封裝好了去DB撈對應資料的方法
- 比如我有devicedId、attrId、時間區間就可以直接拿出歷史紀錄`List<History>`(就是去撈sensornumberinfo這種)
-

### 10. SurveillanceService
- 排程服務,在維管中心叫IVS,定時去執行一些東西
### 11. InFactory
- 主要的WEB服務都在這
- 由Group這個抽象層來區分權限
- 又分成地點與建物
- 裝置(device)必須依附於建物之下
### 12. InEcCommon
- InFactory的一些通用項目,只是個library不用啟動服務
- 注意主分支是hyec而非master
### 13. InEcMessages
- InFactory的i18n部分,提供多國語言

- .properties是給spring:message讀的ResourceBundle
- 用搜尋檔案會發現有超多重複(很多子模組各有一個),屬於歷史遺留,一般用common的那個就好

- 也有.json或.js的是給其他前端用的,總之...大雜燴
### 14. InEms1.0.0
- InFactory的主服務API
- 主分支是iniot,而非master
- 啟動前需要手動在本地C:\複製一份appdata的資料


- 測試帳號: insdev
- 密碼: insdev1234
- JSP頁面鑲嵌是在main.layout
- 關掉groupTree可以加速啟動

- 登入登很久的話,可以用權限小一點的帳號(下方說明流程)
### 15. InFactoryService
- InFactory的導航列部分,啟動在5050

- 如果啟動報錯 CServerFacade循環依賴,先關掉CCS跟data-definition

### 16. 增加功能到麵包屑
- 在in_functionmenu加入這個功能
- i18n也要加

- 到in_rolefunction表
- 指派那些role可以控制這個功能,至少要指派給1超級管理員

- 到`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. 
- mapper生成後,專案的設定應該長這樣

- 如果還是有問題,檢查Run/Debug設定

- 主服務,啟動後訪問 `http://localhost:8085/`

- 接下來需要獲取登入的帳號密碼,回到專案從resources找到application.yml
- profiles.active: 表示當前選中的啟動設定
- 慣例是使用T開頭的表示測試環境
- include則是都會包含的,相當於基底的意思

- 選到`application-tinpark.yml`
- 可以看到其中的DB連線資訊

- 使用DBeaver連線,例如想獲取登入帳號資訊就搜尋userinfo表

- SQL debug想印出查詢的語句,更改`logback-spring.xml`

- 順便一提,在InPark新增Entity要去`hibernate.cfg.xml`增加Mapping,否則查找半天都會是空的

### 18. inpark-api-service
- 提供 In-Park 相關API
- 他的application.yml也是放在/test之下,所以需要手動指定
- 對專案右鍵>Properties>Java Build Path>Source>把resources資料夾直接link到test的對應位置

- InsynergerCommonAPI
- 負責InPark與InFactory共用的API
- 它的設定檔是放在test目錄下,但是有巨坑,改這個active後面的方案沒用,run在本地要改另一個地方

- 要改這個ICA - STS - Run.launch,這邊的優先級>application.yml

- 啟動在port 6060
- 啟動時報錯airScheduleController nest問題,要關掉data-definition-api
- 大概是有重複名字的.java造成的

### 19. API驗證token
- 這邊的Controller分為inner(不用驗證)跟auth(要去auth換token)的,

- token其實就是用InFactory的使用者的帳號密碼做參數去換,舉例來說請求長這樣
- 幾個參數都是固定的,只有username跟password要換成InFactory的使用者的帳號密碼,並且注意密碼是純MD5(32位小寫)算過的值
- 驗證要問的url寫在

- 每個環境都有自己的auth server,交換之後就寫在那個環境的auth db,過一段時間後就被刪掉(過期)

- 帶著換到的token就能訪問auth的Api

### 20. 啟動問題
#### Gradle的JDK
- 因為STS4內建的JVM版本比較新(Eclipse他本身也是java寫的),需要手動指定JDK

- `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即可

### 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`這是方便本地測試用的,不要搞錯

- 這些yml的來龍去脈去是這樣,這些有腳本的,在build的時候會根據案場產出多個資料夾,每個裡面都把`customize-xxx.yml`重新命名成`customize.yml`

- 在https://mc.insynerger.com/執行更版的動作時,會做一個重新命名

- 但是真正上版丟到那台主機(例如某私雲)的時候,又會改成`application.yml`

- 它的組成是來自`customize.yml`,拼接成完整的`application.yml`(放在下半部),放到`/config`路徑之下。舉例來說,上到大鵬灣環境,真正吃的設定就是這兩項的組合

- 使用寫好的.launch腳本Build整個War包

- 然後會跳出彈窗,需要填入版本號(就是當前版本+1)與描述(就是redmine議題追蹤系統上的單號#12893之類的)
- https://redmine.insynerger.com/
- 等待建構,最後會在專案的根目錄產生對應的資料夾,用FTP拉去對應的位置(ftp資料夾裡面找尋INPARK/release)即可

- 開啟維管登入(點擊雲端圖示)選擇環境執行換版作業
- 如果loading半天,有可能是FTP那個資料夾裡面項目太多,把舊的放到history

- 進行到97%會卡特別久,這是正常

- 可以同時上多個專案的版(例如在大鵬灣環境同時更InPark+InEMS)
- 但不能同時在多個地方上同一個專案(例如在測試機上inEMS又在大鵬灣上inEMS),會導致前一個被abort
#### InEms(沒腳本的)
區別在於有沒有include: customize
- 用gradle build,有三種方法,如果1、2都行不通,就用本地安裝的gradle下命令行去build
- 1行不通的原因是gradle版本太新,具體表現是按了會看到快速地閃出錯誤然後又被洗掉,之後無事發生

- 成功build會在跟目錄下/libs資料夾找到war

- 在FTP上,InEMS這邊是用`版本號.場域`來區分

- 先把最新版本的資料夾複製一份,更改版本號

- readme.txt就相當於用腳本更新到一半跳出來那個視窗,裡面填版本號跟說明
- 最後這些訊息會顯示到line上

- setting的部分,需要手動更改
- 最好的方法是直接把setting資料夾複製舊的過去

- 它是由例如application-DBNSA.yml的上半部分加customize.yml的設定組成

- 最後去到服務主機內看只有一個app.yml,其實道理都一樣,真正放到各案場的都是從`customize.yml`拼接出來的

- 上在t-infactory_inems的話,他後續啟動服務要很久(可以長到10分鐘左右)
### 26. 查看主機服務運行
- 用putty連到內網的該主機ip,然後輸入帳號密碼

```
# 查看服務
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啟動,若啟動有問題可以直觀的看到

### 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
```
- 選信任


### 29. ELK查看log
- http://192.168.10.221:5601/app/discover
- 不是全部的主機都有上ELK,但測試環境幾個常用的都有

```
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. 顯示要憑證點`是`

```
### 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技術的產品,是否有認證規範作為依據呢?

### 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());

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)

要把回報時間後面的毫秒弄成0
### 48. 程式檢測
1. 資料庫數值全部null。
2. 資料庫全刪除。
### 49. 虛擬電表(virtualdevice and virtualdeviceattrid兩表)
會有排程(計算30分前)去對虛擬電錶綁訂的電錶做加總
# 專案架構或設定
## 架構
### 1. 各階層通訊架構

### 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順序,需開單

### 5. SessionTimeoutRefreshFilter-登入時間限制

### 6. SecurityConfig-單一登入

```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相同,否則發生以下錯誤



### 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就可以
