人脸/车闸接口整合

This commit is contained in:
Timer
2026-04-29 15:51:02 +08:00
parent e5c3849948
commit 12447a547c
5 changed files with 771 additions and 107 deletions

View File

@ -1,7 +1,11 @@
package com.sipai.controller.administration;
import com.sipai.tools.HttpUtil;
import com.sipai.tools.DeviceAccessHttpUtil;
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.ui.Model;
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 javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@ -17,22 +22,19 @@ import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
//curl --location 'http://127.0.0.1:8090/dh-netsdk/attendance/getDailyRecordList' \
// header 'Content-Type: application/json' \
// 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
// }'
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
@Controller
@RequestMapping("/administration/attendance")
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 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")
public String showList(HttpServletRequest request, Model model) {
@ -53,7 +55,8 @@ public class AttendanceController {
LocalDate startDate = parseDay(start);
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>>();
for (Map<String, Object> item : allRows) {
@ -75,10 +78,10 @@ public class AttendanceController {
if (!status.isEmpty() && !status.equals(st)) {
continue;
}
if (startDate != null && day != null && day.isBefore(startDate)) {
if (startDate != null && (day == null || day.isBefore(startDate))) {
continue;
}
if (endDate != null && day != null && day.isAfter(endDate)) {
if (endDate != null && (day == null || day.isAfter(endDate))) {
continue;
}
filtered.add(item);
@ -97,6 +100,239 @@ public class AttendanceController {
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) {
final boolean asc = "asc".equalsIgnoreCase(order);
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) {
return s == null ? "" : s.trim();
@ -202,4 +383,17 @@ public class AttendanceController {
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);
}
}
}
}

View File

@ -1,6 +1,11 @@
package com.sipai.controller.jsyw;
import com.sipai.tools.DeviceAccessHttpUtil;
import com.sipai.tools.HttpUtil;
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.ui.Model;
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 javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
@Controller
@RequestMapping("/jsyw/vehicleGate")
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")
public String showList(HttpServletRequest request, Model model) {
@ -31,14 +48,18 @@ public class VehicleGateController {
String passDate = safeStr(request.getParameter("passDate"));
String direction = safeStr(request.getParameter("direction"));
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>>();
int inCount = 0;
int outCount = 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 itemPassTime = safeStr(item.get("passTime"));
String itemDirection = safeStr(item.get("direction"));
@ -69,46 +90,209 @@ public class VehicleGateController {
pageRows = filteredRows.subList(pageStart, pageEnd);
}
JSONArray rowsJson = JSONArray.fromObject(pageRows);
String result = "{"
+ "\"total\":" + total + ","
+ "\"rows\":" + rowsJson + ","
+ "\"summaryInCount\":" + inCount + ","
+ "\"summaryOutCount\":" + outCount + ","
+ "\"summaryInsideCount\":" + (inCount - outCount) + ","
+ "\"summaryAbnormalCount\":" + abnormalCount
+ "}";
model.addAttribute("result", result);
JSONObject result = new JSONObject();
result.put("total", total);
result.put("rows", JSONArray.fromObject(pageRows));
result.put("summaryInCount", inCount);
result.put("summaryOutCount", outCount);
result.put("summaryInsideCount", inCount - outCount);
result.put("summaryAbnormalCount", abnormalCount);
model.addAttribute("result", result.toString());
return new ModelAndView("result");
}
private String safeStr(Object value) {
return value == null ? "" : String.valueOf(value).trim();
}
private List<Map<String, Object>> buildMockRows() {
private List<Map<String, Object>> loadRowsFromRemote(LocalDate day, String plateNo) {
List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
rows.add(buildRow("鲁A12345", "IN", "2026-03-03 08:12:21", "东门1号闸", "张三", "NORMAL", "自动识别放行"));
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", "自动识别放行"));
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;
}
int fetched = appendRowsFromResponse(rows, respText, day, pageNum);
if (fetched <= 0 || fetched < pageSize) {
break;
}
}
return rows;
}
private Map<String, Object> buildRow(String plateNo, String direction, String passTime, String gateName,
String driverName, String status, String note) {
Map<String, Object> row = new HashMap<String, Object>();
row.put("plateNo", plateNo);
row.put("direction", direction);
row.put("passTime", passTime);
row.put("gateName", gateName);
row.put("driverName", driverName);
row.put("status", status);
row.put("note", note);
private int appendRowsFromResponse(List<Map<String, Object>> rows, String respText, LocalDate day, int pageNum) {
if (respText == null || respText.trim().isEmpty()) {
LOG.warn("VehicleGate response empty, day={}, pageNum={}", day, pageNum);
return 0;
}
JSONObject resp;
try {
resp = JSONObject.fromObject(respText);
} catch (Exception ex) {
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;
}
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;
}
}

View 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 DeviceAccessInfoip、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]);
}
}

View File

@ -3,3 +3,28 @@ boturl=http://10.194.10.169:9206/nsapi/jqrsssj
bottoken=http://10.194.10.169:9206/nsapi/getoken?objkey=sipai
#interfaces=http://192.168.1.137:8080/BLG/assayMonthWater.do?method=sampleinto&params=
#interfaces=http://localhost:8099/SIPAIIS_WMS_HQAQ/whp/test/WhpSamplingPlanTaskAudit/test1111.do?method=sampleinto&params=
# 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=

View File

@ -147,7 +147,7 @@
<div class="box box-primary">
<div class="box-header with-border">
<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 class="box-body">
<div class="form-inline" style="margin-bottom: 10px;">