人脸/车闸接口整合
This commit is contained in:
@ -1,7 +1,11 @@
|
|||||||
package com.sipai.controller.administration;
|
package com.sipai.controller.administration;
|
||||||
|
|
||||||
|
import com.sipai.tools.HttpUtil;
|
||||||
|
import com.sipai.tools.DeviceAccessHttpUtil;
|
||||||
import net.sf.json.JSONArray;
|
import net.sf.json.JSONArray;
|
||||||
import net.sf.json.JSONObject;
|
import net.sf.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@ -9,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -17,22 +22,19 @@ import java.util.Comparator;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
//curl --location 'http://127.0.0.1:8090/dh-netsdk/attendance/getDailyRecordList' \
|
import java.util.Properties;
|
||||||
// header 'Content-Type: application/json' \
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
// header 'Dh-Device-Ip: 192.168.1.108' \
|
|
||||||
// header 'Dh-Device-User: admin' \
|
|
||||||
// header 'Dh-Device-Port: 37777' \
|
|
||||||
// header 'Dh-Device-Timestamp: {{dhTs}}' \
|
|
||||||
// header 'Dh-Device-Password: {{dhEncPwd}}' \
|
|
||||||
// data '{
|
|
||||||
// "day": "2026-04-28",
|
|
||||||
// "pageNum": 1,
|
|
||||||
// "pageSize": 20
|
|
||||||
// }'
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/administration/attendance")
|
@RequestMapping("/administration/attendance")
|
||||||
public class AttendanceController {
|
public class AttendanceController {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AttendanceController.class);
|
||||||
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
private static final int DEFAULT_FETCH_DAYS = 30;
|
||||||
|
private static final int DEFAULT_REMOTE_PAGE_SIZE = 200;
|
||||||
|
private static final int MAX_REMOTE_PAGES_PER_DAY = 100;
|
||||||
|
private static final String DEFAULT_API_URL = "http://127.0.0.1:8090/dh-netsdk/attendance/getDailyRecordList";
|
||||||
|
private static final Properties ATTENDANCE_PROPS = loadAttendanceProps();
|
||||||
|
|
||||||
@RequestMapping("/showList.do")
|
@RequestMapping("/showList.do")
|
||||||
public String showList(HttpServletRequest request, Model model) {
|
public String showList(HttpServletRequest request, Model model) {
|
||||||
@ -53,7 +55,8 @@ public class AttendanceController {
|
|||||||
|
|
||||||
LocalDate startDate = parseDay(start);
|
LocalDate startDate = parseDay(start);
|
||||||
LocalDate endDate = parseDay(end);
|
LocalDate endDate = parseDay(end);
|
||||||
List<Map<String, Object>> allRows = buildMockRows();
|
LocalDate[] range = resolveRange(startDate, endDate);
|
||||||
|
List<Map<String, Object>> allRows = loadRowsFromRemote(request, range[0], range[1]);
|
||||||
List<Map<String, Object>> filtered = new ArrayList<Map<String, Object>>();
|
List<Map<String, Object>> filtered = new ArrayList<Map<String, Object>>();
|
||||||
|
|
||||||
for (Map<String, Object> item : allRows) {
|
for (Map<String, Object> item : allRows) {
|
||||||
@ -75,10 +78,10 @@ public class AttendanceController {
|
|||||||
if (!status.isEmpty() && !status.equals(st)) {
|
if (!status.isEmpty() && !status.equals(st)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (startDate != null && day != null && day.isBefore(startDate)) {
|
if (startDate != null && (day == null || day.isBefore(startDate))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (endDate != null && day != null && day.isAfter(endDate)) {
|
if (endDate != null && (day == null || day.isAfter(endDate))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
filtered.add(item);
|
filtered.add(item);
|
||||||
@ -97,6 +100,239 @@ public class AttendanceController {
|
|||||||
return new ModelAndView("result");
|
return new ModelAndView("result");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> loadRowsFromRemote(HttpServletRequest request, LocalDate startDate, LocalDate endDate) {
|
||||||
|
List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
|
||||||
|
if (startDate == null || endDate == null) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
int pageSize = getIntProp("attendance.api.pageSize", DEFAULT_REMOTE_PAGE_SIZE);
|
||||||
|
String url = getProp("attendance.api.url", DEFAULT_API_URL);
|
||||||
|
Map<String, String> headers = buildDhHeaders(request);
|
||||||
|
|
||||||
|
LocalDate day = startDate;
|
||||||
|
while (!day.isAfter(endDate)) {
|
||||||
|
for (int pageNum = 1; pageNum <= MAX_REMOTE_PAGES_PER_DAY; pageNum++) {
|
||||||
|
com.alibaba.fastjson.JSONObject req = new com.alibaba.fastjson.JSONObject();
|
||||||
|
req.put("day", DAY_FMT.format(day));
|
||||||
|
req.put("pageNum", pageNum);
|
||||||
|
req.put("pageSize", pageSize);
|
||||||
|
|
||||||
|
String respText;
|
||||||
|
try {
|
||||||
|
respText = HttpUtil.sendPost(url, req, headers);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.error("Attendance request failed, day={}, pageNum={}", day, pageNum, ex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fetched = appendRowsFromResponse(rows, respText, day, pageNum);
|
||||||
|
if (fetched <= 0 || fetched < pageSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
day = day.plusDays(1);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int appendRowsFromResponse(List<Map<String, Object>> rows, String respText, LocalDate day, int pageNum) {
|
||||||
|
if (respText == null || respText.trim().isEmpty()) {
|
||||||
|
LOG.warn("Attendance response empty, day={}, pageNum={}", day, pageNum);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
JSONObject resp;
|
||||||
|
try {
|
||||||
|
resp = JSONObject.fromObject(respText);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.error("Attendance response parse error, day={}, pageNum={}, body={}", day, pageNum, shorten(respText), ex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
boolean success = "true".equalsIgnoreCase(String.valueOf(resp.opt("success")));
|
||||||
|
if (!success) {
|
||||||
|
LOG.warn("Attendance response unsuccessful, day={}, pageNum={}, message={}", day, pageNum, resp.optString("message"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
JSONArray data = resp.optJSONArray("data");
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < data.size(); i++) {
|
||||||
|
Object item = data.get(i);
|
||||||
|
JSONObject dto = item instanceof JSONObject ? (JSONObject) item : JSONObject.fromObject(item);
|
||||||
|
rows.add(mapRemoteRecord(dto));
|
||||||
|
}
|
||||||
|
return data.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> buildDhHeaders(HttpServletRequest request) {
|
||||||
|
String ip = getProp("attendance.api.deviceIp", "192.168.1.108");
|
||||||
|
String user = getProp("attendance.api.deviceUser", "admin");
|
||||||
|
String port = getProp("attendance.api.devicePort", "37777");
|
||||||
|
// String ts = safe(request.getParameter("dhTs"));
|
||||||
|
// if (ts.isEmpty()) {
|
||||||
|
// ts = String.valueOf(System.nanoTime());
|
||||||
|
// }
|
||||||
|
String ts = nextUniqueTimestamp();
|
||||||
|
|
||||||
|
String plainPwd = getProp("attendance.api.devicePassword", "");
|
||||||
|
if (plainPwd.isEmpty()) {
|
||||||
|
LOG.warn("attendance.api.devicePassword is empty, DH attendance call may fail auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> headers;
|
||||||
|
try {
|
||||||
|
headers = DeviceAccessHttpUtil.buildDeviceHeaders(ip, user, plainPwd, ts, port);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException("Build DH device headers failed", ex);
|
||||||
|
}
|
||||||
|
headers.put("Content-Type", "application/json");
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> mapRemoteRecord(JSONObject dto) {
|
||||||
|
String eventTime = dto.optString("eventTime");
|
||||||
|
String attendanceDate = extractDate(eventTime);
|
||||||
|
String checkInTime = extractTime(eventTime);
|
||||||
|
|
||||||
|
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
||||||
|
row.put("id", buildRowId(dto, attendanceDate, checkInTime));
|
||||||
|
row.put("employeeNo", firstNonEmpty(dto.optString("userId"), dto.optString("cardNo")));
|
||||||
|
row.put("employeeName", firstNonEmpty(dto.optString("userName"), dto.optString("userId"), "--"));
|
||||||
|
row.put("deptName", firstNonEmpty(dto.optString("deptName"), "--"));
|
||||||
|
row.put("attendanceDate", attendanceDate);
|
||||||
|
row.put("checkInTime", checkInTime);
|
||||||
|
row.put("checkOutTime", "--");
|
||||||
|
row.put("workHours", "--");
|
||||||
|
row.put("status", resolveStatus(dto));
|
||||||
|
row.put("source", "第三方接口(DH)");
|
||||||
|
|
||||||
|
// Keep raw fields for troubleshooting and later expansion.
|
||||||
|
row.put("cardNo", dto.optString("cardNo"));
|
||||||
|
row.put("channel", dto.optInt("channel"));
|
||||||
|
row.put("openMethod", dto.optInt("openMethod"));
|
||||||
|
row.put("result", dto.optInt("result"));
|
||||||
|
row.put("errCode", dto.optInt("errCode"));
|
||||||
|
row.put("attendanceState", dto.optInt("attendanceState"));
|
||||||
|
row.put("attendanceStateText", dto.optString("attendanceStateText"));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveStatus(JSONObject dto) {
|
||||||
|
String text = safe(dto.optString("attendanceStateText"));
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
int state = dto.optInt("attendanceState", -1);
|
||||||
|
if (state == 0) {
|
||||||
|
return "正常";
|
||||||
|
}
|
||||||
|
if (state == 1) {
|
||||||
|
return "迟到";
|
||||||
|
}
|
||||||
|
if (state == 2) {
|
||||||
|
return "早退";
|
||||||
|
}
|
||||||
|
if (state == 3) {
|
||||||
|
return "缺卡";
|
||||||
|
}
|
||||||
|
return "未知";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildRowId(JSONObject dto, String date, String time) {
|
||||||
|
String uid = safe(dto.optString("userId"));
|
||||||
|
String idx = String.valueOf(dto.optInt("index", 0));
|
||||||
|
return firstNonEmpty(uid, "U") + "-" + safe(date) + "-" + safe(time) + "-" + idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonEmpty(String... values) {
|
||||||
|
if (values == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (String v : values) {
|
||||||
|
if (v != null && !v.trim().isEmpty()) {
|
||||||
|
return v.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractDate(String eventTime) {
|
||||||
|
String v = safe(eventTime);
|
||||||
|
if (v.length() >= 10) {
|
||||||
|
return v.substring(0, 10);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractTime(String eventTime) {
|
||||||
|
String v = safe(eventTime);
|
||||||
|
if (v.contains("T") && v.length() >= 19) {
|
||||||
|
return v.substring(11, 19);
|
||||||
|
}
|
||||||
|
int blank = v.indexOf(' ');
|
||||||
|
if (blank >= 0 && v.length() >= blank + 9) {
|
||||||
|
return v.substring(blank + 1, blank + 9);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDate[] resolveRange(LocalDate startDate, LocalDate endDate) {
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
LocalDate start = startDate;
|
||||||
|
LocalDate end = endDate;
|
||||||
|
if (start == null && end == null) {
|
||||||
|
// No date filter from UI: fetch only current day.
|
||||||
|
start = now;
|
||||||
|
end = now;
|
||||||
|
} else if (start == null) {
|
||||||
|
// Only end date provided: query that single day.
|
||||||
|
start = end;
|
||||||
|
} else if (end == null) {
|
||||||
|
// Only start date provided: query that single day.
|
||||||
|
end = start;
|
||||||
|
}
|
||||||
|
if (start.isAfter(end)) {
|
||||||
|
LocalDate t = start;
|
||||||
|
start = end;
|
||||||
|
end = t;
|
||||||
|
}
|
||||||
|
return new LocalDate[]{start, end};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Properties loadAttendanceProps() {
|
||||||
|
Properties p = new Properties();
|
||||||
|
try (InputStream in = AttendanceController.class.getClassLoader().getResourceAsStream("thirdRequest.properties")) {
|
||||||
|
if (in != null) {
|
||||||
|
p.load(in);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.warn("Load thirdRequest.properties failed", ex);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getProp(String key, String defaultValue) {
|
||||||
|
String v = ATTENDANCE_PROPS.getProperty(key);
|
||||||
|
return v == null || v.trim().isEmpty() ? defaultValue : v.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getIntProp(String key, int defaultValue) {
|
||||||
|
String v = getProp(key, String.valueOf(defaultValue));
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(v);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shorten(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String oneLine = text.replace('\n', ' ').replace('\r', ' ');
|
||||||
|
return oneLine.length() > 400 ? oneLine.substring(0, 400) + "..." : oneLine;
|
||||||
|
}
|
||||||
|
|
||||||
private static void sortRows(List<Map<String, Object>> rows, String sort, String order) {
|
private static void sortRows(List<Map<String, Object>> rows, String sort, String order) {
|
||||||
final boolean asc = "asc".equalsIgnoreCase(order);
|
final boolean asc = "asc".equalsIgnoreCase(order);
|
||||||
final String sortField = (sort == null || sort.trim().isEmpty() || "id".equals(sort)) ? "attendanceDate" : sort;
|
final String sortField = (sort == null || sort.trim().isEmpty() || "id".equals(sort)) ? "attendanceDate" : sort;
|
||||||
@ -128,61 +364,6 @@ public class AttendanceController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Map<String, Object>> buildMockRows() {
|
|
||||||
String[][] employees = {
|
|
||||||
{"E0001", "张三", "生产部"},
|
|
||||||
{"E0002", "李四", "设备部"},
|
|
||||||
{"E0003", "王五", "品控部"},
|
|
||||||
{"E0004", "赵六", "仓储部"},
|
|
||||||
{"E0005", "钱七", "行政部"}
|
|
||||||
};
|
|
||||||
|
|
||||||
List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
int idSeq = 1;
|
|
||||||
|
|
||||||
for (int d = 0; d < 45; d++) {
|
|
||||||
LocalDate day = today.minusDays(d);
|
|
||||||
String dayStr = DAY_FMT.format(day);
|
|
||||||
for (int i = 0; i < employees.length; i++) {
|
|
||||||
String[] emp = employees[i];
|
|
||||||
int flag = d + i;
|
|
||||||
|
|
||||||
String status = "正常";
|
|
||||||
String checkIn = "08:55";
|
|
||||||
String checkOut = "18:05";
|
|
||||||
String workHours = "8.5";
|
|
||||||
|
|
||||||
if (flag % 19 == 0) {
|
|
||||||
status = "缺卡";
|
|
||||||
checkOut = "--";
|
|
||||||
workHours = "4.2";
|
|
||||||
} else if (flag % 11 == 0) {
|
|
||||||
status = "迟到";
|
|
||||||
checkIn = "09:" + (10 + (flag % 20));
|
|
||||||
workHours = "7.8";
|
|
||||||
} else if (flag % 13 == 0) {
|
|
||||||
status = "早退";
|
|
||||||
checkOut = "17:" + (20 + (flag % 30));
|
|
||||||
workHours = "7.1";
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
|
||||||
row.put("id", "mock-" + idSeq++);
|
|
||||||
row.put("employeeNo", emp[0]);
|
|
||||||
row.put("employeeName", emp[1]);
|
|
||||||
row.put("deptName", emp[2]);
|
|
||||||
row.put("attendanceDate", dayStr);
|
|
||||||
row.put("checkInTime", checkIn);
|
|
||||||
row.put("checkOutTime", checkOut);
|
|
||||||
row.put("workHours", workHours);
|
|
||||||
row.put("status", status);
|
|
||||||
row.put("source", "第三方接口(MOCK)");
|
|
||||||
rows.add(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String safe(String s) {
|
private static String safe(String s) {
|
||||||
return s == null ? "" : s.trim();
|
return s == null ? "" : s.trim();
|
||||||
@ -202,4 +383,17 @@ public class AttendanceController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final AtomicLong TS_SEQ = new AtomicLong();
|
||||||
|
|
||||||
|
private static String nextUniqueTimestamp() {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
while (true) {
|
||||||
|
long prev = TS_SEQ.get();
|
||||||
|
long next = now > prev ? now : prev + 1;
|
||||||
|
if (TS_SEQ.compareAndSet(prev, next)) {
|
||||||
|
return String.valueOf(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
package com.sipai.controller.jsyw;
|
package com.sipai.controller.jsyw;
|
||||||
|
|
||||||
|
import com.sipai.tools.DeviceAccessHttpUtil;
|
||||||
|
import com.sipai.tools.HttpUtil;
|
||||||
import net.sf.json.JSONArray;
|
import net.sf.json.JSONArray;
|
||||||
|
import net.sf.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@ -8,15 +13,27 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/jsyw/vehicleGate")
|
@RequestMapping("/jsyw/vehicleGate")
|
||||||
public class VehicleGateController {
|
public class VehicleGateController {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(VehicleGateController.class);
|
||||||
|
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
private static final int DEFAULT_REMOTE_PAGE_SIZE = 200;
|
||||||
|
private static final int MAX_REMOTE_PAGES_PER_DAY = 100;
|
||||||
|
private static final String DEFAULT_API_URL = "http://127.0.0.1:8090/dh-netsdk/vehicleGate/getDailyRecognizedVehicleList";
|
||||||
|
private static final Properties VEHICLE_GATE_PROPS = loadVehicleGateProps();
|
||||||
|
private static final AtomicLong TS_SEQ = new AtomicLong();
|
||||||
|
|
||||||
@RequestMapping("/showList.do")
|
@RequestMapping("/showList.do")
|
||||||
public String showList(HttpServletRequest request, Model model) {
|
public String showList(HttpServletRequest request, Model model) {
|
||||||
@ -31,14 +48,18 @@ public class VehicleGateController {
|
|||||||
String passDate = safeStr(request.getParameter("passDate"));
|
String passDate = safeStr(request.getParameter("passDate"));
|
||||||
String direction = safeStr(request.getParameter("direction"));
|
String direction = safeStr(request.getParameter("direction"));
|
||||||
String status = safeStr(request.getParameter("status"));
|
String status = safeStr(request.getParameter("status"));
|
||||||
|
LocalDate queryDay = parseDay(passDate);
|
||||||
|
if (queryDay == null) {
|
||||||
|
queryDay = LocalDate.now();
|
||||||
|
}
|
||||||
|
|
||||||
List<Map<String, Object>> mockRows = buildMockRows();
|
List<Map<String, Object>> remoteRows = loadRowsFromRemote(queryDay, plateNo);
|
||||||
List<Map<String, Object>> filteredRows = new ArrayList<Map<String, Object>>();
|
List<Map<String, Object>> filteredRows = new ArrayList<Map<String, Object>>();
|
||||||
int inCount = 0;
|
int inCount = 0;
|
||||||
int outCount = 0;
|
int outCount = 0;
|
||||||
int abnormalCount = 0;
|
int abnormalCount = 0;
|
||||||
|
|
||||||
for (Map<String, Object> item : mockRows) {
|
for (Map<String, Object> item : remoteRows) {
|
||||||
String itemPlateNo = safeStr(item.get("plateNo")).toUpperCase(Locale.ROOT);
|
String itemPlateNo = safeStr(item.get("plateNo")).toUpperCase(Locale.ROOT);
|
||||||
String itemPassTime = safeStr(item.get("passTime"));
|
String itemPassTime = safeStr(item.get("passTime"));
|
||||||
String itemDirection = safeStr(item.get("direction"));
|
String itemDirection = safeStr(item.get("direction"));
|
||||||
@ -69,46 +90,209 @@ public class VehicleGateController {
|
|||||||
pageRows = filteredRows.subList(pageStart, pageEnd);
|
pageRows = filteredRows.subList(pageStart, pageEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONArray rowsJson = JSONArray.fromObject(pageRows);
|
JSONObject result = new JSONObject();
|
||||||
String result = "{"
|
result.put("total", total);
|
||||||
+ "\"total\":" + total + ","
|
result.put("rows", JSONArray.fromObject(pageRows));
|
||||||
+ "\"rows\":" + rowsJson + ","
|
result.put("summaryInCount", inCount);
|
||||||
+ "\"summaryInCount\":" + inCount + ","
|
result.put("summaryOutCount", outCount);
|
||||||
+ "\"summaryOutCount\":" + outCount + ","
|
result.put("summaryInsideCount", inCount - outCount);
|
||||||
+ "\"summaryInsideCount\":" + (inCount - outCount) + ","
|
result.put("summaryAbnormalCount", abnormalCount);
|
||||||
+ "\"summaryAbnormalCount\":" + abnormalCount
|
model.addAttribute("result", result.toString());
|
||||||
+ "}";
|
|
||||||
model.addAttribute("result", result);
|
|
||||||
return new ModelAndView("result");
|
return new ModelAndView("result");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String safeStr(Object value) {
|
private List<Map<String, Object>> loadRowsFromRemote(LocalDate day, String plateNo) {
|
||||||
return value == null ? "" : String.valueOf(value).trim();
|
List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
|
||||||
|
int pageSize = getIntProp("vehicleGate.api.pageSize", DEFAULT_REMOTE_PAGE_SIZE);
|
||||||
|
String url = getProp("vehicleGate.api.url", DEFAULT_API_URL);
|
||||||
|
|
||||||
|
for (int pageNum = 1; pageNum <= MAX_REMOTE_PAGES_PER_DAY; pageNum++) {
|
||||||
|
com.alibaba.fastjson.JSONObject req = new com.alibaba.fastjson.JSONObject();
|
||||||
|
req.put("day", DAY_FMT.format(day));
|
||||||
|
req.put("plateNumber", plateNo);
|
||||||
|
req.put("pageNum", pageNum);
|
||||||
|
req.put("pageSize", pageSize);
|
||||||
|
|
||||||
|
String ts = nextUniqueTimestamp();
|
||||||
|
Map<String, String> headers = buildDhHeaders(ts);
|
||||||
|
|
||||||
|
String respText;
|
||||||
|
try {
|
||||||
|
respText = HttpUtil.sendPost(url, req, headers);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.error("VehicleGate request failed, day={}, pageNum={}", day, pageNum, ex);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Map<String, Object>> buildMockRows() {
|
int fetched = appendRowsFromResponse(rows, respText, day, pageNum);
|
||||||
List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
|
if (fetched <= 0 || fetched < pageSize) {
|
||||||
rows.add(buildRow("鲁A12345", "IN", "2026-03-03 08:12:21", "东门1号闸", "张三", "NORMAL", "自动识别放行"));
|
break;
|
||||||
rows.add(buildRow("鲁B66K88", "OUT", "2026-03-03 08:18:46", "东门1号闸", "李四", "NORMAL", "自动识别放行"));
|
}
|
||||||
rows.add(buildRow("鲁C99871", "IN", "2026-03-03 08:27:19", "南门2号闸", "王五", "ABNORMAL", "车牌识别异常,人工放行"));
|
}
|
||||||
rows.add(buildRow("鲁A77889", "IN", "2026-03-03 09:04:52", "北门1号闸", "赵六", "NORMAL", "自动识别放行"));
|
|
||||||
rows.add(buildRow("鲁D22319", "OUT", "2026-03-03 09:19:11", "南门2号闸", "钱七", "NORMAL", "自动识别放行"));
|
|
||||||
rows.add(buildRow("鲁E55120", "IN", "2026-03-03 10:03:35", "西门1号闸", "孙八", "NORMAL", "访客车辆"));
|
|
||||||
rows.add(buildRow("鲁F90111", "OUT", "2026-03-03 10:16:05", "北门1号闸", "周九", "ABNORMAL", "未登记离场,值班确认"));
|
|
||||||
rows.add(buildRow("鲁A0P365", "IN", "2026-03-03 10:42:30", "东门1号闸", "吴十", "NORMAL", "自动识别放行"));
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildRow(String plateNo, String direction, String passTime, String gateName,
|
private int appendRowsFromResponse(List<Map<String, Object>> rows, String respText, LocalDate day, int pageNum) {
|
||||||
String driverName, String status, String note) {
|
if (respText == null || respText.trim().isEmpty()) {
|
||||||
Map<String, Object> row = new HashMap<String, Object>();
|
LOG.warn("VehicleGate response empty, day={}, pageNum={}", day, pageNum);
|
||||||
row.put("plateNo", plateNo);
|
return 0;
|
||||||
row.put("direction", direction);
|
}
|
||||||
row.put("passTime", passTime);
|
JSONObject resp;
|
||||||
row.put("gateName", gateName);
|
try {
|
||||||
row.put("driverName", driverName);
|
resp = JSONObject.fromObject(respText);
|
||||||
row.put("status", status);
|
} catch (Exception ex) {
|
||||||
row.put("note", note);
|
LOG.error("VehicleGate response parse error, day={}, pageNum={}, body={}", day, pageNum, shorten(respText), ex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
boolean success = "true".equalsIgnoreCase(String.valueOf(resp.opt("success")));
|
||||||
|
if (!success) {
|
||||||
|
LOG.warn("VehicleGate response unsuccessful, day={}, pageNum={}, message={}", day, pageNum, resp.optString("message"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
JSONArray data = resp.optJSONArray("data");
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < data.size(); i++) {
|
||||||
|
Object item = data.get(i);
|
||||||
|
JSONObject dto = item instanceof JSONObject ? (JSONObject) item : JSONObject.fromObject(item);
|
||||||
|
rows.add(mapRemoteRecord(dto));
|
||||||
|
}
|
||||||
|
return data.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> buildDhHeaders(String ts) {
|
||||||
|
String ip = getProp("vehicleGate.api.deviceIp", "192.168.1.108");
|
||||||
|
String user = getProp("vehicleGate.api.deviceUser", "admin");
|
||||||
|
String port = getProp("vehicleGate.api.devicePort", "37777");
|
||||||
|
String plainPwd = getProp("vehicleGate.api.devicePassword", "");
|
||||||
|
if (plainPwd.isEmpty()) {
|
||||||
|
LOG.warn("vehicleGate.api.devicePassword is empty, vehicle gate call may fail auth");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Map<String, String> headers = DeviceAccessHttpUtil.buildDeviceHeaders(ip, user, plainPwd, ts, port);
|
||||||
|
headers.put("Content-Type", "application/json");
|
||||||
|
return headers;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException("Build vehicle gate headers failed", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nextUniqueTimestamp() {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
while (true) {
|
||||||
|
long prev = TS_SEQ.get();
|
||||||
|
long next = now > prev ? now : prev + 1;
|
||||||
|
if (TS_SEQ.compareAndSet(prev, next)) {
|
||||||
|
return String.valueOf(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> mapRemoteRecord(JSONObject dto) {
|
||||||
|
String eventTime = safeStr(dto.optString("eventTime"));
|
||||||
|
int channel = dto.optInt("channel", -1);
|
||||||
|
int lane = dto.optInt("lane", -1);
|
||||||
|
|
||||||
|
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
||||||
|
row.put("plateNo", safeStr(dto.optString("plateNumber")));
|
||||||
|
row.put("direction", resolveDirection(dto));
|
||||||
|
row.put("passTime", eventTime);
|
||||||
|
row.put("gateName", buildGateName(channel, lane));
|
||||||
|
row.put("driverName", "--");
|
||||||
|
row.put("status", resolveStatus(dto));
|
||||||
|
row.put("note", firstNonEmpty(safeStr(dto.optString("allowStatusText")), "通道" + channel + " 车道" + lane));
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String resolveDirection(JSONObject dto) {
|
||||||
|
String text = safeStr(dto.optString("allowStatusText"));
|
||||||
|
if (text.contains("出")) {
|
||||||
|
return "OUT";
|
||||||
|
}
|
||||||
|
if (text.contains("进") || text.contains("入")) {
|
||||||
|
return "IN";
|
||||||
|
}
|
||||||
|
int lane = dto.optInt("lane", -1);
|
||||||
|
if (lane > 0 && lane % 2 == 0) {
|
||||||
|
return "OUT";
|
||||||
|
}
|
||||||
|
return "IN";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveStatus(JSONObject dto) {
|
||||||
|
int allow = dto.optInt("allowStatus", -1);
|
||||||
|
if (allow == 1) {
|
||||||
|
return "NORMAL";
|
||||||
|
}
|
||||||
|
return "ABNORMAL";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildGateName(int channel, int lane) {
|
||||||
|
if (channel < 0 && lane < 0) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
return "通道" + channel + "-车道" + lane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeStr(Object value) {
|
||||||
|
return value == null ? "" : String.valueOf(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonEmpty(String... values) {
|
||||||
|
if (values == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.trim().isEmpty()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDate parseDay(String day) {
|
||||||
|
if (day == null || day.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(day.trim(), DAY_FMT);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Properties loadVehicleGateProps() {
|
||||||
|
Properties p = new Properties();
|
||||||
|
try (InputStream in = VehicleGateController.class.getClassLoader().getResourceAsStream("thirdRequest.properties")) {
|
||||||
|
if (in != null) {
|
||||||
|
p.load(in);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.warn("Load thirdRequest.properties failed", ex);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getProp(String key, String defaultValue) {
|
||||||
|
String v = VEHICLE_GATE_PROPS.getProperty(key);
|
||||||
|
return v == null || v.trim().isEmpty() ? defaultValue : v.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getIntProp(String key, int defaultValue) {
|
||||||
|
String v = getProp(key, String.valueOf(defaultValue));
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(v);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shorten(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String oneLine = text.replace('\n', ' ').replace('\r', ' ');
|
||||||
|
return oneLine.length() > 400 ? oneLine.substring(0, 400) + "..." : oneLine;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
261
src/main/java/com/sipai/tools/DeviceAccessHttpUtil.java
Normal file
261
src/main/java/com/sipai/tools/DeviceAccessHttpUtil.java
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
package com.sipai.tools;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备HTTP访问工具类:
|
||||||
|
* <ul>
|
||||||
|
* <li>发送时自动填充设备相关请求头</li>
|
||||||
|
* <li>接收时解析并校验请求头,防止重放、参数篡改、时间戳过期等安全风险</li>
|
||||||
|
* <li>提供AES加密/解密工具,保护敏感信息</li>
|
||||||
|
* <li>支持配置化密钥和时间戳有效期,便于运维和安全加固</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 主要头部:
|
||||||
|
* Dh-Device-Ip、Dh-Device-User、Dh-Device-Password、Dh-Device-Timestamp、Dh-Device-Port
|
||||||
|
* <p>
|
||||||
|
* 推荐通过Spring注入本工具类,避免静态方法带来的配置注入问题。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DeviceAccessHttpUtil {
|
||||||
|
/**
|
||||||
|
* AES加密密钥,长度16位。可通过配置覆盖,避免硬编码泄露。
|
||||||
|
*/
|
||||||
|
private static String AES_KEY = "NetSDK1234567890"; // 16位密钥
|
||||||
|
@Value("${crypto.aes-key:NetSDK1234567890}")
|
||||||
|
public void setAesKey(String aesKey) {
|
||||||
|
String key = aesKey == null ? "" : aesKey.trim();
|
||||||
|
if (key.length() != 16 && key.length() != 24 && key.length() != 32) {
|
||||||
|
throw new IllegalArgumentException("crypto.aes-key length must be 16/24/32");
|
||||||
|
}
|
||||||
|
AES_KEY = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备请求时间戳有效期(毫秒),默认5分钟。可通过配置覆盖。
|
||||||
|
* 注意:内部会转换为微秒进行比较,以支持微秒级时间戳精度。
|
||||||
|
*/
|
||||||
|
private static long TIMESTAMP_EXPIRE_MS = 300000L;
|
||||||
|
@Value("${device.timestamp.expire-ms:300000}")
|
||||||
|
public void setTimestampExpireMs(long timestampExpireMs) {
|
||||||
|
TIMESTAMP_EXPIRE_MS = timestampExpireMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String ALGORITHM = "AES";
|
||||||
|
|
||||||
|
// 设备 HTTP 头部 key 常量,便于全局复用和维护
|
||||||
|
public static final String HEADER_DEVICE_IP = "Dh-Device-Ip";
|
||||||
|
public static final String HEADER_DEVICE_USER = "Dh-Device-User";
|
||||||
|
public static final String HEADER_DEVICE_PASSWORD = "Dh-Device-Password";
|
||||||
|
public static final String HEADER_DEVICE_TIMESTAMP = "Dh-Device-Timestamp";
|
||||||
|
public static final String HEADER_DEVICE_PORT = "Dh-Device-Port";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防重放 nonce 集合及最大容量,防止同一加密串被多次使用。
|
||||||
|
*/
|
||||||
|
private static final int NONCE_SET_MAX_SIZE = 10000;
|
||||||
|
private static final java.util.Set<String> usedNonceSet = java.util.Collections.synchronizedSet(new java.util.LinkedHashSet<>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造设备访问请求头,自动加密密码并填充所有必需字段。
|
||||||
|
* 使用方式:获取返回结果,遍历map kv,直接填充到请求头。
|
||||||
|
* @param ip 设备IP
|
||||||
|
* @param user 用户名
|
||||||
|
* @param password 明文密码
|
||||||
|
* @param timestamp 时间戳(建议用System.nanoTime()获取微秒级时间戳)
|
||||||
|
* @param port 端口号
|
||||||
|
* @return 设备请求头Map
|
||||||
|
* @throws Exception 加密异常
|
||||||
|
*/
|
||||||
|
public static Map<String, String> buildDeviceHeaders(String ip, String user, String password, String timestamp, String port) throws Exception {
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
headers.put(HEADER_DEVICE_IP, ip);
|
||||||
|
headers.put(HEADER_DEVICE_USER, user);
|
||||||
|
headers.put(HEADER_DEVICE_TIMESTAMP, timestamp);
|
||||||
|
headers.put(HEADER_DEVICE_PORT, port);
|
||||||
|
// 密码加密(password|timestamp)
|
||||||
|
String encPwd = encryptWithTimestamp(password, timestamp);
|
||||||
|
headers.put(HEADER_DEVICE_PASSWORD, encPwd);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析并校验设备请求头,返回解析结果DeviceAccessInfo。
|
||||||
|
* 校验项:参数完整性、密码解密、时间戳一致性、防重放、端口校验。
|
||||||
|
* 校验失败抛异常。
|
||||||
|
* @param request HttpServletRequest
|
||||||
|
* @return DeviceAccessInfo(ip、user、password、timestamp、port)
|
||||||
|
* @throws IllegalArgumentException 校验或解密异常
|
||||||
|
*/
|
||||||
|
public static DeviceAccessInfo parseAndValidateDeviceHeaders(javax.servlet.http.HttpServletRequest request) {
|
||||||
|
String ip = request.getHeader(HEADER_DEVICE_IP);
|
||||||
|
String encPwd = request.getHeader(HEADER_DEVICE_PASSWORD);
|
||||||
|
String user = request.getHeader(HEADER_DEVICE_USER);
|
||||||
|
String ts = request.getHeader(HEADER_DEVICE_TIMESTAMP);
|
||||||
|
String portStr = request.getHeader(HEADER_DEVICE_PORT);
|
||||||
|
if (user == null || user.isEmpty()) user = "admin";
|
||||||
|
return parseAndValidateDeviceHeaders(ip, encPwd, user, ts, portStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一参数校验和解密逻辑,失败抛异常,成功返回DeviceAccessInfo。
|
||||||
|
* <ul>
|
||||||
|
* <li>参数完整性校验</li>
|
||||||
|
* <li>重放攻击防护(同一加密串只允许用一次)</li>
|
||||||
|
* <li>密码解密与格式校验</li>
|
||||||
|
* <li>时间戳一致性与有效期校验</li>
|
||||||
|
* <li>端口号格式校验</li>
|
||||||
|
* </ul>
|
||||||
|
* @param ip 设备IP
|
||||||
|
* @param encPwd 加密密码
|
||||||
|
* @param user 用户名
|
||||||
|
* @param ts 时间戳
|
||||||
|
* @param portStr 端口字符串
|
||||||
|
* @return DeviceAccessInfo 校验通过的设备信息
|
||||||
|
* @throws IllegalArgumentException 校验失败
|
||||||
|
*/
|
||||||
|
public static DeviceAccessInfo parseAndValidateDeviceHeaders(String ip, String encPwd, String user, String ts, String portStr) {
|
||||||
|
if (ip == null || encPwd == null || ts == null) {
|
||||||
|
throw new IllegalArgumentException("缺少设备登录信息: ip, encPwd, ts");
|
||||||
|
}
|
||||||
|
String nonce = encPwd;
|
||||||
|
synchronized (usedNonceSet) {
|
||||||
|
if (usedNonceSet.contains(nonce)) {
|
||||||
|
throw new IllegalArgumentException("请求重放: nonce=" + nonce);
|
||||||
|
}
|
||||||
|
usedNonceSet.add(nonce);
|
||||||
|
if (usedNonceSet.size() > NONCE_SET_MAX_SIZE) {
|
||||||
|
String first = usedNonceSet.iterator().next();
|
||||||
|
usedNonceSet.remove(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String[] pwdTsNonce;
|
||||||
|
try {
|
||||||
|
pwdTsNonce = decryptWithTimestamp(encPwd);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("密码解密异常: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
if (pwdTsNonce.length != 2) {
|
||||||
|
throw new IllegalArgumentException("密码解密格式错误: encPwd=" + encPwd);
|
||||||
|
}
|
||||||
|
String pwd = pwdTsNonce[0];
|
||||||
|
String tsInPwd = pwdTsNonce[1];
|
||||||
|
if (!ts.equals(tsInPwd)) {
|
||||||
|
throw new IllegalArgumentException("时间戳校验失败: ts=" + ts + ", tsInPwd=" + tsInPwd);
|
||||||
|
}
|
||||||
|
long now = System.nanoTime();
|
||||||
|
long tsLong;
|
||||||
|
try { tsLong = Long.parseLong(ts); } catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("时间戳格式错误: ts=" + ts);
|
||||||
|
}
|
||||||
|
if (Math.abs(now - tsLong) > TIMESTAMP_EXPIRE_MS * 1000) { // 转换为微秒进行比较
|
||||||
|
throw new IllegalArgumentException("登录请求已过期: now=" + now + ", tsLong=" + tsLong + ", expireMs=" + TIMESTAMP_EXPIRE_MS);
|
||||||
|
}
|
||||||
|
int port = 37777;
|
||||||
|
if (portStr != null && !portStr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
port = Integer.parseInt(portStr);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("Device-Port格式错误: portStr=" + portStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new DeviceAccessInfo(ip, user, pwd, ts, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES加密(ECB模式,PKCS5Padding),用于加密敏感数据。
|
||||||
|
* @param data 明文
|
||||||
|
* @return base64编码的密文
|
||||||
|
* @throws Exception 加密异常
|
||||||
|
*/
|
||||||
|
private static String encrypt(String data) throws Exception {
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
|
||||||
|
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return Base64.getEncoder().encodeToString(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES解密(ECB模式,PKCS5Padding),用于解密敏感数据。
|
||||||
|
* @param encryptedData base64编码的密文
|
||||||
|
* @return 明文
|
||||||
|
* @throws Exception 解密异常
|
||||||
|
*/
|
||||||
|
private static String decrypt(String encryptedData) throws Exception {
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec);
|
||||||
|
byte[] decoded = Base64.getDecoder().decode(encryptedData);
|
||||||
|
byte[] decrypted = cipher.doFinal(decoded);
|
||||||
|
return new String(decrypted, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成加密字符串:password|timestamp
|
||||||
|
* @param password 明文密码
|
||||||
|
* @param timestamp 时间戳
|
||||||
|
* @return 加密后字符串
|
||||||
|
* @throws Exception 加密异常
|
||||||
|
*/
|
||||||
|
private static String encryptWithTimestamp(String password, String timestamp) throws Exception {
|
||||||
|
return encrypt(password + "|" + timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密并返回[password, timestamp]
|
||||||
|
* @param encryptedData 加密后字符串
|
||||||
|
* @return [password, timestamp]
|
||||||
|
* @throws Exception 解密异常
|
||||||
|
*/
|
||||||
|
private static String[] decryptWithTimestamp(String encryptedData) throws Exception {
|
||||||
|
String plain = decrypt(encryptedData);
|
||||||
|
return plain.split("\\|", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备访问信息结构体,封装所有校验通过的关键信息。
|
||||||
|
* 用于业务层安全登录、日志审计等场景。
|
||||||
|
*/
|
||||||
|
public static class DeviceAccessInfo {
|
||||||
|
/** 设备IP */
|
||||||
|
public final String ip;
|
||||||
|
/** 用户名 */
|
||||||
|
public final String user;
|
||||||
|
/** 明文密码(已解密) */
|
||||||
|
public final String password;
|
||||||
|
/** 时间戳 */
|
||||||
|
public final String timestamp;
|
||||||
|
/** 端口号 */
|
||||||
|
public final int port;
|
||||||
|
public DeviceAccessInfo(String ip, String user, String password, String timestamp, int port) {
|
||||||
|
this.ip = ip;
|
||||||
|
this.user = user;
|
||||||
|
this.password = password;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地测试主方法,演示加密解密流程。
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
String password = "admin";
|
||||||
|
String timestamp = String.valueOf(System.nanoTime());
|
||||||
|
String encrypted = encryptWithTimestamp(password, timestamp);
|
||||||
|
System.out.println("加密后: " + encrypted);
|
||||||
|
String[] decrypted = decryptWithTimestamp(encrypted);
|
||||||
|
System.out.println("解密后 password: " + decrypted[0]);
|
||||||
|
System.out.println("解密后 timestamp: " + decrypted[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,3 +3,28 @@ boturl=http://10.194.10.169:9206/nsapi/jqrsssj
|
|||||||
bottoken=http://10.194.10.169:9206/nsapi/getoken?objkey=sipai
|
bottoken=http://10.194.10.169:9206/nsapi/getoken?objkey=sipai
|
||||||
#interfaces=http://192.168.1.137:8080/BLG/assayMonthWater.do?method=sampleinto¶ms=
|
#interfaces=http://192.168.1.137:8080/BLG/assayMonthWater.do?method=sampleinto¶ms=
|
||||||
#interfaces=http://localhost:8099/SIPAIIS_WMS_HQAQ/whp/test/WhpSamplingPlanTaskAudit/test1111.do?method=sampleinto¶ms=
|
#interfaces=http://localhost:8099/SIPAIIS_WMS_HQAQ/whp/test/WhpSamplingPlanTaskAudit/test1111.do?method=sampleinto¶ms=
|
||||||
|
|
||||||
|
# Attendance third-party API settings
|
||||||
|
attendance.api.url=http://127.0.0.1:8090/dh-netsdk/attendance/getDailyRecordList
|
||||||
|
attendance.api.pageSize=200
|
||||||
|
attendance.api.deviceIp=192.168.1.108
|
||||||
|
attendance.api.deviceUser=admin
|
||||||
|
attendance.api.devicePort=37777
|
||||||
|
# Plain password used for header encryption.
|
||||||
|
attendance.api.devicePassword=
|
||||||
|
|
||||||
|
# DeviceAccessHttpUtil crypto settings
|
||||||
|
# AES key length must be 16/24/32 characters
|
||||||
|
crypto.aes-key=NetSDK1234567890
|
||||||
|
# Timestamp expiration (ms)
|
||||||
|
device.timestamp.expire-ms=300000
|
||||||
|
|
||||||
|
# Vehicle gate third-party API settings (independent from attendance.api.*)
|
||||||
|
vehicleGate.api.url=http://127.0.0.1:8090/dh-netsdk/vehicleGate/getDailyRecognizedVehicleList
|
||||||
|
vehicleGate.api.pageSize=200
|
||||||
|
vehicleGate.api.deviceIp=192.168.1.108
|
||||||
|
vehicleGate.api.deviceUser=admin
|
||||||
|
vehicleGate.api.devicePort=37777
|
||||||
|
# Plain password used for header encryption.
|
||||||
|
vehicleGate.api.devicePassword=
|
||||||
|
|
||||||
|
|||||||
@ -147,7 +147,7 @@
|
|||||||
<div class="box box-primary">
|
<div class="box box-primary">
|
||||||
<div class="box-header with-border">
|
<div class="box-header with-border">
|
||||||
<h3 class="box-title">员工考勤记录</h3>
|
<h3 class="box-title">员工考勤记录</h3>
|
||||||
<span class="text-muted" style="margin-left: 10px;">当前为第三方考勤接口模拟数据(MOCK)</span>
|
<span class="text-muted" style="margin-left: 10px;">当前为第三方考勤接口实时数据</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<div class="form-inline" style="margin-bottom: 10px;">
|
<div class="form-inline" style="margin-bottom: 10px;">
|
||||||
|
|||||||
Reference in New Issue
Block a user