Initial commit of Hanvon F710X Attendance Service
commit
6867cb90e7
|
|
@ -0,0 +1,363 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<connectionStrings>
|
||||
<add name="HrmsDb"
|
||||
connectionString="SERVER=192.168.90.147;PORT=3306;DATABASE=hrms;UID=utopia;PASSWORD=Utopia01;Connection Timeout=60;"
|
||||
providerName="MySql.Data.MySqlClient" />
|
||||
</connectionStrings>
|
||||
<appSettings>
|
||||
<add key="ATTENDANCE_SCOPE" value="CENTRAL" />
|
||||
<!--<add key="ATTENDANCE_SCOPE" value="SITE" />-->
|
||||
<add key="ATTENDANCE_JOB_INTERVAL_MINUTES" value="20" />
|
||||
<add key="TEMPLATE_JOB_INTERVAL_MINUTES" value="240" />
|
||||
<add key="TEMPLATE_SYNC_ENABLED" value="true" />
|
||||
<add key="TRANSFER_MODE" value="DEVICE_TO_DB" />
|
||||
<add key="DEVICE_SETTINGS_PATH" value="DeviceSettings.xml" />
|
||||
<add key="TEMPLATE_VERIFY_AFTER_PUSH" value="true" />
|
||||
<add key="TEMPLATE_DEVICE_DELAY_MS" value="0" />
|
||||
<add key="TEMPLATE_AUTO_DISTRIBUTE_AFTER_DEVICE_TO_DB" value="false" />
|
||||
<!-- Only machines with machine_type = HANVON are processed by this service -->
|
||||
<add key="MACHINE_TYPES" value="HANVON" />
|
||||
<!-- F710X uses HTTP web API (port 80). Set HDCP only for classic FaceID devices on 9922. -->
|
||||
<add key="DEVICE_PROTOCOL" value="HTTP" />
|
||||
<add key="DEVICE_HTTP_PORT" value="80" />
|
||||
<add key="DEVICE_FACE_BACKUPNUM" value="50" />
|
||||
<!-- Hanvon HDCP device type string used in DeviceInfo(...) when DEVICE_PROTOCOL=HDCP -->
|
||||
<add key="DEVICE_TYPE" value="HW_HDCP" />
|
||||
<!-- Device web UI / SDK auth (F710X) -->
|
||||
<add key="DEVICE_USERNAME" value="admin" />
|
||||
<add key="DEVICE_PASSWORD" value="12345678" />
|
||||
<!-- Remove attendance from device only after successful DB batch insert -->
|
||||
<add key="ATTENDANCE_DELETE_AFTER_INSERT" value="true" />
|
||||
<add key="DB_CONNECTION_RETRY_ATTEMPTS" value="3" />
|
||||
<add key="DB_CONNECTION_RETRY_DELAY_MS" value="2000" />
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.1" newVersion="4.0.1.1" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
class Attendance
|
||||
{
|
||||
private string acNo;
|
||||
private DateTime checkTime;
|
||||
private bool isProcessed;
|
||||
private string machineId;
|
||||
private string inOutTypeId;
|
||||
private string machineIp;
|
||||
private DateTime date;
|
||||
|
||||
public Attendance() {
|
||||
|
||||
}
|
||||
|
||||
public Attendance(string acNo, DateTime checkTime, bool isProcessed, string machineId, string inOutTypeId, string machineIp, DateTime date) {
|
||||
this.AcNo = acNo;
|
||||
this.CheckTime = checkTime;
|
||||
this.IsProcessed = isProcessed;
|
||||
this.MachineId = machineId;
|
||||
this.InOutTypeId = inOutTypeId;
|
||||
this.MachineIp = machineIp;
|
||||
this.Date = date;
|
||||
}
|
||||
|
||||
public string AcNo { get => acNo; set => acNo = value; }
|
||||
public DateTime CheckTime { get => checkTime; set => checkTime = value; }
|
||||
public bool IsProcessed { get => isProcessed; set => isProcessed = value; }
|
||||
public string MachineId { get => machineId; set => machineId = value; }
|
||||
public string InOutTypeId { get => inOutTypeId; set => inOutTypeId = value; }
|
||||
public string MachineIp { get => machineIp; set => machineIp = value; }
|
||||
public DateTime Date { get => date; set => date = value; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using MySql.Data.MySqlClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
class AttendanceDAO
|
||||
{
|
||||
public AttendanceDAO() {
|
||||
}
|
||||
public int Add(Attendance attendance, MySqlConnection connection)
|
||||
{
|
||||
string query = "REPLACE INTO `hrms`.`attendance_log` (`ac_no`, `checktime`, `processed`, `machine_id`, `in_out_type_id`, `machine_ip`, `date`) VALUES (@AcNo, @CHECKTIME, @isProcessed, @MachineId, @InOutTypeId, @MachineIP, @Getdate)";
|
||||
|
||||
MySqlCommand cmd = new MySqlCommand(query, connection);
|
||||
cmd.Parameters.AddWithValue("@AcNo", attendance.AcNo);
|
||||
cmd.Parameters.AddWithValue("@CHECKTIME", attendance.CheckTime);
|
||||
cmd.Parameters.AddWithValue("@isProcessed", attendance.IsProcessed);
|
||||
cmd.Parameters.AddWithValue("@machineId", attendance.MachineId);
|
||||
cmd.Parameters.AddWithValue("@InOutTypeId", attendance.InOutTypeId);
|
||||
cmd.Parameters.AddWithValue("@MachineIP", attendance.MachineIp);
|
||||
cmd.Parameters.AddWithValue("@Getdate", attendance.Date);
|
||||
|
||||
return cmd.ExecuteNonQuery();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
class AttendanceMachine
|
||||
{
|
||||
private string machineId;
|
||||
private string machineIp;
|
||||
private int portNumber;
|
||||
private string machineName;
|
||||
private int siteId;
|
||||
private string status;
|
||||
private DateTime lastSyncDate;
|
||||
|
||||
public AttendanceMachine()
|
||||
{
|
||||
}
|
||||
|
||||
public AttendanceMachine(string machineId, string machineIp, int portNumber, string machineName, int siteId, string status, DateTime lastSyncDate)
|
||||
{
|
||||
this.MachineId = machineId;
|
||||
this.MachineIp = machineIp;
|
||||
this.PortNumber = portNumber;
|
||||
this.MachineName = machineName;
|
||||
this.SiteId = siteId;
|
||||
this.Status = status;
|
||||
this.LastSyncDate = lastSyncDate;
|
||||
}
|
||||
|
||||
public string MachineId { get => machineId; set => machineId = value; }
|
||||
public string MachineIp
|
||||
{
|
||||
get => machineIp;
|
||||
set
|
||||
{
|
||||
// Normalize DB/config IP values that sometimes contain whitespace (e.g. "192.168. 0.50").
|
||||
string ip = value ?? "";
|
||||
if (ip.Length > 0)
|
||||
{
|
||||
ip = new string(ip.Trim().Where(c => !char.IsWhiteSpace(c)).ToArray());
|
||||
}
|
||||
machineIp = ip;
|
||||
}
|
||||
}
|
||||
public int PortNumber { get => portNumber; set => portNumber = value; }
|
||||
public string MachineName { get => machineName; set => machineName = value; }
|
||||
public int SiteId { get => siteId; set => siteId = value; }
|
||||
public string Status { get => status; set => status = value; }
|
||||
public DateTime LastSyncDate { get => lastSyncDate; set => lastSyncDate = value; }
|
||||
|
||||
public string GetDeviceInfo()
|
||||
{
|
||||
// HDCP SDK DeviceInfo uses password for device auth (same credentials as device web UI).
|
||||
string password = MachineScope.GetDevicePassword() ?? "";
|
||||
return "DeviceInfo( dev_id = \"1\" dev_type = \"" + MachineScope.GetDeviceType()
|
||||
+ "\" comm_type = \"ip\" ip_address = \"" + this.machineIp
|
||||
+ "\", password = \"" + EscapeDeviceInfoValue(password)
|
||||
+ "\", port_number = \"" + this.portNumber + "\")";
|
||||
}
|
||||
|
||||
/// <summary>Safe for InternalLogs — password redacted.</summary>
|
||||
public string GetDeviceInfoForLog()
|
||||
{
|
||||
return "DeviceInfo( dev_id = \"1\" dev_type = \"" + MachineScope.GetDeviceType()
|
||||
+ "\" comm_type = \"ip\" ip_address = \"" + this.machineIp
|
||||
+ "\", password = \"***\", port_number = \"" + this.portNumber + "\")";
|
||||
}
|
||||
|
||||
private static string EscapeDeviceInfoValue(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
return value.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
using MySql.Data.MySqlClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
class AttendanceMachineDAO
|
||||
{
|
||||
|
||||
public void update(AttendanceMachine attendanceMachine, MySqlConnection connection) {
|
||||
string query = "UPDATE `hrms`.`attendance_machine` SET `machine_ip` = @machine_ip, `port_number` = @port_number, `machine_name` = @machine_name, `site_id` = @site_id, `status` = @status, `last_sync_date` = @last_sync_date WHERE `machine_id` = @machine_id";
|
||||
MySqlCommand cmd = new MySqlCommand(query, connection);
|
||||
cmd.Parameters.AddWithValue("@machine_ip", attendanceMachine.MachineIp);
|
||||
cmd.Parameters.AddWithValue("@port_number", attendanceMachine.PortNumber);
|
||||
cmd.Parameters.AddWithValue("@machine_name", attendanceMachine.MachineName);
|
||||
cmd.Parameters.AddWithValue("@site_id", attendanceMachine.SiteId);
|
||||
cmd.Parameters.AddWithValue("@status", attendanceMachine.Status);
|
||||
cmd.Parameters.AddWithValue("@last_sync_date", attendanceMachine.LastSyncDate);
|
||||
cmd.Parameters.AddWithValue("@machine_id", attendanceMachine.MachineId);
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public List<AttendanceMachine> getAttendanceMachines(MySqlConnection connection, String siteId) {
|
||||
List<AttendanceMachine> attendanceMachines = null;
|
||||
using (var cmd = new MySqlCommand())
|
||||
{
|
||||
cmd.Connection = connection;
|
||||
cmd.CommandTimeout = 120;
|
||||
string typeClause = MachineScope.BuildMachineTypeWhereClause(cmd);
|
||||
cmd.CommandText = "SELECT * FROM hrms.attendance_machine WHERE site_id = @siteId AND " + typeClause;
|
||||
cmd.Parameters.AddWithValue("@siteId", siteId);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader != null)
|
||||
{
|
||||
attendanceMachines = getAttendanceMachineList(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
return attendanceMachines ?? new List<AttendanceMachine>();
|
||||
}
|
||||
|
||||
public List<AttendanceMachine> getAllAttendanceMachines(MySqlConnection connection)
|
||||
{
|
||||
List<AttendanceMachine> attendanceMachines = null;
|
||||
using (var cmd = new MySqlCommand())
|
||||
{
|
||||
cmd.Connection = connection;
|
||||
cmd.CommandTimeout = 120;
|
||||
string typeClause = MachineScope.BuildMachineTypeWhereClause(cmd);
|
||||
cmd.CommandText =
|
||||
"SELECT machine_id, machine_ip, port_number, machine_name, site_id, status, last_sync_date " +
|
||||
"FROM hrms.attendance_machine WHERE " + typeClause;
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader != null)
|
||||
{
|
||||
attendanceMachines = getAttendanceMachineList(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attendanceMachines ?? new List<AttendanceMachine>();
|
||||
}
|
||||
|
||||
/// <summary>Normalize IP for comparison (trim + remove spaces) so "192.168. 0.50" matches "192.168.0.50".</summary>
|
||||
private static string NormalizeIp(string ip)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ip)) return "";
|
||||
return new string(ip.Trim().Where(c => !char.IsWhiteSpace(c)).ToArray());
|
||||
}
|
||||
|
||||
public bool TryGetAttendanceMachineByIp(MySqlConnection connection, string machineIp, out AttendanceMachine machine, out string machineType)
|
||||
{
|
||||
machine = null;
|
||||
machineType = null;
|
||||
if (string.IsNullOrWhiteSpace(machineIp)) return false;
|
||||
|
||||
string normalizedIp = NormalizeIp(machineIp);
|
||||
|
||||
using (var cmd = new MySqlCommand())
|
||||
{
|
||||
cmd.Connection = connection;
|
||||
cmd.CommandTimeout = 60;
|
||||
string typeClause = MachineScope.BuildMachineTypeWhereClause(cmd, "machine_type", "mtype");
|
||||
cmd.CommandText =
|
||||
"SELECT machine_id, machine_ip, port_number, machine_name, site_id, status, last_sync_date, machine_type " +
|
||||
"FROM hrms.attendance_machine WHERE machine_ip = @machine_ip AND " + typeClause + " LIMIT 1";
|
||||
cmd.Parameters.AddWithValue("@machine_ip", machineIp);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
FillMachineFromReader(reader, out machine, out machineType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (var cmd = new MySqlCommand())
|
||||
{
|
||||
cmd.Connection = connection;
|
||||
cmd.CommandTimeout = 120;
|
||||
string typeClause = MachineScope.BuildMachineTypeWhereClause(cmd, "machine_type", "mtype2");
|
||||
cmd.CommandText =
|
||||
"SELECT machine_id, machine_ip, port_number, machine_name, site_id, status, last_sync_date, machine_type " +
|
||||
"FROM hrms.attendance_machine WHERE " + typeClause;
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
string dbIp = reader.IsDBNull(1) ? "" : reader.GetString("machine_ip");
|
||||
if (NormalizeIp(dbIp) == normalizedIp)
|
||||
{
|
||||
FillMachineFromReader(reader, out machine, out machineType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
private static void FillMachineFromReader(MySqlDataReader reader, out AttendanceMachine machine, out string machineType)
|
||||
{
|
||||
machine = new AttendanceMachine(
|
||||
reader.IsDBNull(0) ? "" : reader.GetString("machine_id"),
|
||||
reader.IsDBNull(1) ? "" : reader.GetString("machine_ip"),
|
||||
reader.IsDBNull(2) ? default(int) : reader.GetInt16("port_number"),
|
||||
reader.IsDBNull(3) ? "" : reader.GetString("machine_name"),
|
||||
reader.IsDBNull(4) ? default(int) : reader.GetInt16("site_id"),
|
||||
reader.IsDBNull(5) ? "" : reader.GetString("status"),
|
||||
reader.IsDBNull(6) ? default(DateTime) : reader.GetDateTime("last_sync_date")
|
||||
);
|
||||
machineType = reader.IsDBNull(7) ? null : reader.GetString("machine_type");
|
||||
}
|
||||
|
||||
private List<AttendanceMachine> getAttendanceMachineList(MySqlDataReader reader) {
|
||||
List<AttendanceMachine> attendanceMachines = new List<AttendanceMachine>();
|
||||
while (reader.Read()) {
|
||||
attendanceMachines.Add(toAttendanceMachine(reader));
|
||||
}
|
||||
return attendanceMachines;
|
||||
}
|
||||
|
||||
private AttendanceMachine toAttendanceMachine(MySqlDataReader reader) {
|
||||
return new AttendanceMachine(
|
||||
reader.IsDBNull(0) ? "" : reader.GetString("machine_id"),
|
||||
reader.IsDBNull(1) ? "" : reader.GetString("machine_ip"),
|
||||
reader.IsDBNull(2) ? default(int) : reader.GetInt16("port_number"),
|
||||
reader.IsDBNull(3) ? "" : reader.GetString("machine_name"),
|
||||
reader.IsDBNull(4) ? default(int) : reader.GetInt16("site_id"),
|
||||
reader.IsDBNull(5) ? "" : reader.GetString("status"),
|
||||
reader.IsDBNull(6) ? default(DateTime) : reader.GetDateTime("last_sync_date")
|
||||
|
||||
) ;
|
||||
}
|
||||
|
||||
public void UpdateTotalEmpInMachines(string machine_id, int total_users,MySqlConnection connection)
|
||||
{
|
||||
string query = "UPDATE `hrms`.`attendance_machine` SET `total_users` ="+ total_users + " WHERE `machine_id` = '"+ machine_id+ "'";
|
||||
MySqlCommand cmd = new MySqlCommand(query, connection);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
|
||||
public string CheckEmpInDB(string machine_id, int emp_cd, MySqlConnection connection)
|
||||
{
|
||||
string result = "FALSE";
|
||||
string query = "Select * from attendance_machine_user a where a.machine_id='"+ machine_id + "' and a.serial_number = "+ emp_cd + "";
|
||||
MySqlCommand cmd = new MySqlCommand(query, connection);
|
||||
DataTable dt = new DataTable();
|
||||
MySqlDataAdapter MySqlAdt = new MySqlDataAdapter();
|
||||
MySqlAdt.SelectCommand = cmd;
|
||||
MySqlAdt.Fill(dt);
|
||||
if(dt.Rows.Count > 0)
|
||||
{
|
||||
result = "TRUE";
|
||||
}
|
||||
else
|
||||
{
|
||||
result = "FALSE";
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,357 @@
|
|||
using MySql.Data.MySqlClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
internal sealed class AttendanceMachineFaceTemplateDAO
|
||||
{
|
||||
public bool ExistsBySerialNo(MySqlConnection connection, string serialNo)
|
||||
{
|
||||
const string checkQuery = "SELECT COUNT(*) FROM attendance_machine_face_templates WHERE serial_no = @SerialNumber";
|
||||
using (var cmd = new MySqlCommand(checkQuery, connection))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@SerialNumber", serialNo);
|
||||
object result = cmd.ExecuteScalar();
|
||||
if (result == null || result == DBNull.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (int.TryParse(result.ToString(), out int count))
|
||||
{
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int InsertTemplate(MySqlConnection connection, string serialNo, byte[] templateBytes, DateTime createdDate)
|
||||
{
|
||||
const string insertQuery =
|
||||
"INSERT INTO attendance_machine_face_templates (serial_no, template, created_date) " +
|
||||
"VALUES (@SerialNumber, @FaceTemplate, @created_date)";
|
||||
|
||||
using (var cmd = new MySqlCommand(insertQuery, connection))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@SerialNumber", serialNo);
|
||||
cmd.Parameters.Add("@FaceTemplate", MySqlDbType.Blob).Value = templateBytes ?? Array.Empty<byte>();
|
||||
cmd.Parameters.AddWithValue("@created_date", createdDate);
|
||||
return cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, byte[]> GetActiveTemplatesBySerialNos(MySqlConnection connection, IReadOnlyList<string> serialNos)
|
||||
{
|
||||
var result = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
|
||||
if (serialNos == null || serialNos.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Chunk to keep query size reasonable.
|
||||
const int chunkSize = 200;
|
||||
for (int offset = 0; offset < serialNos.Count; offset += chunkSize)
|
||||
{
|
||||
var chunk = serialNos.Skip(offset).Take(chunkSize).ToArray();
|
||||
var parameterNames = new List<string>(chunk.Length);
|
||||
for (int i = 0; i < chunk.Length; i++)
|
||||
{
|
||||
parameterNames.Add("@SerialNumber" + i);
|
||||
}
|
||||
|
||||
string inClause = string.Join(",", parameterNames);
|
||||
string query =
|
||||
$"SELECT serial_no, template FROM attendance_machine_face_templates " +
|
||||
$"WHERE serial_no IN ({inClause}) AND is_active = 1";
|
||||
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
for (int i = 0; i < chunk.Length; i++)
|
||||
{
|
||||
cmd.Parameters.AddWithValue(parameterNames[i], chunk[i]);
|
||||
}
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var serial = reader.IsDBNull(0) ? null : reader.GetString(0);
|
||||
if (string.IsNullOrWhiteSpace(serial))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] blob = null;
|
||||
if (!reader.IsDBNull(1))
|
||||
{
|
||||
blob = (byte[])reader["template"];
|
||||
}
|
||||
|
||||
result[serial] = blob ?? Array.Empty<byte>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>All active template serial numbers (CENTRAL hub scope).</summary>
|
||||
public List<string> GetActiveSerialNumbersAll(MySqlConnection connection)
|
||||
{
|
||||
var list = new List<string>();
|
||||
const string query = "SELECT serial_no FROM hrms.attendance_machine_face_templates WHERE is_active = 1";
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
cmd.CommandTimeout = 120;
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.IsDBNull(0)) continue;
|
||||
var s = reader.GetString(0);
|
||||
if (!string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
list.Add(s.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serial numbers that have an active template and appear on scoped machines (SITE deployment).
|
||||
/// </summary>
|
||||
public List<string> GetActiveSerialNumbersForScopedMachines(MySqlConnection connection, IReadOnlyList<string> machineIds)
|
||||
{
|
||||
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (machineIds == null || machineIds.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
const int chunkSize = 80;
|
||||
for (int offset = 0; offset < machineIds.Count; offset += chunkSize)
|
||||
{
|
||||
var chunk = machineIds.Skip(offset).Take(chunkSize).ToArray();
|
||||
var parameterNames = new List<string>(chunk.Length);
|
||||
for (int i = 0; i < chunk.Length; i++)
|
||||
{
|
||||
parameterNames.Add("@Mid" + i);
|
||||
}
|
||||
|
||||
string inClause = string.Join(",", parameterNames);
|
||||
string query =
|
||||
"SELECT DISTINCT t.serial_no " +
|
||||
"FROM hrms.attendance_machine_face_templates t " +
|
||||
"INNER JOIN hrms.attendance_machine_user u ON u.serial_number = t.serial_no AND u.is_deleted = 0 " +
|
||||
"WHERE t.is_active = 1 AND u.machine_id IN (" + inClause + ")";
|
||||
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
cmd.CommandTimeout = 120;
|
||||
for (int i = 0; i < chunk.Length; i++)
|
||||
{
|
||||
cmd.Parameters.AddWithValue(parameterNames[i], chunk[i]);
|
||||
}
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.IsDBNull(0)) continue;
|
||||
var s = reader.GetString(0);
|
||||
if (!string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
result.Add(s.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Active templates for employees registered on a specific Nedo machine.
|
||||
/// Registration-based target selection: attendance_machine_user (is_deleted=0) drives which employees are allowed.
|
||||
/// </summary>
|
||||
public Dictionary<string, byte[]> GetActiveTemplatesForRegisteredMachine(MySqlConnection connection, string machineId)
|
||||
{
|
||||
var result = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
|
||||
if (string.IsNullOrWhiteSpace(machineId))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const string query =
|
||||
"SELECT t.serial_no, t.template " +
|
||||
"FROM hrms.attendance_machine_face_templates t " +
|
||||
"INNER JOIN hrms.attendance_machine_user u ON u.serial_number = t.serial_no " +
|
||||
"WHERE t.is_active = 1 AND u.is_deleted = 0 AND u.machine_id = @MachineId";
|
||||
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
cmd.CommandTimeout = 120;
|
||||
cmd.Parameters.AddWithValue("@MachineId", machineId);
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.IsDBNull(0)) continue;
|
||||
var serial = reader.GetString(0);
|
||||
if (string.IsNullOrWhiteSpace(serial)) continue;
|
||||
|
||||
byte[] blob = Array.Empty<byte>();
|
||||
if (!reader.IsDBNull(1))
|
||||
{
|
||||
blob = (byte[])reader["template"];
|
||||
}
|
||||
|
||||
result[serial.Trim()] = blob;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For each serial_no, return machine_ids where the employee is registered (attendance_machine_user),
|
||||
/// restricted to the allowedMachineIds list (scope) and to Nedo machines (exclude ZKTECO by machine_type).
|
||||
/// </summary>
|
||||
public Dictionary<string, List<string>> GetRegisteredTargetMachineIdsForSerialNos(
|
||||
MySqlConnection connection,
|
||||
IReadOnlyList<string> serialNos,
|
||||
IReadOnlyList<string> allowedMachineIds)
|
||||
{
|
||||
var result = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
if (serialNos == null || serialNos.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var allowedSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (allowedMachineIds != null)
|
||||
{
|
||||
foreach (var id in allowedMachineIds)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
allowedSet.Add(id.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allowedSet.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const int serialChunkSize = 200;
|
||||
var allowedList = allowedSet.ToList();
|
||||
|
||||
// Keep allowed-machine IN clause reasonably small.
|
||||
const int allowedChunkSize = 100;
|
||||
for (int offset = 0; offset < serialNos.Count; offset += serialChunkSize)
|
||||
{
|
||||
var serialChunk = serialNos.Skip(offset).Take(serialChunkSize).ToArray();
|
||||
var serialParamNames = new List<string>(serialChunk.Length);
|
||||
for (int i = 0; i < serialChunk.Length; i++)
|
||||
{
|
||||
serialParamNames.Add("@S" + i);
|
||||
}
|
||||
|
||||
string serialInClause = string.Join(",", serialParamNames);
|
||||
|
||||
for (int ao = 0; ao < allowedList.Count; ao += allowedChunkSize)
|
||||
{
|
||||
var allowedChunk = allowedList.Skip(ao).Take(allowedChunkSize).ToArray();
|
||||
var allowedParamNames = new List<string>(allowedChunk.Length);
|
||||
for (int i = 0; i < allowedChunk.Length; i++)
|
||||
{
|
||||
allowedParamNames.Add("@A" + i);
|
||||
}
|
||||
string allowedInClause = string.Join(",", allowedParamNames);
|
||||
|
||||
const string query =
|
||||
"SELECT u.serial_number, u.machine_id " +
|
||||
"FROM hrms.attendance_machine_user u " +
|
||||
"INNER JOIN hrms.attendance_machine m ON m.machine_id = u.machine_id " +
|
||||
"WHERE u.is_deleted = 0 " +
|
||||
"AND (m.machine_type IS NULL OR m.machine_type <> 'ZKTECO') " +
|
||||
"AND u.serial_number IN ({SERIAL_IN}) " +
|
||||
"AND u.machine_id IN ({ALLOWED_IN})";
|
||||
|
||||
string q = query
|
||||
.Replace("{SERIAL_IN}", serialInClause)
|
||||
.Replace("{ALLOWED_IN}", allowedInClause);
|
||||
|
||||
using (var cmd = new MySqlCommand())
|
||||
{
|
||||
cmd.Connection = connection;
|
||||
cmd.CommandTimeout = 120;
|
||||
string typeClause = MachineScope.BuildMachineTypeWhereClause(cmd, "m.machine_type", "mtreg" + offset + ao);
|
||||
cmd.CommandText = q.Replace("AND (m.machine_type IS NULL OR m.machine_type <> 'ZKTECO')", "AND " + typeClause);
|
||||
for (int i = 0; i < serialChunk.Length; i++)
|
||||
{
|
||||
cmd.Parameters.AddWithValue(serialParamNames[i], serialChunk[i]);
|
||||
}
|
||||
for (int i = 0; i < allowedChunk.Length; i++)
|
||||
{
|
||||
cmd.Parameters.AddWithValue(allowedParamNames[i], allowedChunk[i]);
|
||||
}
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
// temp set per serial to avoid duplicates from allowed-chunk partitioning
|
||||
var serialToSet = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.IsDBNull(0) || reader.IsDBNull(1)) continue;
|
||||
var serial = reader.GetString(0);
|
||||
var mid = reader.GetString(1);
|
||||
if (string.IsNullOrWhiteSpace(serial) || string.IsNullOrWhiteSpace(mid)) continue;
|
||||
|
||||
if (!serialToSet.TryGetValue(serial, out var set))
|
||||
{
|
||||
set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
serialToSet[serial] = set;
|
||||
}
|
||||
set.Add(mid.Trim());
|
||||
}
|
||||
|
||||
foreach (var kvp in serialToSet)
|
||||
{
|
||||
if (!result.TryGetValue(kvp.Key, out var list))
|
||||
{
|
||||
list = new List<string>();
|
||||
result[kvp.Key] = list;
|
||||
}
|
||||
|
||||
foreach (var mid in kvp.Value)
|
||||
{
|
||||
if (!list.Contains(mid))
|
||||
{
|
||||
list.Add(mid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
internal class AttendanceMachineUser
|
||||
{
|
||||
private string machineId;
|
||||
private string serialNumber;
|
||||
private string employeeName;
|
||||
|
||||
public AttendanceMachineUser(string machineId, string serialNumber, string employeeName)
|
||||
{
|
||||
this.MachineId = machineId;
|
||||
this.SerialNumber = serialNumber;
|
||||
this.EmployeeName = employeeName;
|
||||
}
|
||||
|
||||
public string MachineId { get => machineId; set => machineId = value; }
|
||||
public string SerialNumber { get => serialNumber; set => serialNumber = value; }
|
||||
public string EmployeeName { get => employeeName; set => employeeName = value; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
using MySql.Data.MySqlClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
internal class AttendanceMachineUserDAO
|
||||
{
|
||||
public AttendanceMachineUserDAO() { }
|
||||
public void Add(AttendanceMachineUser attendanceMachineUser, MySqlConnection connection)
|
||||
{
|
||||
// Ensure synced users are active and clear any pending deletion request.
|
||||
const string query =
|
||||
"REPLACE INTO `hrms`.`attendance_machine_user` " +
|
||||
"(`machine_id`, `serial_number`, `employee_name`, `is_deletion_requested`, `is_deleted`, `updated_by`, `updated_at`) " +
|
||||
"VALUES (@MachineId, @SerialNumber, @EmployeeName, 0, 0, @UpdatedBy, @UpdatedAt)";
|
||||
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@MachineId", attendanceMachineUser.MachineId);
|
||||
cmd.Parameters.AddWithValue("@SerialNumber", attendanceMachineUser.SerialNumber);
|
||||
cmd.Parameters.AddWithValue("@EmployeeName", attendanceMachineUser.EmployeeName);
|
||||
cmd.Parameters.AddWithValue("@UpdatedBy", "SERVICE");
|
||||
cmd.Parameters.AddWithValue("@UpdatedAt", DateTime.Now);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public List<AttendanceMachineUser> getActiveEmployees(MySqlConnection connection)
|
||||
{
|
||||
const string query =
|
||||
"SELECT machine_id, serial_number, employee_name " +
|
||||
"FROM hrms.attendance_machine_user " +
|
||||
"WHERE is_deleted = 0";
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
cmd.CommandTimeout = 120;
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
return getDBUsers(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetDeletionRequestedSerials(MySqlConnection connection, string machineId)
|
||||
{
|
||||
var serials = new List<string>();
|
||||
const string query =
|
||||
"SELECT serial_number " +
|
||||
"FROM hrms.attendance_machine_user " +
|
||||
"WHERE machine_id = @MachineId AND is_deletion_requested = 1 AND is_deleted = 0";
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
cmd.CommandTimeout = 60;
|
||||
cmd.Parameters.AddWithValue("@MachineId", machineId);
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (!reader.IsDBNull(0))
|
||||
{
|
||||
var s = reader.GetString(0);
|
||||
if (!string.IsNullOrWhiteSpace(s))
|
||||
serials.Add(s.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return serials;
|
||||
}
|
||||
|
||||
public int MarkUserDeleted(MySqlConnection connection, string machineId, string serialNumber)
|
||||
{
|
||||
const string query =
|
||||
"UPDATE hrms.attendance_machine_user " +
|
||||
"SET is_deleted = 1, is_deletion_requested = 0, updated_by = @UpdatedBy, updated_at = @UpdatedAt " +
|
||||
"WHERE machine_id = @MachineId AND serial_number = @SerialNumber AND is_deleted = 0";
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
cmd.CommandTimeout = 60;
|
||||
cmd.Parameters.AddWithValue("@UpdatedBy", "SERVICE");
|
||||
cmd.Parameters.AddWithValue("@UpdatedAt", DateTime.Now);
|
||||
cmd.Parameters.AddWithValue("@MachineId", machineId);
|
||||
cmd.Parameters.AddWithValue("@SerialNumber", serialNumber);
|
||||
return cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public List<AttendanceMachineUser> getAllEmployees(MySqlConnection connection)
|
||||
{
|
||||
string query = "SELECT machine_id, serial_number, employee_name FROM hrms.attendance_machine_user";
|
||||
using (var cmd = new MySqlCommand(query, connection))
|
||||
{
|
||||
cmd.CommandTimeout = 120;
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
return getDBUsers(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<AttendanceMachineUser> getDBUsers(MySqlDataReader reader)
|
||||
{
|
||||
List<AttendanceMachineUser> DbUsers = new List<AttendanceMachineUser>();
|
||||
while (reader.Read())
|
||||
{
|
||||
DbUsers.Add(GetDbUsers(reader));
|
||||
}
|
||||
return DbUsers;
|
||||
}
|
||||
|
||||
|
||||
private AttendanceMachineUser GetDbUsers(MySqlDataReader reader)
|
||||
{
|
||||
return new AttendanceMachineUser(
|
||||
reader.IsDBNull(0) ? "" : reader.GetString("machine_id"),
|
||||
reader.IsDBNull(1) ? "" : reader.GetString("serial_number"),
|
||||
reader.IsDBNull(1) ? "" : reader.GetString("serial_number")
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<DeviceSettings Mode="Multi" SiteId="2">
|
||||
<!--<Device IpAddress="192.168.70.14" Enabled="true" />-->
|
||||
<Device IpAddress="192.168.86.2" Enabled="true" />
|
||||
|
||||
<TemplateTransfer Enabled="false">
|
||||
<Job Id="1"
|
||||
Enabled="true"
|
||||
SourceIp="192.168.86.7"
|
||||
EmpIds="7044">
|
||||
<Target Ip="192.168.86.4" />
|
||||
</Job>
|
||||
</TemplateTransfer>
|
||||
|
||||
</DeviceSettings>
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
internal sealed class DeviceSettingsConfig
|
||||
{
|
||||
public string Mode { get; private set; }
|
||||
public int? SiteId { get; private set; }
|
||||
public List<DeviceRule> Devices { get; } = new List<DeviceRule>();
|
||||
public TemplateTransferConfig TemplateTransfer { get; private set; }
|
||||
|
||||
public static bool TryLoad(string path, out DeviceSettingsConfig config, out string error)
|
||||
{
|
||||
config = null;
|
||||
error = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
error = "Device settings path is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
config = new DeviceSettingsConfig();
|
||||
return true;
|
||||
}
|
||||
|
||||
var doc = XDocument.Load(path);
|
||||
var root = doc.Root;
|
||||
if (root == null || !string.Equals(root.Name.LocalName, "DeviceSettings", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"Invalid device settings root element. Expected <DeviceSettings>, found <{root?.Name.LocalName ?? "null"}>.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var parsed = new DeviceSettingsConfig
|
||||
{
|
||||
Mode = (string)root.Attribute("Mode")
|
||||
};
|
||||
|
||||
var rootSiteIdStr = (string)root.Attribute("SiteId") ?? (string)root.Attribute("SITE_ID") ?? (string)root.Attribute("SiteID");
|
||||
if (!string.IsNullOrWhiteSpace(rootSiteIdStr) && int.TryParse(rootSiteIdStr.Trim(), out var rootSiteId))
|
||||
{
|
||||
parsed.SiteId = rootSiteId;
|
||||
}
|
||||
|
||||
foreach (var deviceEl in root.Elements().Where(e => string.Equals(e.Name.LocalName, "Device", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var ip = (string)deviceEl.Attribute("IpAddress") ?? (string)deviceEl.Attribute("Ip") ?? "";
|
||||
var enabledStr = (string)deviceEl.Attribute("Enabled");
|
||||
if (string.IsNullOrWhiteSpace(ip) || string.IsNullOrWhiteSpace(enabledStr))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseBool(enabledStr, out var enabled))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int? siteId = null;
|
||||
var siteIdStr = (string)deviceEl.Attribute("SiteId") ?? (string)deviceEl.Attribute("SITE_ID") ?? (string)deviceEl.Attribute("SiteID");
|
||||
if (!string.IsNullOrWhiteSpace(siteIdStr) && int.TryParse(siteIdStr.Trim(), out var parsedSiteId))
|
||||
{
|
||||
siteId = parsedSiteId;
|
||||
}
|
||||
|
||||
parsed.Devices.Add(new DeviceRule(ip.Trim(), enabled, siteId));
|
||||
}
|
||||
|
||||
// If SiteId is not specified at root, but devices specify a single consistent SiteId, use it.
|
||||
if (!parsed.SiteId.HasValue)
|
||||
{
|
||||
var distinct = parsed.Devices.Where(d => d.SiteId.HasValue).Select(d => d.SiteId.Value).Distinct().ToList();
|
||||
if (distinct.Count == 1)
|
||||
{
|
||||
parsed.SiteId = distinct[0];
|
||||
}
|
||||
}
|
||||
|
||||
var templateTransferEl = root.Elements().FirstOrDefault(e => string.Equals(e.Name.LocalName, "TemplateTransfer", StringComparison.OrdinalIgnoreCase));
|
||||
if (templateTransferEl != null)
|
||||
{
|
||||
var ttEnabled = TryParseBool((string)templateTransferEl.Attribute("Enabled"), out var tt) && tt;
|
||||
var ttConfig = new TemplateTransferConfig(ttEnabled);
|
||||
|
||||
foreach (var jobEl in templateTransferEl.Elements().Where(e => string.Equals(e.Name.LocalName, "Job", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var id = (string)jobEl.Attribute("Id") ?? "";
|
||||
var enabled = TryParseBool((string)jobEl.Attribute("Enabled"), out var jEnabled) && jEnabled;
|
||||
var sourceIp = (string)jobEl.Attribute("SourceIp") ?? (string)jobEl.Attribute("SourceIP") ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(sourceIp))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var targets = new List<string>();
|
||||
foreach (var targetEl in jobEl.Elements().Where(e => string.Equals(e.Name.LocalName, "Target", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var tip = (string)targetEl.Attribute("Ip") ?? (string)targetEl.Attribute("IpAddress") ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(tip))
|
||||
{
|
||||
targets.Add(tip.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Optional comma-separated list of employee IDs for DB_TO_DEVICE filtering
|
||||
var empIdsAttr = (string)jobEl.Attribute("EmpIds") ?? (string)jobEl.Attribute("EmpIDs");
|
||||
List<string> empIds = null;
|
||||
if (!string.IsNullOrWhiteSpace(empIdsAttr))
|
||||
{
|
||||
empIds = empIdsAttr
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(v => v.Trim())
|
||||
.Where(v => v.Length > 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
ttConfig.Jobs.Add(new TemplateTransferJob(id.Trim(), enabled, sourceIp.Trim(), targets, empIds));
|
||||
}
|
||||
|
||||
parsed.TemplateTransfer = ttConfig;
|
||||
}
|
||||
|
||||
config = parsed;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"Failed to load device settings: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasAnyAllowlistedDeviceIp()
|
||||
=> Devices.Any(d => d.Enabled);
|
||||
|
||||
public HashSet<string> GetAllowlistedIps()
|
||||
=> new HashSet<string>(Devices.Where(d => d.Enabled).Select(d => d.IpAddress), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public HashSet<string> GetBlocklistedIps()
|
||||
=> new HashSet<string>(Devices.Where(d => !d.Enabled).Select(d => d.IpAddress), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool TryParseBool(string value, out bool result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return bool.TryParse(value.Trim(), out result);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DeviceRule
|
||||
{
|
||||
public DeviceRule(string ipAddress, bool enabled, int? siteId)
|
||||
{
|
||||
IpAddress = ipAddress ?? "";
|
||||
Enabled = enabled;
|
||||
SiteId = siteId;
|
||||
}
|
||||
|
||||
public string IpAddress { get; }
|
||||
public bool Enabled { get; }
|
||||
public int? SiteId { get; }
|
||||
}
|
||||
|
||||
internal sealed class TemplateTransferConfig
|
||||
{
|
||||
public TemplateTransferConfig(bool enabled)
|
||||
{
|
||||
Enabled = enabled;
|
||||
}
|
||||
|
||||
public bool Enabled { get; }
|
||||
public List<TemplateTransferJob> Jobs { get; } = new List<TemplateTransferJob>();
|
||||
}
|
||||
|
||||
internal sealed class TemplateTransferJob
|
||||
{
|
||||
public TemplateTransferJob(string id, bool enabled, string sourceIp, List<string> targetIps, List<string> empIds)
|
||||
{
|
||||
Id = id ?? "";
|
||||
Enabled = enabled;
|
||||
SourceIp = sourceIp ?? "";
|
||||
TargetIps = targetIps ?? new List<string>();
|
||||
EmpIds = empIds ?? new List<string>();
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public bool Enabled { get; }
|
||||
public string SourceIp { get; }
|
||||
public List<string> TargetIps { get; }
|
||||
public List<string> EmpIds { get; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-4789-A012-3456789ABCDE}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>HanvonF710XAttendanceService</RootNamespace>
|
||||
<AssemblyName>HanvonF710XAttendanceService</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="BouncyCastle.Crypto, Version=1.8.5.0, Culture=neutral, PublicKeyToken=0e99375e54769942">
|
||||
<HintPath>packages\BouncyCastle.1.8.5\lib\BouncyCastle.Crypto.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Google.Protobuf, Version=3.11.4.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Google.Protobuf.3.11.4\lib\net45\Google.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="K4os.Compression.LZ4, Version=1.1.11.0, Culture=neutral, PublicKeyToken=2186fa9121ef231d, processorArchitecture=MSIL">
|
||||
<HintPath>packages\K4os.Compression.LZ4.1.1.11\lib\net46\K4os.Compression.LZ4.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="K4os.Compression.LZ4.Streams, Version=1.1.11.0, Culture=neutral, PublicKeyToken=2186fa9121ef231d, processorArchitecture=MSIL">
|
||||
<HintPath>packages\K4os.Compression.LZ4.Streams.1.1.11\lib\net46\K4os.Compression.LZ4.Streams.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="K4os.Hash.xxHash, Version=1.0.6.0, Culture=neutral, PublicKeyToken=32cd54395057cec3, processorArchitecture=MSIL">
|
||||
<HintPath>packages\K4os.Hash.xxHash.1.0.6\lib\net46\K4os.Hash.xxHash.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MySql.Data, Version=8.0.23.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d, processorArchitecture=MSIL">
|
||||
<HintPath>packages\MySql.Data.8.0.23\lib\net452\MySql.Data.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Renci.SshNet, Version=2020.0.0.0, Culture=neutral, PublicKeyToken=1cee9f8bde3db106, processorArchitecture=MSIL">
|
||||
<HintPath>packages\SSH.NET.2020.0.0\lib\net40\Renci.SshNet.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.ComponentModel" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Configuration.Install" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Memory.4.5.3\lib\netstandard2.0\System.Memory.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Numerics" />
|
||||
<Reference Include="System.Numerics.Vectors, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Transactions" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Ubiety.Dns.Core, Version=2.2.1.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d, processorArchitecture=MSIL">
|
||||
<HintPath>packages\MySql.Data.8.0.23\lib\net452\Ubiety.Dns.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Zstandard.Net, Version=1.1.7.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d, processorArchitecture=MSIL">
|
||||
<HintPath>packages\MySql.Data.8.0.23\lib\net452\Zstandard.Net.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Attendance.cs" />
|
||||
<Compile Include="AttendanceDAO.cs" />
|
||||
<Compile Include="AttendanceMachine.cs" />
|
||||
<Compile Include="AttendanceMachineDAO.cs" />
|
||||
<Compile Include="AttendanceMachineFaceTemplateDAO.cs" />
|
||||
<Compile Include="AttendanceMachineUser.cs" />
|
||||
<Compile Include="AttendanceMachineUserDAO.cs" />
|
||||
<Compile Include="DeviceSettingsConfig.cs" />
|
||||
<Compile Include="LogService.cs" />
|
||||
<Compile Include="ProjectInstaller.cs">
|
||||
<SubType>Component</SubType>
|
||||
</Compile>
|
||||
<Compile Include="ProjectInstaller.Designer.cs">
|
||||
<DependentUpon>ProjectInstaller.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="HanvonHttpApiClient.cs" />
|
||||
<Compile Include="HanvonF710XWindowsService.cs">
|
||||
<SubType>Component</SubType>
|
||||
</Compile>
|
||||
<Compile Include="HanvonF710XWindowsService.Designer.cs">
|
||||
<DependentUpon>HanvonF710XWindowsService.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="MachineScope.cs" />
|
||||
<Compile Include="TemplateParser.cs" />
|
||||
<Compile Include="TemplateTransferRunner.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="DeviceSettings.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="ProjectInstaller.resx">
|
||||
<DependentUpon>ProjectInstaller.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.4.11626.88 stable
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HanvonF710XAttendanceService", "HanvonF710XAttendanceService.csproj", "{A1B2C3D4-E5F6-4789-A012-3456789ABCDE}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {198B6901-F16A-45C5-9B8E-DAD67C218E8B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
partial class HanvonF710XWindowsService
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
this.ServiceName = "HanvonF710XAttendanceService";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,457 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.ServiceProcess;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Configuration;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
public partial class HanvonF710XWindowsService : ServiceBase
|
||||
{
|
||||
// Separate timers for attendance/users and template sync.
|
||||
System.Timers.Timer timer = new System.Timers.Timer(); // attendance + users
|
||||
System.Timers.Timer timer1 = new System.Timers.Timer(); // templates
|
||||
|
||||
// Independent locks/flags so jobs don't block each other.
|
||||
private static readonly object _attendanceLock = new object();
|
||||
private static readonly object _templateLock = new object();
|
||||
private static bool _attendanceJobRunning = false;
|
||||
private static bool _templateJobRunning = false;
|
||||
public HanvonF710XWindowsService()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>Starts timers and jobs when running outside the Windows Service Control Manager.</summary>
|
||||
public void StartForConsole(string[] args)
|
||||
{
|
||||
OnStart(args ?? Array.Empty<string>());
|
||||
}
|
||||
|
||||
/// <summary>Stops timers and flushes logs when running in console/debug mode.</summary>
|
||||
public void StopForConsole()
|
||||
{
|
||||
OnStop();
|
||||
}
|
||||
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
// So the service finds HwDevComm.dll (and sibling DLLs) in the EXE folder
|
||||
string exeDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
try
|
||||
{
|
||||
Environment.CurrentDirectory = exeDir;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { WriteInternalLog("OnStart set CurrentDirectory failed: " + ex.Message); } catch { }
|
||||
}
|
||||
|
||||
// Start buffered, cross-process-safe logging.
|
||||
try { LogService.Start(); } catch { }
|
||||
|
||||
WriteToFile("--Service is started at " + DateTime.Now);
|
||||
WriteInternalLog("--Service is started at " + DateTime.Now);
|
||||
if (Program.IsConsoleMode)
|
||||
{
|
||||
Console.WriteLine("--Service is started at " + DateTime.Now);
|
||||
}
|
||||
|
||||
int attendanceMinutes = GetIntConfig("ATTENDANCE_JOB_INTERVAL_MINUTES", 20);
|
||||
int templateMinutes = GetIntConfig("TEMPLATE_JOB_INTERVAL_MINUTES", 240);
|
||||
|
||||
double attendanceIntervalMs = Math.Max(1, attendanceMinutes) * 60 * 1000;
|
||||
double templateIntervalMs = Math.Max(1, templateMinutes) * 60 * 1000;
|
||||
|
||||
timer.Elapsed += new ElapsedEventHandler(OnAttendanceElapsed);
|
||||
timer.Interval = attendanceIntervalMs;
|
||||
timer.AutoReset = true;
|
||||
timer.Enabled = true;
|
||||
|
||||
timer1.Elapsed += new ElapsedEventHandler(OnTemplateElapsed);
|
||||
timer1.Interval = templateIntervalMs;
|
||||
timer1.AutoReset = true;
|
||||
timer1.Enabled = true;
|
||||
|
||||
WriteInternalLog($"Attendance job interval: {attendanceMinutes} minutes");
|
||||
WriteInternalLog($"Template job interval: {templateMinutes} minutes");
|
||||
WriteToFile($"Attendance job interval: {attendanceMinutes} minutes");
|
||||
WriteToFile($"Template job interval: {templateMinutes} minutes");
|
||||
|
||||
// Run both jobs once immediately on startup (non-blocking).
|
||||
// Uses the same guarded execution path as the timer callbacks.
|
||||
try
|
||||
{
|
||||
WriteInternalLog("--Startup trigger: running attendance job immediately at " + DateTime.Now);
|
||||
Task.Run(() => RunAttendanceJob());
|
||||
}
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
int delaySec = 30;
|
||||
WriteInternalLog("--Startup trigger: running template job after " + delaySec + "s at " + DateTime.Now);
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(delaySec * 1000).ConfigureAwait(false);
|
||||
RunTemplateJob();
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
protected override void OnStop()
|
||||
{
|
||||
WriteToFile("--Service is stopped at " + DateTime.Now);
|
||||
try
|
||||
{
|
||||
timer.Enabled = false;
|
||||
timer1.Enabled = false;
|
||||
timer.Dispose();
|
||||
timer1.Dispose();
|
||||
}
|
||||
catch { }
|
||||
try { LogService.StopAndFlush(TimeSpan.FromSeconds(5)); } catch { }
|
||||
}
|
||||
|
||||
private static int GetIntConfig(string key, int defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = ConfigurationManager.AppSettings[key];
|
||||
if (string.IsNullOrWhiteSpace(raw)) return defaultValue;
|
||||
return int.TryParse(raw, out var v) && v > 0 ? v : defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Attendance + machine-user job
|
||||
private void OnAttendanceElapsed(object source, ElapsedEventArgs e)
|
||||
{
|
||||
RunAttendanceJob();
|
||||
}
|
||||
|
||||
private void RunAttendanceJob()
|
||||
{
|
||||
if (!Monitor.TryEnter(_attendanceLock))
|
||||
{
|
||||
WriteInternalLog("--Attendance tick skipped (previous attendance job still running) at " + DateTime.Now);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (_attendanceJobRunning)
|
||||
{
|
||||
WriteInternalLog("--Attendance tick skipped (previous attendance job still running) at " + DateTime.Now);
|
||||
return;
|
||||
}
|
||||
_attendanceJobRunning = true;
|
||||
|
||||
WriteSyncCycleHeader();
|
||||
Program.BeginSyncCycle();
|
||||
WriteInternalLog("--Attendance job started at " + DateTime.Now);
|
||||
WriteAttendanceServiceLog("--Attendance job started at " + DateTime.Now);
|
||||
|
||||
try
|
||||
{
|
||||
syncMachineUsers();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteInternalLog("syncMachineUsers error: " + ex.Message);
|
||||
WriteMachineUserServiceLog("syncMachineUsers error: " + ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
syncAttendance();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteInternalLog("syncAttendance error: " + ex.Message);
|
||||
WriteAttendanceServiceLog("syncAttendance error: " + ex.Message);
|
||||
}
|
||||
|
||||
WriteInternalLog("--Attendance job finished at " + DateTime.Now);
|
||||
WriteAttendanceServiceLog("--Attendance job finished at " + DateTime.Now);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Program.EndSyncCycle(); } catch { }
|
||||
_attendanceJobRunning = false;
|
||||
Monitor.Exit(_attendanceLock);
|
||||
}
|
||||
}
|
||||
|
||||
// Template job
|
||||
private void OnTemplateElapsed(object source, ElapsedEventArgs e)
|
||||
{
|
||||
RunTemplateJob();
|
||||
}
|
||||
|
||||
private void RunTemplateJob()
|
||||
{
|
||||
if (!Monitor.TryEnter(_templateLock))
|
||||
{
|
||||
WriteInternalLog("--Template tick skipped (previous template job still running) at " + DateTime.Now);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (_templateJobRunning)
|
||||
{
|
||||
WriteInternalLog("--Template tick skipped (previous template job still running) at " + DateTime.Now);
|
||||
return;
|
||||
}
|
||||
_templateJobRunning = true;
|
||||
|
||||
WriteJobHeaderToSimpleLogs("TEMPLATE");
|
||||
WriteInternalLog("--Template job started at " + DateTime.Now);
|
||||
WriteSaveFaceTemplateLog("--Template job started at " + DateTime.Now);
|
||||
WriteFaceTemplateTransferLog("--Template job started at " + DateTime.Now);
|
||||
try
|
||||
{
|
||||
syncTemplatesIfEnabled();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteInternalLog("syncTemplatesIfEnabled error: " + ex.ToString());
|
||||
WriteFaceTemplateTransferLog("syncTemplatesIfEnabled error: " + ex.ToString());
|
||||
WriteSaveFaceTemplateLog("syncTemplatesIfEnabled error: " + ex.ToString());
|
||||
}
|
||||
WriteInternalLog("--Template job finished at " + DateTime.Now);
|
||||
WriteSaveFaceTemplateLog("--Template job finished at " + DateTime.Now);
|
||||
WriteFaceTemplateTransferLog("--Template job finished at " + DateTime.Now);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_templateJobRunning = false;
|
||||
Monitor.Exit(_templateLock);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void syncAttendance()
|
||||
{
|
||||
|
||||
|
||||
WriteToFile("--Sync Attendance is recall at " + DateTime.Now);
|
||||
WriteInternalLog("--Sync Attendance is recall at " + DateTime.Now);
|
||||
WriteAttendanceServiceLog("--Sync Attendance is recall at " + DateTime.Now);
|
||||
List<string> responses = Program.syncAttendance();
|
||||
foreach (var response in responses)
|
||||
{
|
||||
WriteToFile(response);
|
||||
WriteInternalLog(response);
|
||||
|
||||
// Attendance simple log: only machine-level results and connectivity.
|
||||
if (IsAttendanceSimpleLine(response))
|
||||
{
|
||||
WriteAttendanceServiceLog(response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public void syncMachineUsers()
|
||||
{
|
||||
|
||||
WriteToFile("--Sync Machine users is recall at " + DateTime.Now);
|
||||
WriteInternalLog("--Sync Machine users is recall at " + DateTime.Now);
|
||||
WriteMachineUserServiceLog("--Sync Machine users is recall at " + DateTime.Now);
|
||||
List<string> responses = Program.syncMachineUsers();
|
||||
foreach (var response in responses)
|
||||
{
|
||||
WriteToFile(response);
|
||||
WriteInternalLog(response);
|
||||
|
||||
// Machine user simple log: only machine totals and connectivity.
|
||||
if (IsMachineUserSimpleLine(response))
|
||||
{
|
||||
WriteMachineUserServiceLog(response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void syncTemplatesIfEnabled()
|
||||
{
|
||||
WriteToFile("--Template sync is recall at " + DateTime.Now);
|
||||
WriteInternalLog("--Template sync is recall at " + DateTime.Now);
|
||||
// We'll route template logs into either SaveFaceTemplate or FaceTransfer logs based on TRANSFER_MODE.
|
||||
List<string> responses = Program.syncTemplates();
|
||||
foreach (var response in responses)
|
||||
{
|
||||
WriteToFile(response);
|
||||
WriteInternalLog(response);
|
||||
|
||||
// SaveFaceTemplate logs: DEVICE_TO_DB and any "Serial number--" lines.
|
||||
if (IsSaveFaceTemplateLine(response))
|
||||
{
|
||||
WriteSaveFaceTemplateLog(response);
|
||||
}
|
||||
|
||||
// FaceTransfer logs: DB_TO_DEVICE / DEVICE_TO_DEVICE and connectivity issues.
|
||||
if (IsFaceTransferLine(response))
|
||||
{
|
||||
WriteFaceTemplateTransferLog(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void WriteToFile(string Message)
|
||||
{
|
||||
string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\Logs\\ServiceLog_" +
|
||||
DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt";
|
||||
LogService.EnqueueLine(filepath, Message);
|
||||
}
|
||||
|
||||
private void WriteSyncCycleHeader()
|
||||
{
|
||||
string time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
string line1 = "========================================================";
|
||||
string line2 = "SYNC CYCLE START";
|
||||
string line3 = "Time: " + time;
|
||||
string line4 = "=========================";
|
||||
|
||||
WriteToFile(line1);
|
||||
WriteToFile(line2);
|
||||
WriteToFile(line3);
|
||||
WriteToFile(line4);
|
||||
|
||||
WriteInternalLog(line1);
|
||||
WriteInternalLog(line2);
|
||||
WriteInternalLog(line3);
|
||||
WriteInternalLog(line4);
|
||||
|
||||
// Simple per-feature logs (keep InternalLogs unchanged).
|
||||
WriteAttendanceServiceLog(line1);
|
||||
WriteAttendanceServiceLog(line2);
|
||||
WriteAttendanceServiceLog(line3);
|
||||
WriteAttendanceServiceLog(line4);
|
||||
|
||||
WriteMachineUserServiceLog(line1);
|
||||
WriteMachineUserServiceLog(line2);
|
||||
WriteMachineUserServiceLog(line3);
|
||||
WriteMachineUserServiceLog(line4);
|
||||
}
|
||||
|
||||
// Ensures per-feature simple logs have a header even when a job doesn't call WriteSyncCycleHeader.
|
||||
// Used by template job.
|
||||
private void WriteJobHeaderToSimpleLogs(string jobName)
|
||||
{
|
||||
string time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
string line1 = "========================================================";
|
||||
string line2 = "SYNC CYCLE START";
|
||||
string line3 = "Time: " + time;
|
||||
string line4 = "=========================";
|
||||
string line5 = "JOB: " + jobName;
|
||||
|
||||
WriteSaveFaceTemplateLog(line1);
|
||||
WriteSaveFaceTemplateLog(line2);
|
||||
WriteSaveFaceTemplateLog(line3);
|
||||
WriteSaveFaceTemplateLog(line4);
|
||||
WriteSaveFaceTemplateLog(line5);
|
||||
|
||||
WriteFaceTemplateTransferLog(line1);
|
||||
WriteFaceTemplateTransferLog(line2);
|
||||
WriteFaceTemplateTransferLog(line3);
|
||||
WriteFaceTemplateTransferLog(line4);
|
||||
WriteFaceTemplateTransferLog(line5);
|
||||
}
|
||||
|
||||
private static bool IsAttendanceSimpleLine(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) return false;
|
||||
// Keep only the machine summary lines.
|
||||
if (line.IndexOf(" has ", StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
line.IndexOf(" records", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf(" is not connected", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsMachineUserSimpleLine(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) return false;
|
||||
// Keep only "MACHINE X -> N", "X removed from MACHINE Y", and connectivity lines.
|
||||
if (line.IndexOf(" -> ", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf(" removed from ", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf(" is not connected", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsSaveFaceTemplateLine(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) return false;
|
||||
if (line.StartsWith("Serial number--", StringComparison.OrdinalIgnoreCase)) return true;
|
||||
if (line.IndexOf("SaveTemplateDB", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf("DEVICE_TO_DB", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsFaceTransferLine(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) return false;
|
||||
// Any transfer mode other than DEVICE_TO_DB and connectivity issues.
|
||||
if (line.IndexOf("DB_TO_DEVICE", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf("DEVICE_TO_DEVICE", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf("[DB_TO_DEVICE]", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf("TemplateDistribution", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf("TemplateRegistrationTargets", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
// Peer distribution uses job id "TemplateDist:<ip>" in [TemplateJob ...] lines from GetEmployeeID on targets.
|
||||
if (line.IndexOf("TemplateDist:", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf("SetEmployee", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (line.IndexOf(" is not connected", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void AppendLineToDatedLog(string folderName, string filePrefix, string message)
|
||||
{
|
||||
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
string logsRoot = Path.Combine(baseDir, "Logs");
|
||||
string folderPath = Path.Combine(logsRoot, folderName);
|
||||
|
||||
string datePart = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
string filePath = Path.Combine(folderPath, filePrefix + "_" + datePart + ".txt");
|
||||
LogService.EnqueueLine(filePath, message);
|
||||
}
|
||||
|
||||
private static void WriteAttendanceServiceLog(string message)
|
||||
{
|
||||
AppendLineToDatedLog("Attendance", "AttendanceServiceLogs", message);
|
||||
}
|
||||
|
||||
private static void WriteMachineUserServiceLog(string message)
|
||||
{
|
||||
AppendLineToDatedLog("Machine User service", "MachineServiceLogs", message);
|
||||
}
|
||||
|
||||
private static void WriteSaveFaceTemplateLog(string message)
|
||||
{
|
||||
AppendLineToDatedLog("SaveFaceTemplate logs", "SaveFaceTemplateLogs", message);
|
||||
}
|
||||
|
||||
private static void WriteFaceTemplateTransferLog(string message)
|
||||
{
|
||||
AppendLineToDatedLog("FaceTransferLog", "FaceTransferLogs", message);
|
||||
}
|
||||
public void WriteInternalLog(string Message)
|
||||
{
|
||||
string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\InternalLogs\\InternalLog_" +
|
||||
DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt";
|
||||
LogService.EnqueueLine(filepath, Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
/// <summary>
|
||||
/// F710X / AI Biometric web API client (POST http://{ip}/api).
|
||||
/// Auth: each request includes the device web UI password.
|
||||
/// </summary>
|
||||
internal sealed class HanvonHttpApiClient
|
||||
{
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _password;
|
||||
private readonly int _timeoutMs;
|
||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
|
||||
|
||||
public HanvonHttpApiClient(string machineIp, int httpPort = 80, string password = null, int timeoutMs = 60000)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(machineIp)) throw new ArgumentException("machineIp required");
|
||||
int port = httpPort > 0 ? httpPort : 80;
|
||||
_baseUrl = "http://" + machineIp.Trim() + (port == 80 ? "" : ":" + port) + "/api";
|
||||
_password = password ?? MachineScope.GetDevicePassword() ?? "";
|
||||
_timeoutMs = timeoutMs > 0 ? timeoutMs : 60000;
|
||||
}
|
||||
|
||||
public static HanvonHttpApiClient ForMachine(AttendanceMachine machine)
|
||||
{
|
||||
int httpPort = GetIntSetting("DEVICE_HTTP_PORT", 80);
|
||||
return new HanvonHttpApiClient(machine.MachineIp, httpPort, MachineScope.GetDevicePassword());
|
||||
}
|
||||
|
||||
public bool TryLogin(out string error)
|
||||
{
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
var resp = Post(new Dictionary<string, object>
|
||||
{
|
||||
{ "cmd", "login" },
|
||||
{ "username", MachineScope.GetDeviceUsername() },
|
||||
{ "password", _password },
|
||||
{ "rememberMe", false }
|
||||
});
|
||||
if (IsSuccess(resp)) return true;
|
||||
error = GetMsg(resp) ?? "login failed";
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<HttpLogRecord> GetLogs(DateTime? from, DateTime? to, out string error)
|
||||
{
|
||||
error = null;
|
||||
var all = new List<HttpLogRecord>();
|
||||
try
|
||||
{
|
||||
int index = 0;
|
||||
for (int guard = 0; guard < 10000; guard++)
|
||||
{
|
||||
var req = new Dictionary<string, object>
|
||||
{
|
||||
{ "cmd", "getlog" },
|
||||
{ "password", _password },
|
||||
{ "index", index }
|
||||
};
|
||||
if (from.HasValue) req["from"] = from.Value.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
if (to.HasValue) req["to"] = to.Value.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
var resp = Post(req);
|
||||
if (!IsSuccess(resp))
|
||||
{
|
||||
error = GetMsg(resp) ?? "getlog failed";
|
||||
return all;
|
||||
}
|
||||
|
||||
var batch = ParseLogRecords(resp);
|
||||
all.AddRange(batch);
|
||||
|
||||
int count = GetInt(resp, "count");
|
||||
int toIdx = GetInt(resp, "to");
|
||||
if (count <= 0 || toIdx <= 0 || count <= toIdx || batch.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
index = toIdx + 1;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return all;
|
||||
}
|
||||
}
|
||||
|
||||
public List<HttpUserRecord> GetUserList(out string error)
|
||||
{
|
||||
error = null;
|
||||
var all = new List<HttpUserRecord>();
|
||||
try
|
||||
{
|
||||
int stn = 1;
|
||||
for (int guard = 0; guard < 10000; guard++)
|
||||
{
|
||||
var resp = Post(new Dictionary<string, object>
|
||||
{
|
||||
{ "cmd", "getuserlist" },
|
||||
{ "password", _password },
|
||||
{ "stn", stn }
|
||||
});
|
||||
if (!IsSuccess(resp))
|
||||
{
|
||||
error = GetMsg(resp) ?? "getuserlist failed";
|
||||
return all;
|
||||
}
|
||||
|
||||
var batch = ParseUserRecords(resp);
|
||||
all.AddRange(batch);
|
||||
|
||||
int count = GetInt(resp, "count");
|
||||
int toIdx = GetInt(resp, "to");
|
||||
if (count <= 0 || batch.Count == 0 || (toIdx > 0 && toIdx >= count))
|
||||
{
|
||||
break;
|
||||
}
|
||||
stn = 0;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return all;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DeleteUsers(IEnumerable<string> enrollIds, out string error)
|
||||
{
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
var list = new List<object>();
|
||||
foreach (var id in enrollIds)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id)) continue;
|
||||
if (int.TryParse(id.Trim(), out int n)) list.Add(n);
|
||||
else list.Add(id.Trim());
|
||||
}
|
||||
if (list.Count == 0) return true;
|
||||
|
||||
var resp = Post(new Dictionary<string, object>
|
||||
{
|
||||
{ "cmd", "deleteusers" },
|
||||
{ "password", _password },
|
||||
{ "list", list }
|
||||
});
|
||||
if (IsSuccess(resp)) return true;
|
||||
error = GetMsg(resp) ?? "deleteusers failed";
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CleanLog(out string error)
|
||||
{
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
var resp = Post(new Dictionary<string, object>
|
||||
{
|
||||
{ "cmd", "cleanlog" },
|
||||
{ "password", _password }
|
||||
});
|
||||
if (IsSuccess(resp)) return true;
|
||||
error = GetMsg(resp) ?? "cleanlog failed";
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetFaceRecord(int enrollId, out string recordBase64, out string name, out string error)
|
||||
{
|
||||
recordBase64 = null;
|
||||
name = null;
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
var resp = Post(new Dictionary<string, object>
|
||||
{
|
||||
{ "cmd", "getuserinfo" },
|
||||
{ "password", _password },
|
||||
{ "enrollid", enrollId },
|
||||
{ "backupnum", GetIntSetting("DEVICE_FACE_BACKUPNUM", 50) }
|
||||
});
|
||||
if (!IsSuccess(resp))
|
||||
{
|
||||
error = GetMsg(resp) ?? "getuserinfo failed";
|
||||
return false;
|
||||
}
|
||||
name = GetString(resp, "name");
|
||||
recordBase64 = GetString(resp, "record");
|
||||
if (string.IsNullOrWhiteSpace(recordBase64))
|
||||
{
|
||||
error = "no face/record payload";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, object> Post(Dictionary<string, object> payload)
|
||||
{
|
||||
string body = _json.Serialize(payload);
|
||||
var req = (HttpWebRequest)WebRequest.Create(_baseUrl);
|
||||
req.Method = "POST";
|
||||
req.ContentType = "application/json; charset=utf-8";
|
||||
req.Timeout = _timeoutMs;
|
||||
req.ReadWriteTimeout = _timeoutMs;
|
||||
req.KeepAlive = false;
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(body);
|
||||
req.ContentLength = bytes.Length;
|
||||
using (var stream = req.GetRequestStream())
|
||||
{
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
using (var resp = (HttpWebResponse)req.GetResponse())
|
||||
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.UTF8))
|
||||
{
|
||||
string text = reader.ReadToEnd();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return new Dictionary<string, object>();
|
||||
}
|
||||
return _json.Deserialize<Dictionary<string, object>>(text)
|
||||
?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSuccess(Dictionary<string, object> resp)
|
||||
{
|
||||
if (resp == null) return false;
|
||||
if (!resp.TryGetValue("result", out object r) || r == null) return false;
|
||||
if (r is bool b) return b;
|
||||
return string.Equals(r.ToString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetMsg(Dictionary<string, object> resp)
|
||||
{
|
||||
return GetString(resp, "msg") ?? GetString(resp, "reason");
|
||||
}
|
||||
|
||||
private static string GetString(Dictionary<string, object> resp, string key)
|
||||
{
|
||||
if (resp == null || !resp.TryGetValue(key, out object v) || v == null) return null;
|
||||
return v.ToString();
|
||||
}
|
||||
|
||||
private static int GetInt(Dictionary<string, object> resp, string key)
|
||||
{
|
||||
if (resp == null || !resp.TryGetValue(key, out object v) || v == null) return 0;
|
||||
if (v is int i) return i;
|
||||
if (v is long l) return (int)l;
|
||||
if (v is decimal d) return (int)d;
|
||||
int.TryParse(v.ToString(), out int n);
|
||||
return n;
|
||||
}
|
||||
|
||||
private static List<HttpLogRecord> ParseLogRecords(Dictionary<string, object> resp)
|
||||
{
|
||||
var list = new List<HttpLogRecord>();
|
||||
if (resp == null || !resp.TryGetValue("record", out object rec) || rec == null) return list;
|
||||
if (rec is ArrayList arr)
|
||||
{
|
||||
foreach (var item in arr) AddLog(list, item as Dictionary<string, object>);
|
||||
}
|
||||
else if (rec is object[] oa)
|
||||
{
|
||||
foreach (var item in oa) AddLog(list, item as Dictionary<string, object>);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void AddLog(List<HttpLogRecord> list, Dictionary<string, object> row)
|
||||
{
|
||||
if (row == null) return;
|
||||
string enroll = GetString(row, "enrollid") ?? "";
|
||||
string name = GetString(row, "name") ?? "";
|
||||
string timeStr = GetString(row, "time") ?? "";
|
||||
if (!DateTime.TryParse(timeStr, out DateTime time)) return;
|
||||
string acNo = !string.IsNullOrWhiteSpace(name) ? name.Trim() : enroll.Trim();
|
||||
if (string.IsNullOrWhiteSpace(acNo)) return;
|
||||
list.Add(new HttpLogRecord
|
||||
{
|
||||
EnrollId = enroll,
|
||||
Name = name,
|
||||
AcNo = acNo,
|
||||
CheckTime = time,
|
||||
Mode = GetInt(row, "mode"),
|
||||
InOut = GetInt(row, "inout"),
|
||||
Event = GetInt(row, "event")
|
||||
});
|
||||
}
|
||||
|
||||
private static List<HttpUserRecord> ParseUserRecords(Dictionary<string, object> resp)
|
||||
{
|
||||
var list = new List<HttpUserRecord>();
|
||||
if (resp == null || !resp.TryGetValue("record", out object rec) || rec == null) return list;
|
||||
|
||||
IEnumerable items = null;
|
||||
if (rec is ArrayList arr) items = arr;
|
||||
else if (rec is object[] oa) items = oa;
|
||||
if (items == null) return list;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var row = item as Dictionary<string, object>;
|
||||
if (row == null) continue;
|
||||
string id = GetString(row, "id") ?? "";
|
||||
string name = GetString(row, "name") ?? "";
|
||||
string serial = !string.IsNullOrWhiteSpace(name) ? name.Trim() : id.Trim();
|
||||
if (string.IsNullOrWhiteSpace(serial)) continue;
|
||||
list.Add(new HttpUserRecord
|
||||
{
|
||||
DeviceUserId = id.Trim(),
|
||||
Name = name.Trim(),
|
||||
SerialNumber = serial
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static int GetIntSetting(string key, int defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = ConfigurationManager.AppSettings[key];
|
||||
if (string.IsNullOrWhiteSpace(raw)) return defaultValue;
|
||||
return int.TryParse(raw.Trim(), out var v) && v > 0 ? v : defaultValue;
|
||||
}
|
||||
catch { return defaultValue; }
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class HttpLogRecord
|
||||
{
|
||||
public string EnrollId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string AcNo { get; set; }
|
||||
public DateTime CheckTime { get; set; }
|
||||
public int Mode { get; set; }
|
||||
public int InOut { get; set; }
|
||||
public int Event { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class HttpUserRecord
|
||||
{
|
||||
public string DeviceUserId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string SerialNumber { get; set; }
|
||||
}
|
||||
|
||||
internal static class DeviceProtocol
|
||||
{
|
||||
public static bool UseHttp()
|
||||
{
|
||||
var p = ConfigurationManager.AppSettings["DEVICE_PROTOCOL"] ?? "HTTP";
|
||||
return string.Equals(p.Trim(), "HTTP", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(p.Trim(), "WEB", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(p.Trim(), "API", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
Hanvon F710X Attendance Service - Deployment Notes
|
||||
==================================================
|
||||
|
||||
1. Build output folder: bin\Debug\ (or bin\Release\)
|
||||
|
||||
2. Copy Hanvon HDCP SDK native DLLs next to the EXE:
|
||||
- HwDevComm.dll
|
||||
- HDCP_Utils.dll
|
||||
These ship with the Hanvon face-terminal SDK (HDCP protocol). F710/F710X is supported by this SDK.
|
||||
|
||||
3. Configure App.config (copied as HanvonF710XAttendanceService.exe.config):
|
||||
- connectionStrings / HrmsDb
|
||||
- ATTENDANCE_SCOPE = CENTRAL or SITE
|
||||
- MACHINE_TYPES = HANVON,F710X,F710
|
||||
- DEVICE_TYPE = HW_HDCP
|
||||
- ATTENDANCE_DELETE_AFTER_INSERT = true
|
||||
|
||||
4. Configure DeviceSettings.xml for SITE scope and template transfer jobs.
|
||||
|
||||
5. Ensure attendance_machine rows use machine_type HANVON (or F710X/F710).
|
||||
|
||||
6. Install the Windows service (run elevated):
|
||||
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe HanvonF710XAttendanceService.exe
|
||||
|
||||
7. Start the service:
|
||||
net start HanvonF710XAttendanceService
|
||||
|
||||
8. Uninstall:
|
||||
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe /u HanvonF710XAttendanceService.exe
|
||||
|
||||
Log folders (created at runtime beside the EXE):
|
||||
- InternalLogs\
|
||||
- Logs\Attendance\
|
||||
- Logs\Machine User service\
|
||||
- Logs\SaveFaceTemplate logs\
|
||||
- Logs\FaceTransferLog\
|
||||
- TemplateRawLogs\
|
||||
|
||||
Jobs:
|
||||
- Attendance + machine users: every 20 minutes (ATTENDANCE_JOB_INTERVAL_MINUTES)
|
||||
- Templates: every 4 hours (TEMPLATE_JOB_INTERVAL_MINUTES)
|
||||
- Both jobs run once on startup (templates after 30s delay)
|
||||
|
||||
Console / debug mode (no InstallUtil needed):
|
||||
- From Visual Studio: F5 (Debug builds run as console when interactive)
|
||||
- From command line:
|
||||
HanvonF710XAttendanceService.exe --console
|
||||
HanvonF710XAttendanceService.exe --debug
|
||||
- Force Windows Service host even if interactive:
|
||||
HanvonF710XAttendanceService.exe --service
|
||||
- Press Enter or Ctrl+C to stop console mode.
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
internal static class LogService
|
||||
{
|
||||
private sealed class LogItem
|
||||
{
|
||||
public string FilePath { get; }
|
||||
public string Line { get; }
|
||||
public LogItem(string filePath, string line)
|
||||
{
|
||||
FilePath = filePath;
|
||||
Line = line;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ConcurrentQueue<LogItem> _queue = new ConcurrentQueue<LogItem>();
|
||||
private static readonly object _startLock = new object();
|
||||
private static CancellationTokenSource _cts;
|
||||
private static Task _worker;
|
||||
private static volatile bool _started;
|
||||
|
||||
// One global cross-process mutex to serialize file writes across multiple services.
|
||||
private static readonly Mutex _globalMutex = new Mutex(false, @"Global\HanvonF710XAttendanceService_LogMutex");
|
||||
|
||||
public static void Start()
|
||||
{
|
||||
lock (_startLock)
|
||||
{
|
||||
if (_started) return;
|
||||
_started = true;
|
||||
_cts = new CancellationTokenSource();
|
||||
_worker = Task.Run(() => WorkerLoop(_cts.Token));
|
||||
}
|
||||
}
|
||||
|
||||
public static void StopAndFlush(TimeSpan maxWait)
|
||||
{
|
||||
lock (_startLock)
|
||||
{
|
||||
if (!_started) return;
|
||||
_cts.Cancel();
|
||||
}
|
||||
|
||||
try { _worker?.Wait(maxWait); } catch { }
|
||||
|
||||
// Best-effort final flush
|
||||
try { FlushOnce(maxItems: 5000, allowWait: true); } catch { }
|
||||
}
|
||||
|
||||
public static void EnqueueLine(string filePath, string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath)) return;
|
||||
if (line == null) line = "";
|
||||
|
||||
if (!_started)
|
||||
{
|
||||
// If service forgot to start the logger, still try to start it lazily.
|
||||
Start();
|
||||
}
|
||||
|
||||
_queue.Enqueue(new LogItem(filePath, line));
|
||||
}
|
||||
|
||||
private static async Task WorkerLoop(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
FlushOnce(maxItems: 500, allowWait: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// swallow; keep loop alive
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(1000, token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore cancellation timing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlushOnce(int maxItems, bool allowWait)
|
||||
{
|
||||
if (_queue.IsEmpty) return;
|
||||
|
||||
var drained = new List<LogItem>(maxItems);
|
||||
while (drained.Count < maxItems && _queue.TryDequeue(out var item))
|
||||
{
|
||||
drained.Add(item);
|
||||
}
|
||||
|
||||
if (drained.Count == 0) return;
|
||||
|
||||
bool mutexTaken = false;
|
||||
try
|
||||
{
|
||||
// Avoid blocking service operations; only background worker waits.
|
||||
int waitMs = allowWait ? 2000 : 0;
|
||||
mutexTaken = _globalMutex.WaitOne(waitMs);
|
||||
if (!mutexTaken)
|
||||
{
|
||||
// Couldn't acquire lock; put back and try later.
|
||||
for (int i = drained.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_queue.Enqueue(drained[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var grp in drained.GroupBy(d => d.FilePath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
string filePath = grp.Key;
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
// Open with FileShare.ReadWrite so other processes can read while we write.
|
||||
using (var fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
|
||||
using (var sw = new StreamWriter(fs, Encoding.UTF8))
|
||||
{
|
||||
foreach (var li in grp)
|
||||
{
|
||||
sw.WriteLine(li.Line);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// If file is temporarily locked, re-enqueue and try next flush.
|
||||
foreach (var li in grp)
|
||||
{
|
||||
_queue.Enqueue(li);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Drop lines on unexpected errors (best-effort logging).
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (mutexTaken)
|
||||
{
|
||||
try { _globalMutex.ReleaseMutex(); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
using MySql.Data.MySqlClient;
|
||||
using System;
|
||||
using System.Configuration;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
internal static class MachineScope
|
||||
{
|
||||
public const string MachineTypeHanvon = "HANVON";
|
||||
|
||||
public static string GetDeviceType()
|
||||
{
|
||||
return ConfigurationManager.AppSettings["DEVICE_TYPE"] ?? "HW_HDCP";
|
||||
}
|
||||
|
||||
public static string GetDeviceUsername()
|
||||
{
|
||||
return ConfigurationManager.AppSettings["DEVICE_USERNAME"] ?? "admin";
|
||||
}
|
||||
|
||||
public static string GetDevicePassword()
|
||||
{
|
||||
return ConfigurationManager.AppSettings["DEVICE_PASSWORD"] ?? "12345678";
|
||||
}
|
||||
|
||||
public static bool IsHanvonMachineType(string machineType)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(machineType)
|
||||
&& string.Equals(machineType.Trim(), MachineTypeHanvon, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string BuildMachineTypeWhereClause(MySqlCommand cmd, string columnName = "machine_type", string paramPrefix = "mt")
|
||||
{
|
||||
string p = "@" + paramPrefix;
|
||||
cmd.Parameters.AddWithValue(p, MachineTypeHanvon);
|
||||
return $"UPPER({columnName}) = {p}";
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,60 @@
|
|||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
partial class ProjectInstaller
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Component Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.serviceProcessInstaller1 = new System.ServiceProcess.ServiceProcessInstaller();
|
||||
this.serviceInstaller1 = new System.ServiceProcess.ServiceInstaller();
|
||||
//
|
||||
// serviceProcessInstaller1
|
||||
//
|
||||
this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem;
|
||||
this.serviceProcessInstaller1.Password = null;
|
||||
this.serviceProcessInstaller1.Username = null;
|
||||
//
|
||||
// serviceInstaller1
|
||||
//
|
||||
this.serviceInstaller1.Description = "Hanvon F710X attendance, user sync, and face template service";
|
||||
this.serviceInstaller1.DisplayName = "Hanvon F710X Attendance Service";
|
||||
this.serviceInstaller1.ServiceName = "HanvonF710XAttendanceService";
|
||||
//
|
||||
// ProjectInstaller
|
||||
//
|
||||
this.Installers.AddRange(new System.Configuration.Install.Installer[] {
|
||||
this.serviceProcessInstaller1,
|
||||
this.serviceInstaller1});
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1;
|
||||
private System.ServiceProcess.ServiceInstaller serviceInstaller1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Configuration.Install;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
[RunInstaller(true)]
|
||||
public partial class ProjectInstaller : System.Configuration.Install.Installer
|
||||
{
|
||||
public ProjectInstaller()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="serviceProcessInstaller1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="serviceInstaller1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>196, 17</value>
|
||||
</metadata>
|
||||
<metadata name="$this.TrayLargeIcon" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>False</value>
|
||||
</metadata>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("HanvonF710XAttendanceService")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("HanvonF710XAttendanceService")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2021")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("00db44af-5145-417f-9dc7-8490ad30ce49")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
internal static class TemplateParser
|
||||
{
|
||||
// Mirrors UTS MainForm.removeReturn(...)
|
||||
public static bool TryExtractTemplatePayloadFromGetEmployeeResponse(string getEmployeeResponse, out string payload)
|
||||
{
|
||||
payload = null;
|
||||
if (string.IsNullOrWhiteSpace(getEmployeeResponse))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] splitData = getEmployeeResponse.Split('(');
|
||||
for (int i = 0; i < splitData.Length; i++)
|
||||
{
|
||||
string s = splitData[i];
|
||||
if (!s.Contains("Return"))
|
||||
{
|
||||
string updatedData = s.Replace("result=\"success\" ", "");
|
||||
string[] updatedResult = updatedData.Split(')');
|
||||
payload = updatedResult.Length > 0 ? updatedResult[0] : null;
|
||||
payload = string.IsNullOrWhiteSpace(payload) ? null : payload;
|
||||
return payload != null;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mirrors UTS MainForm.SetEmployeeInfo(...) string transformation
|
||||
public static bool TryBuildSetEmployeeCommand(string templatePayloadOrGetEmployeeResponse, out string setEmployeeCommand)
|
||||
{
|
||||
setEmployeeCommand = null;
|
||||
if (string.IsNullOrWhiteSpace(templatePayloadOrGetEmployeeResponse))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] splitData = templatePayloadOrGetEmployeeResponse.Split('(');
|
||||
for (int i = 0; i < splitData.Length; i++)
|
||||
{
|
||||
string s = splitData[i];
|
||||
if (!s.Contains("Return"))
|
||||
{
|
||||
string updatedData = s.Replace("result=\"success\" ", "");
|
||||
string[] updatedResult = updatedData.Split(')');
|
||||
var inner = updatedResult.Length > 0 ? updatedResult[0] : null;
|
||||
if (!string.IsNullOrWhiteSpace(inner))
|
||||
{
|
||||
setEmployeeCommand = "SetEmployee(" + inner + ")";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<string> ParseEmployeeIdsFromGetEmployeeIdResponse(string getEmployeeIdResponse)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(getEmployeeIdResponse))
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// Typical responses contain many occurrences like: id="6286"
|
||||
var matches = Regex.Matches(getEmployeeIdResponse, "\\bid=\"([^\"]+)\"");
|
||||
var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Match m in matches)
|
||||
{
|
||||
if (!m.Success || m.Groups.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = m.Groups[1].Value?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ids.Add(id);
|
||||
}
|
||||
|
||||
return ids.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,865 @@
|
|||
using MySql.Data.MySqlClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace HanvonF710XAttendanceService
|
||||
{
|
||||
internal enum TemplateTransferMode
|
||||
{
|
||||
DEVICE_TO_DB,
|
||||
DB_TO_DEVICE,
|
||||
DEVICE_TO_DEVICE
|
||||
}
|
||||
|
||||
internal sealed class TemplateTransferRunner
|
||||
{
|
||||
private readonly AttendanceMachineDAO _machineDao;
|
||||
private readonly AttendanceMachineFaceTemplateDAO _templateDao;
|
||||
|
||||
public TemplateTransferRunner(AttendanceMachineDAO machineDao, AttendanceMachineFaceTemplateDAO templateDao)
|
||||
{
|
||||
_machineDao = machineDao ?? throw new ArgumentNullException(nameof(machineDao));
|
||||
_templateDao = templateDao ?? throw new ArgumentNullException(nameof(templateDao));
|
||||
}
|
||||
|
||||
public void RunJobs(DeviceSettingsConfig settings, TemplateTransferMode mode, MySqlConnection connection, List<string> logs)
|
||||
{
|
||||
if (mode == TemplateTransferMode.DEVICE_TO_DB)
|
||||
{
|
||||
// TEMPLATE SAVE (DEVICE_TO_DB) now follows the same scope model
|
||||
// as attendance and machine user sync (CENTRAL vs SITE).
|
||||
var machinesInScope = Program.GetMachinesForAttendanceAndUsers(_machineDao, connection, logs);
|
||||
var scope = (ConfigurationManager.AppSettings["ATTENDANCE_SCOPE"] ?? "CENTRAL").ToUpperInvariant();
|
||||
logs.Add($"Template DEVICE_TO_DB scope={scope}. machines_in_scope={machinesInScope.Count}");
|
||||
bool autoDistributeAfterSave = GetBoolAppSetting("TEMPLATE_AUTO_DISTRIBUTE_AFTER_DEVICE_TO_DB", true);
|
||||
|
||||
if (machinesInScope.Count == 0)
|
||||
{
|
||||
logs.Add("Template DEVICE_TO_DB: no machines found in scope.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var m in machinesInScope)
|
||||
{
|
||||
if (!_machineDao.TryGetAttendanceMachineByIp(connection, m.MachineIp, out var sourceMachine, out var sourceType))
|
||||
{
|
||||
logs.Add($"[TemplateSave] source machine not found in DB. machine_id={m.MachineId} ip={m.MachineIp}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MachineScope.IsHanvonMachineType(sourceType))
|
||||
{
|
||||
logs.Add($"[TemplateSave] source machine_type={sourceType} not supported for Hanvon F710X service. machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp}. Skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
logs.Add($"[TemplateSave] DEVICE_TO_DB start. machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp}");
|
||||
bool saveOk = SaveTemplateDbFromDevice(sourceMachine, connection, logs, $"TemplateSave:{sourceMachine.MachineIp}");
|
||||
if (saveOk && autoDistributeAfterSave)
|
||||
{
|
||||
DistributeMissingTemplatesAfterDeviceToDb(sourceMachine, machinesInScope, logs);
|
||||
}
|
||||
else if (!saveOk)
|
||||
{
|
||||
LogDistributionLine($"[TemplateDistribution] skipped for source_machine={sourceMachine.MachineId} ip={sourceMachine.MachineIp} (DEVICE_TO_DB did not complete; device unreachable or no employee list).", logs);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings?.TemplateTransfer == null || !settings.TemplateTransfer.Enabled)
|
||||
{
|
||||
logs.Add("TemplateTransfer is disabled in DeviceSettings.");
|
||||
return;
|
||||
}
|
||||
|
||||
var enabledJobs = settings.TemplateTransfer.Jobs.Where(j => j.Enabled).ToList();
|
||||
|
||||
if (enabledJobs.Count == 0)
|
||||
{
|
||||
logs.Add("No enabled TemplateTransfer jobs found.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var job in enabledJobs)
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] Mode={mode} SourceIp={job.SourceIp} Targets={job.TargetIps.Count}");
|
||||
|
||||
if (!_machineDao.TryGetAttendanceMachineByIp(connection, job.SourceIp, out var sourceMachine, out var sourceType))
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] Source machine not found in DB. ip={job.SourceIp}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MachineScope.IsHanvonMachineType(sourceType))
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] Source machine_type={sourceType} is not a Hanvon F710X device. Skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetMachines = new List<(AttendanceMachine Machine, string Type)>();
|
||||
foreach (var targetIp in job.TargetIps.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!_machineDao.TryGetAttendanceMachineByIp(connection, targetIp, out var targetMachine, out var targetType))
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] Target machine not found in DB. ip={targetIp}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MachineScope.IsHanvonMachineType(targetType))
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] Target ip={targetIp} machine_type={targetType} is not a Hanvon F710X device. Skipping this target.");
|
||||
continue;
|
||||
}
|
||||
|
||||
targetMachines.Add((targetMachine, targetType));
|
||||
}
|
||||
|
||||
if (targetMachines.Count == 0)
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] No valid targets to transfer to.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var employeeIds, logs, job.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (employeeIds.Count == 0)
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] Source device returned 0 employee IDs.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == TemplateTransferMode.DB_TO_DEVICE)
|
||||
{
|
||||
TransferDbToDevice(job, employeeIds, targetMachines, connection, logs);
|
||||
}
|
||||
else if (mode == TemplateTransferMode.DEVICE_TO_DEVICE)
|
||||
{
|
||||
TransferDeviceToDevice(job.Id, sourceMachine, employeeIds, targetMachines, logs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetEmployeeIdsFromDevice(AttendanceMachine machine, out List<string> employeeIds, List<string> logs, string jobId)
|
||||
{
|
||||
employeeIds = new List<string>();
|
||||
|
||||
if (DeviceProtocol.UseHttp())
|
||||
{
|
||||
var client = HanvonHttpApiClient.ForMachine(machine);
|
||||
if (!client.TryLogin(out string loginErr))
|
||||
{
|
||||
logs.Add($"[TemplateJob {jobId}] HTTP login failed. source={machine.MachineIp} err={loginErr}");
|
||||
Program.RecordUnreachableMachine(machine, "TemplateSave HTTP: " + loginErr);
|
||||
return false;
|
||||
}
|
||||
var users = client.GetUserList(out string listErr);
|
||||
if (!string.IsNullOrEmpty(listErr) && (users == null || users.Count == 0))
|
||||
{
|
||||
logs.Add($"[TemplateJob {jobId}] getuserlist failed. source={machine.MachineIp} err={listErr}");
|
||||
Program.RecordUnreachableMachine(machine, "TemplateSave HTTP getuserlist: " + listErr);
|
||||
return false;
|
||||
}
|
||||
foreach (var u in users)
|
||||
{
|
||||
// Prefer device enroll id for getuserinfo; also keep serial for DB key.
|
||||
if (!string.IsNullOrWhiteSpace(u.DeviceUserId))
|
||||
employeeIds.Add(u.DeviceUserId);
|
||||
else if (!string.IsNullOrWhiteSpace(u.SerialNumber))
|
||||
employeeIds.Add(u.SerialNumber);
|
||||
}
|
||||
logs.Add($"[TemplateJob {jobId}] HTTP getuserlist ok. parsed={employeeIds.Count}");
|
||||
return true;
|
||||
}
|
||||
|
||||
var callback = new CallBack(Program.BeCalled);
|
||||
string devInfo = machine.GetDeviceInfo();
|
||||
string cmd = "GetEmployeeID()";
|
||||
string response = "";
|
||||
uint recvLen = 0;
|
||||
|
||||
if (Program.test(devInfo, devInfo.Length, cmd, cmd.Length, ref response, ref recvLen, callback) != 0)
|
||||
{
|
||||
logs.Add($"[TemplateJob {jobId}] GetEmployeeID() failed. source={machine.MachineIp}");
|
||||
Program.RecordUnreachableMachine(machine, "TemplateSave: device not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
employeeIds = TemplateParser.ParseEmployeeIdsFromGetEmployeeIdResponse(response);
|
||||
try
|
||||
{
|
||||
logs.Add($"[TemplateJob {jobId}] GetEmployeeID() ok. total={Program.GetLoopCnt(response)} parsed={employeeIds.Count}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
logs.Add($"[TemplateJob {jobId}] GetEmployeeID() ok. parsed={employeeIds.Count}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SaveTemplateDbFromDevice(AttendanceMachine sourceMachine, MySqlConnection connection, List<string> logs, string jobId)
|
||||
{
|
||||
var swTotal = Stopwatch.StartNew();
|
||||
if (!TryGetEmployeeIdsFromDevice(sourceMachine, out var employeeIds, logs, jobId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int inserted = 0;
|
||||
int skippedExisting = 0;
|
||||
int failed = 0;
|
||||
|
||||
bool capturedRaw = false;
|
||||
|
||||
int totalIds = employeeIds.Count;
|
||||
int batchSize = ChooseBatchSize(totalIds);
|
||||
var batches = SplitIntoBatches(employeeIds, batchSize).ToList();
|
||||
logs.Add($"[TemplateSave] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} total_ids={totalIds} batch_size={batchSize} total_batches={batches.Count}");
|
||||
|
||||
for (int bi = 0; bi < batches.Count; bi++)
|
||||
{
|
||||
var swBatch = Stopwatch.StartNew();
|
||||
int attempted = 0;
|
||||
int bInserted = 0;
|
||||
int bExisting = 0;
|
||||
int bFailed = 0;
|
||||
|
||||
foreach (var empId in batches[bi])
|
||||
{
|
||||
attempted++;
|
||||
try
|
||||
{
|
||||
if (_templateDao.ExistsBySerialNo(connection, empId))
|
||||
{
|
||||
skippedExisting++;
|
||||
bExisting++;
|
||||
// Simple log line for SaveFaceTemplate logs (keep deep logs in InternalLogs as-is).
|
||||
logs.Add($"Serial number-- {empId} already exists in the database");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DeviceProtocol.UseHttp())
|
||||
{
|
||||
if (!int.TryParse(empId, out int enrollId))
|
||||
{
|
||||
failed++;
|
||||
bFailed++;
|
||||
logs.Add($"[TemplateJob {jobId}] invalid enroll id for HTTP. emp={empId}");
|
||||
continue;
|
||||
}
|
||||
var httpClient = HanvonHttpApiClient.ForMachine(sourceMachine);
|
||||
if (!httpClient.TryGetFaceRecord(enrollId, out string recordB64, out string empName, out string faceErr))
|
||||
{
|
||||
failed++;
|
||||
bFailed++;
|
||||
logs.Add($"[TemplateJob {jobId}] HTTP getuserinfo failed. emp={empId} err={faceErr}");
|
||||
continue;
|
||||
}
|
||||
// Store under employee name/serial when available.
|
||||
string serialKey = !string.IsNullOrWhiteSpace(empName) ? empName.Trim() : empId;
|
||||
if (_templateDao.ExistsBySerialNo(connection, serialKey))
|
||||
{
|
||||
skippedExisting++;
|
||||
bExisting++;
|
||||
logs.Add($"Serial number-- {serialKey} already exists in the database");
|
||||
continue;
|
||||
}
|
||||
var faceBytes = Encoding.UTF8.GetBytes(recordB64);
|
||||
int faceRows = _templateDao.InsertTemplate(connection, serialKey, faceBytes, DateTime.Now);
|
||||
if (faceRows > 0) { inserted++; bInserted++; }
|
||||
else { failed++; bFailed++; }
|
||||
continue;
|
||||
}
|
||||
|
||||
var callback = new CallBack(Program.BeCalled);
|
||||
string devInfo = sourceMachine.GetDeviceInfo();
|
||||
string cmd = "GetEmployee(id=\"" + empId + "\")";
|
||||
string response = "";
|
||||
uint recvLen = 0;
|
||||
|
||||
if (Program.test(devInfo, devInfo.Length, cmd, cmd.Length, ref response, ref recvLen, callback) != 0)
|
||||
{
|
||||
failed++;
|
||||
bFailed++;
|
||||
logs.Add($"[TemplateJob {jobId}] GetEmployee failed. emp={empId} source={sourceMachine.MachineIp}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!capturedRaw && response != null && response.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
capturedRaw = true;
|
||||
WriteRawTemplateLog($"[TemplateJob {jobId}] GetEmployee raw response emp={empId}", response);
|
||||
}
|
||||
|
||||
if (response == null || response.IndexOf("success", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
failed++;
|
||||
bFailed++;
|
||||
logs.Add($"[TemplateJob {jobId}] GetEmployee returned non-success. emp={empId} source={sourceMachine.MachineIp}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TemplateParser.TryExtractTemplatePayloadFromGetEmployeeResponse(response, out var payload))
|
||||
{
|
||||
failed++;
|
||||
bFailed++;
|
||||
logs.Add($"[TemplateJob {jobId}] Failed to parse template payload. emp={empId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(payload);
|
||||
int rows = _templateDao.InsertTemplate(connection, empId, bytes, DateTime.Now);
|
||||
if (rows > 0)
|
||||
{
|
||||
inserted++;
|
||||
bInserted++;
|
||||
}
|
||||
else
|
||||
{
|
||||
failed++;
|
||||
bFailed++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failed++;
|
||||
bFailed++;
|
||||
logs.Add($"[TemplateJob {jobId}] SaveTemplateDB error. emp={empId} err={ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
swBatch.Stop();
|
||||
logs.Add($"[TemplateSaveBatch] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} batch={bi + 1}/{batches.Count} attempted={attempted} inserted={bInserted} existing={bExisting} failed={bFailed} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}");
|
||||
}
|
||||
|
||||
swTotal.Stop();
|
||||
logs.Add($"[TemplateSave] machine_id={sourceMachine.MachineId} ip={sourceMachine.MachineIp} completed inserted={inserted} existing={skippedExisting} failed={failed} duration_sec={Math.Round(swTotal.Elapsed.TotalSeconds, 1)}");
|
||||
logs.Add($"[TemplateJob {jobId}] SaveTemplateDB done. inserted={inserted} existing={skippedExisting} failed={failed}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After DEVICE_TO_DB, distribute templates to peer Nedo machines using registration-based target selection:
|
||||
/// only push an employee template to machines where that employee is registered (attendance_machine_user, is_deleted=0).
|
||||
/// Central truth is still DB templates; device lists are only used to skip already-present templates.
|
||||
/// Uses a dedicated DB connection for query + template fetch (no shared connection with attendance job).
|
||||
/// </summary>
|
||||
private void DistributeMissingTemplatesAfterDeviceToDb(AttendanceMachine sourceMachine, List<AttendanceMachine> machinesInScope, List<string> logs)
|
||||
{
|
||||
if (sourceMachine == null || machinesInScope == null || machinesInScope.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var peers = machinesInScope.Where(m => !IsSameScopedMachine(m, sourceMachine)).ToList();
|
||||
if (peers.Count == 0)
|
||||
{
|
||||
LogDistributionLine($"[TemplateDistributionPlan] source_machine={sourceMachine.MachineId} ip={sourceMachine.MachineIp} no peer Hanvon machines in scope; skipping.", logs);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var distConn = new MySqlConnection(Program.GetConnectionString()))
|
||||
{
|
||||
distConn.Open();
|
||||
|
||||
// Targets to consider are already filtered by ATTENDANCE_SCOPE + DeviceSettings in machinesInScope.
|
||||
// Now apply registration-based selection inside DB queries.
|
||||
var targets = new List<AttendanceMachine>();
|
||||
foreach (var t in peers)
|
||||
{
|
||||
if (t == null) continue;
|
||||
if (string.IsNullOrWhiteSpace(t.MachineId) || string.IsNullOrWhiteSpace(t.MachineIp))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
targets.Add(t);
|
||||
}
|
||||
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
LogDistributionLine("[TemplateDistribution] no valid peer Hanvon targets in scope; skipping.", logs);
|
||||
return;
|
||||
}
|
||||
|
||||
var allowedMachineIds = targets
|
||||
.Select(t => t.MachineId)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
// 1) For each target machine, get already-present employee ids from the device,
|
||||
// then get DB templates only for employees registered on that specific target.
|
||||
// 2) missing = registeredTemplates - onDeviceIds
|
||||
var toSendByTarget = new Dictionary<string, Dictionary<string, byte[]>>(StringComparer.OrdinalIgnoreCase);
|
||||
var missingSerialsUnion = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var targetMachine in targets)
|
||||
{
|
||||
if (!TryGetEmployeeIdsFromDevice(targetMachine, out var onDeviceIds, logs, "TemplateDist:" + targetMachine.MachineIp))
|
||||
{
|
||||
continue; // unreachable or device communication error already logged
|
||||
}
|
||||
|
||||
var onDevice = new HashSet<string>(onDeviceIds, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Registration-based: only templates whose employee is registered to THIS target machine.
|
||||
var registeredTemplates = _templateDao.GetActiveTemplatesForRegisteredMachine(distConn, targetMachine.MachineId);
|
||||
|
||||
var missing = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kvp in registeredTemplates)
|
||||
{
|
||||
if (!onDevice.Contains(kvp.Key))
|
||||
{
|
||||
missing[kvp.Key] = kvp.Value;
|
||||
missingSerialsUnion.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
toSendByTarget[targetMachine.MachineId] = missing;
|
||||
}
|
||||
|
||||
if (missingSerialsUnion.Count == 0)
|
||||
{
|
||||
LogDistributionLine("[TemplateDistribution] nothing missing on peer Hanvon machines; registration-based distribution skipped.", logs);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build serial -> list of registered target machine ids (restricted to current scope targets).
|
||||
var serialToRegisteredTargets = _templateDao.GetRegisteredTargetMachineIdsForSerialNos(
|
||||
distConn,
|
||||
missingSerialsUnion.ToList(),
|
||||
allowedMachineIds);
|
||||
|
||||
// Pre-compute pushed_to / skipped_existing per serial (based on which targets have missing templates).
|
||||
var pushedTargetsBySerial = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var serial in missingSerialsUnion)
|
||||
{
|
||||
pushedTargetsBySerial[serial] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
foreach (var kvp in toSendByTarget)
|
||||
{
|
||||
var targetMachineId = kvp.Key;
|
||||
var missingTemplates = kvp.Value;
|
||||
if (missingTemplates == null || missingTemplates.Count == 0) continue;
|
||||
|
||||
foreach (var serial in missingTemplates.Keys)
|
||||
{
|
||||
if (pushedTargetsBySerial.TryGetValue(serial, out var set))
|
||||
{
|
||||
set.Add(targetMachineId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log registration targets + per-serial distribution summary.
|
||||
foreach (var serial in missingSerialsUnion)
|
||||
{
|
||||
int registeredTargets = 0;
|
||||
if (serialToRegisteredTargets.TryGetValue(serial, out var targetList) && targetList != null)
|
||||
{
|
||||
registeredTargets = targetList.Count;
|
||||
}
|
||||
|
||||
int pushedTo = 0;
|
||||
if (pushedTargetsBySerial.TryGetValue(serial, out var pushedSet) && pushedSet != null)
|
||||
{
|
||||
pushedTo = pushedSet.Count;
|
||||
}
|
||||
|
||||
int skippedExisting = Math.Max(0, registeredTargets - pushedTo);
|
||||
|
||||
LogDistributionLine($"[TemplateRegistrationTargets] serial_no={serial} registered_targets={registeredTargets}", logs);
|
||||
LogDistributionLine($"[TemplateDistributionSummary] serial_no={serial} total_targets={registeredTargets} pushed_to={pushedTo} skipped_existing={skippedExisting}", logs);
|
||||
}
|
||||
|
||||
// 3) Push missing templates in batches per target machine.
|
||||
foreach (var targetMachine in targets)
|
||||
{
|
||||
if (!toSendByTarget.TryGetValue(targetMachine.MachineId, out var missingTemplates) ||
|
||||
missingTemplates == null ||
|
||||
missingTemplates.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int batchSize = ChooseBatchSize(missingTemplates.Count);
|
||||
int totalBatches = (missingTemplates.Count + batchSize - 1) / batchSize;
|
||||
LogDistributionLine(
|
||||
$"[TemplateDistributionPlan] target_machine={targetMachine.MachineId} missing_templates={missingTemplates.Count} batch_size={batchSize} total_batches={totalBatches}",
|
||||
logs);
|
||||
|
||||
foreach (var serial in missingTemplates.Keys)
|
||||
{
|
||||
LogDistributionLine(
|
||||
$"[TemplateDistributionPlan] serial_no={serial} target_machine={targetMachine.MachineId} already_present=false will_push=true",
|
||||
logs);
|
||||
}
|
||||
|
||||
PushDistributionBatches(targetMachine, missingTemplates, batchSize, logs, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogDistributionLine("[TemplateDistribution] failed: " + ex.ToString(), logs);
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogDistributionLine(string message, List<string> logs)
|
||||
{
|
||||
logs?.Add(message);
|
||||
}
|
||||
|
||||
private static bool IsSameScopedMachine(AttendanceMachine a, AttendanceMachine b)
|
||||
{
|
||||
if (a == null || b == null) return false;
|
||||
if (!string.IsNullOrWhiteSpace(a.MachineId) && !string.IsNullOrWhiteSpace(b.MachineId))
|
||||
{
|
||||
return string.Equals(a.MachineId.Trim(), b.MachineId.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return string.Equals(a.MachineIp?.Trim(), b.MachineIp?.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Send only missing templates; does not issue SetEmployee for serials already on device.</summary>
|
||||
private int PushDistributionBatches(AttendanceMachine target, Dictionary<string, byte[]> templates, int batchSize, List<string> logs, out int failedTotal)
|
||||
{
|
||||
failedTotal = 0;
|
||||
if (templates == null || templates.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true);
|
||||
int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0);
|
||||
string targetDevInfo = target.GetDeviceInfo();
|
||||
var callback = new CallBack(Program.BeCalled);
|
||||
var pairs = templates.ToList();
|
||||
var batches = SplitIntoBatches(pairs, batchSize).ToList();
|
||||
int sent = 0;
|
||||
|
||||
for (int bi = 0; bi < batches.Count; bi++)
|
||||
{
|
||||
var swBatch = Stopwatch.StartNew();
|
||||
int attempted = 0;
|
||||
int success = 0;
|
||||
int failed = 0;
|
||||
|
||||
foreach (var kvp in batches[bi])
|
||||
{
|
||||
attempted++;
|
||||
string empId = kvp.Key;
|
||||
string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty<byte>());
|
||||
if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd))
|
||||
{
|
||||
failed++;
|
||||
failedTotal++;
|
||||
LogDistributionLine($"[TemplateDistribution] target={target.MachineIp} emp={empId} send=FAIL reason=BuildSetEmployee", logs);
|
||||
continue;
|
||||
}
|
||||
|
||||
string response = "";
|
||||
uint recvLen = 0;
|
||||
int rc = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref response, ref recvLen, callback);
|
||||
if (rc != 0 || (response != null && response.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
failed++;
|
||||
failedTotal++;
|
||||
LogDistributionLine($"[TemplateDistribution] target={target.MachineIp} emp={empId} send=FAIL rc={rc}", logs);
|
||||
continue;
|
||||
}
|
||||
|
||||
success++;
|
||||
sent++;
|
||||
|
||||
if (verifyAfterPush)
|
||||
{
|
||||
string verifyCmd = "GetEmployee(id=\"" + empId + "\")";
|
||||
string verifyResp = "";
|
||||
uint verifyRecvLen = 0;
|
||||
int vrc = Program.test(targetDevInfo, targetDevInfo.Length, verifyCmd, verifyCmd.Length, ref verifyResp, ref verifyRecvLen, callback);
|
||||
bool ok = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
LogDistributionLine(ok
|
||||
? $"[TemplateDistribution] target={target.MachineIp} emp={empId} verify=OK"
|
||||
: $"[TemplateDistribution] target={target.MachineIp} emp={empId} verify=FAIL rc={vrc}", logs);
|
||||
}
|
||||
}
|
||||
|
||||
if (delayMs > 0)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
}
|
||||
|
||||
swBatch.Stop();
|
||||
LogDistributionLine(
|
||||
$"[TemplateDistributionBatch] target_machine={target.MachineId} ip={target.MachineIp} batch={bi + 1}/{batches.Count} attempted={attempted} success={success} failed={failed} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}",
|
||||
logs);
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
private static int ChooseBatchSize(int total)
|
||||
{
|
||||
if (total <= 0) return 1;
|
||||
if (total <= 20) return 5;
|
||||
if (total <= 100) return 10;
|
||||
if (total <= 300) return 25;
|
||||
if (total <= 1000) return 50;
|
||||
return 100;
|
||||
}
|
||||
|
||||
private static IEnumerable<List<T>> SplitIntoBatches<T>(IReadOnlyList<T> items, int batchSize)
|
||||
{
|
||||
if (items == null || items.Count == 0) yield break;
|
||||
if (batchSize <= 0) batchSize = 1;
|
||||
for (int i = 0; i < items.Count; i += batchSize)
|
||||
{
|
||||
int count = Math.Min(batchSize, items.Count - i);
|
||||
var batch = new List<T>(count);
|
||||
for (int j = 0; j < count; j++)
|
||||
{
|
||||
batch.Add(items[i + j]);
|
||||
}
|
||||
yield return batch;
|
||||
}
|
||||
}
|
||||
|
||||
private void TransferDbToDevice(TemplateTransferJob job, List<string> employeeIds, List<(AttendanceMachine Machine, string Type)> targets, MySqlConnection connection, List<string> logs)
|
||||
{
|
||||
bool verifyAfterPush = GetBoolAppSetting("TEMPLATE_VERIFY_AFTER_PUSH", true);
|
||||
int delayMs = GetIntAppSetting("TEMPLATE_DEVICE_DELAY_MS", 0);
|
||||
|
||||
var effectiveEmployeeIds = employeeIds ?? new List<string>();
|
||||
|
||||
// Optional filtering by EmpIds configured on the job (for safe, small-scope tests)
|
||||
if (job.EmpIds != null && job.EmpIds.Count > 0)
|
||||
{
|
||||
var filter = new HashSet<string>(job.EmpIds, StringComparer.OrdinalIgnoreCase);
|
||||
effectiveEmployeeIds = effectiveEmployeeIds.Where(id => filter.Contains(id)).ToList();
|
||||
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} employees_filtered={effectiveEmployeeIds.Count}/{employeeIds.Count}");
|
||||
}
|
||||
|
||||
var templates = _templateDao.GetActiveTemplatesBySerialNos(connection, effectiveEmployeeIds);
|
||||
logs.Add($"[TemplateJob {job.Id}] DB templates fetched. count={templates.Count}");
|
||||
|
||||
// Log whether each requested employee has a template in DB
|
||||
foreach (var empId in effectiveEmployeeIds)
|
||||
{
|
||||
if (templates.ContainsKey(empId))
|
||||
{
|
||||
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} emp={empId} template=FOUND");
|
||||
}
|
||||
else
|
||||
{
|
||||
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} source={job.SourceIp} emp={empId} template=NOT_FOUND");
|
||||
}
|
||||
}
|
||||
|
||||
// Batch per target machine.
|
||||
var templatePairs = templates.ToList();
|
||||
int totalTemplates = templatePairs.Count;
|
||||
int batchSize = ChooseBatchSize(totalTemplates);
|
||||
var batches = SplitIntoBatches(templatePairs, batchSize).ToList();
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
string targetDevInfo = target.Machine.GetDeviceInfo();
|
||||
var callback = new CallBack(Program.BeCalled);
|
||||
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
int verifyOk = 0;
|
||||
int verifyFail = 0;
|
||||
|
||||
logs.Add($"[TemplateTransfer] job={job.Id} target_ip={target.Machine.MachineIp} total_templates={totalTemplates} batch_size={batchSize} total_batches={batches.Count}");
|
||||
|
||||
for (int bi = 0; bi < batches.Count; bi++)
|
||||
{
|
||||
var swBatch = Stopwatch.StartNew();
|
||||
int bAttempted = 0;
|
||||
int bOk = 0;
|
||||
int bFail = 0;
|
||||
int bVerifyOk = 0;
|
||||
int bVerifyFail = 0;
|
||||
|
||||
foreach (var kvp in batches[bi])
|
||||
{
|
||||
bAttempted++;
|
||||
string empId = kvp.Key;
|
||||
string templateStr = Encoding.UTF8.GetString(kvp.Value ?? Array.Empty<byte>());
|
||||
if (!TemplateParser.TryBuildSetEmployeeCommand(templateStr, out var setCmd))
|
||||
{
|
||||
fail++;
|
||||
bFail++;
|
||||
logs.Add($"[TemplateJob {job.Id}] Build SetEmployee failed. emp={empId} target={target.Machine.MachineIp}");
|
||||
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL reason=BuildSetEmployee");
|
||||
continue;
|
||||
}
|
||||
|
||||
string response = "";
|
||||
uint recvLen = 0;
|
||||
int rc = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref response, ref recvLen, callback);
|
||||
if (rc != 0 || (response != null && response.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
fail++;
|
||||
bFail++;
|
||||
logs.Add($"[TemplateJob {job.Id}] SetEmployee failed. emp={empId} target={target.Machine.MachineIp}");
|
||||
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=FAIL rc={rc}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ok++;
|
||||
bOk++;
|
||||
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} send=OK");
|
||||
|
||||
if (verifyAfterPush)
|
||||
{
|
||||
string verifyCmd = "GetEmployee(id=\"" + empId + "\")";
|
||||
string verifyResp = "";
|
||||
uint verifyRecvLen = 0;
|
||||
int vrc = Program.test(targetDevInfo, targetDevInfo.Length, verifyCmd, verifyCmd.Length, ref verifyResp, ref verifyRecvLen, callback);
|
||||
bool success = vrc == 0 && verifyResp != null && verifyResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
if (success)
|
||||
{
|
||||
verifyOk++;
|
||||
bVerifyOk++;
|
||||
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=OK");
|
||||
}
|
||||
else
|
||||
{
|
||||
verifyFail++;
|
||||
bVerifyFail++;
|
||||
Program.WriteInternalLog($"[DB_TO_DEVICE] job={job.Id} target={target.Machine.MachineIp} emp={empId} verify=FAIL rc={vrc}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (delayMs > 0)
|
||||
{
|
||||
System.Threading.Thread.Sleep(delayMs);
|
||||
}
|
||||
|
||||
swBatch.Stop();
|
||||
logs.Add($"[TemplateTransferBatch] job={job.Id} target_ip={target.Machine.MachineIp} batch={bi + 1}/{batches.Count} attempted={bAttempted} ok={bOk} fail={bFail} verify_ok={bVerifyOk} verify_fail={bVerifyFail} duration_sec={Math.Round(swBatch.Elapsed.TotalSeconds, 1)}");
|
||||
}
|
||||
|
||||
if (verifyAfterPush)
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] DB_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail} verify_ok={verifyOk} verify_fail={verifyFail}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logs.Add($"[TemplateJob {job.Id}] DB_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool GetBoolAppSetting(string key, bool defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var val = ConfigurationManager.AppSettings[key];
|
||||
if (string.IsNullOrWhiteSpace(val)) return defaultValue;
|
||||
return bool.TryParse(val.Trim(), out var parsed) ? parsed : defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetIntAppSetting(string key, int defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var val = ConfigurationManager.AppSettings[key];
|
||||
if (string.IsNullOrWhiteSpace(val)) return defaultValue;
|
||||
return int.TryParse(val.Trim(), out var parsed) ? parsed : defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private void TransferDeviceToDevice(string jobId, AttendanceMachine source, List<string> employeeIds, List<(AttendanceMachine Machine, string Type)> targets, List<string> logs)
|
||||
{
|
||||
string sourceDevInfo = source.GetDeviceInfo();
|
||||
var callback = new CallBack(Program.BeCalled);
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
string targetDevInfo = target.Machine.GetDeviceInfo();
|
||||
|
||||
foreach (var empId in employeeIds)
|
||||
{
|
||||
string getCmd = "GetEmployee(id=\"" + empId + "\")";
|
||||
string getResp = "";
|
||||
uint recvLen = 0;
|
||||
|
||||
int rc = Program.test(sourceDevInfo, sourceDevInfo.Length, getCmd, getCmd.Length, ref getResp, ref recvLen, callback);
|
||||
if (rc != 0 || getResp == null || getResp.IndexOf("success", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
fail++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TemplateParser.TryBuildSetEmployeeCommand(getResp, out var setCmd))
|
||||
{
|
||||
fail++;
|
||||
continue;
|
||||
}
|
||||
|
||||
string setResp = "";
|
||||
uint setRecvLen = 0;
|
||||
int rc2 = Program.test(targetDevInfo, targetDevInfo.Length, setCmd, setCmd.Length, ref setResp, ref setRecvLen, callback);
|
||||
if (rc2 != 0 || (setResp != null && setResp.IndexOf("fail", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
fail++;
|
||||
}
|
||||
else
|
||||
{
|
||||
ok++;
|
||||
}
|
||||
}
|
||||
|
||||
logs.Add($"[TemplateJob {jobId}] DEVICE_TO_DEVICE target={target.Machine.MachineIp} ok={ok} fail={fail}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteRawTemplateLog(string header, string raw)
|
||||
{
|
||||
try
|
||||
{
|
||||
string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\TemplateRawLogs\\TemplateRaw_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt";
|
||||
LogService.EnqueueLine(filepath, "----- " + DateTime.Now + " -----");
|
||||
LogService.EnqueueLine(filepath, header);
|
||||
LogService.EnqueueLine(filepath, raw ?? "");
|
||||
LogService.EnqueueLine(filepath, "");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore logging failures
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,392 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Log File Browser</title>
|
||||
<script src="resources/plugins/jquery.1.12.4.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-3.3.0/js/bootstrap.min.js"></script>
|
||||
<script src="resources/plugins/waves-0.7.5/waves.min.js"></script>
|
||||
<script src="resources/plugins/jquery.cookie.js"></script>
|
||||
<script src="resources/js/notify.js"></script>
|
||||
<script src="resources/js/common.js"></script>
|
||||
<script src="resources/js/lang.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e0e6ed;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #2c3e50;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
.btn.back {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
.btn.back:hover {
|
||||
background-color: #34495e;
|
||||
}
|
||||
#file-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-header {
|
||||
padding: 12px 20px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 100px;
|
||||
}
|
||||
.file-item {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 100px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.file-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
word-break: break-all;
|
||||
}
|
||||
.file-size {
|
||||
color: #7f8c8d;
|
||||
text-align: right;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
#file-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
display: none;
|
||||
}
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
#content-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: #2c3e50;
|
||||
flex-grow: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
#file-text {
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
.loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
.content-header {
|
||||
display: flex;
|
||||
gap: 16px; /* 控制按钮间距,按需调整 */
|
||||
}
|
||||
/* 新增网格布局样式 */
|
||||
#list-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr); /* 一行4列 */
|
||||
gap: 16px; /* 项目间距 */
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e6ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
transform: translateY(-2px);
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 1200px) {
|
||||
#list-container {
|
||||
grid-template-columns: repeat(3, 1fr); /* 中等屏幕显示3列 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#list-container {
|
||||
grid-template-columns: repeat(2, 1fr); /* 小屏幕显示2列 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#list-container {
|
||||
grid-template-columns: 1fr; /* 超小屏幕显示1列 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Log File Browser</h1>
|
||||
<button id="refresh-btn" class="btn">Refresh list</button>
|
||||
</header>
|
||||
|
||||
<div id="file-list">
|
||||
<div class="list-header">
|
||||
<span>file name</span>
|
||||
</div>
|
||||
<div id="list-container">
|
||||
<div class="loading">Loading file list...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="file-content">
|
||||
<div class="content-header">
|
||||
<h2 id="content-title"></h2>
|
||||
<button id="copy-btn" class="btn">Copy</button>
|
||||
<button id="back-btn" class="btn back">Return to list</button>
|
||||
</div>
|
||||
<pre id="file-text"></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
check_permission();
|
||||
const listContainer = document.getElementById('list-container');
|
||||
const fileContent = document.getElementById('file-content');
|
||||
const contentTitle = document.getElementById('content-title');
|
||||
const fileText = document.getElementById('file-text');
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
const backBtn = document.getElementById('back-btn');
|
||||
|
||||
// 获取日志文件列表(适配新版后端API)
|
||||
async function loglist_get() {
|
||||
const payload = {
|
||||
password: $.cookie("pwd"),
|
||||
cmd: 'getdir',
|
||||
path: '/data/log'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await $.ajax({
|
||||
url: debugserver + '/api',
|
||||
type: 'POST',
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
// 转换后端数据结构为前端需要的格式
|
||||
return {
|
||||
status: response.result ? 'success' : 'error',
|
||||
files: response.files.map(filename => ({ name: filename }))
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("API request failed:", error);
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Failed to obtain file list'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function renderFileList() {
|
||||
listContainer.innerHTML = '<div class="loading">Loading file list...</div>';
|
||||
const result = await loglist_get();
|
||||
|
||||
if (result.status === 'success' && result.files && result.files.length > 0) {
|
||||
// 按文件名中的时间戳倒序排序(新文件在前)
|
||||
result.files.sort((a, b) => {
|
||||
// 提取文件名中的时间戳(格式如:20250731-15-36-55.log)
|
||||
const timestampA = a.name.split('.')[0]; // 去掉后缀
|
||||
const timestampB = b.name.split('.')[0];
|
||||
// 按字符串倒序排列(新文件在前)
|
||||
return timestampB.localeCompare(timestampA);
|
||||
});
|
||||
|
||||
let listHTML = '';
|
||||
result.files.forEach(file => {
|
||||
listHTML += `
|
||||
<div class="file-item" data-filename="${file.name}">
|
||||
<div class="file-name">${file.name}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
listContainer.innerHTML = listHTML;
|
||||
|
||||
// 添加点击事件处理(保持不变)
|
||||
document.querySelectorAll('.file-item').forEach(item => {
|
||||
item.addEventListener('click', async () => {
|
||||
const filename = item.dataset.filename;
|
||||
await showFileContent(filename);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const errorMsg = result.message || 'Log file not found';
|
||||
listContainer.innerHTML = `<div class="empty-message">${errorMsg}</div>`;
|
||||
}
|
||||
}
|
||||
// 获取文件内容(连接真实API)
|
||||
async function fetchFileContent(filename) {
|
||||
const payload = {
|
||||
password: $.cookie("pwd"),
|
||||
cmd: 'getfile',
|
||||
path: '/data/log/'+filename
|
||||
};
|
||||
try {
|
||||
const response = await $.ajax({
|
||||
url: debugserver + '/api',
|
||||
type: 'POST',
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(payload)
|
||||
});
|
||||
return {
|
||||
status: response.result ? 'success' : 'error',
|
||||
file: response.base64
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("API request failed:", error);
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Failed to getfile'
|
||||
};
|
||||
}
|
||||
}
|
||||
async function showFileContent(filename) {
|
||||
fileContent.style.display = 'block';
|
||||
contentTitle.textContent = filename;
|
||||
fileText.textContent = 'loading...';
|
||||
const result=await fetchFileContent(filename);
|
||||
if (result.status === 'success')
|
||||
{
|
||||
const base64Data = result.file;
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const utf8Text = decoder.decode(bytes);
|
||||
fileText.textContent = utf8Text;
|
||||
}
|
||||
else
|
||||
fileText.textContent = `error: ${result.message || 'Unknown error'}`;
|
||||
fileContent.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 返回文件列表
|
||||
function backToList() {
|
||||
fileContent.style.display = 'none';
|
||||
document.querySelector('header').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// 事件监听
|
||||
refreshBtn.addEventListener('click', renderFileList);
|
||||
backBtn.addEventListener('click', backToList);
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', renderFileList);
|
||||
|
||||
// 在JS中添加以下代码
|
||||
// 获取复制按钮DOM
|
||||
const copyBtn = document.getElementById('copy-btn');
|
||||
|
||||
// 复制功能实现
|
||||
async function copyFileContent() {
|
||||
const content = fileText.innerText;
|
||||
|
||||
try {
|
||||
// 现代浏览器方案(Clipboard API)
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(content);
|
||||
notify('Copied to clipboard!');
|
||||
return;
|
||||
}
|
||||
|
||||
// 兼容旧浏览器的回退方案
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = content;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
// 选择并复制
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
notify('Copied!');
|
||||
} catch (err) {
|
||||
console.error('Copy failed:', err);
|
||||
notify('Copy failed! Please copy manually');
|
||||
}
|
||||
}
|
||||
// 事件绑定(在DOMContentLoaded或初始化时添加)
|
||||
copyBtn.addEventListener('click', copyFileContent);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,783 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>log view</title>
|
||||
|
||||
<link href="resources/plugins/bootstrap-3.3.0/css/bootstrap.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/material-design-iconic-font-2.2.0/css/material-design-iconic-font.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/bootstrap-table-1.17.0/bootstrap-table.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/waves-0.7.5/waves.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/jquery-confirm/jquery-confirm.min.css" rel="stylesheet"/>
|
||||
<link href="resources/css/common.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.img-max-height-lg {
|
||||
max-height: 240px !important;
|
||||
}
|
||||
.img-max-height-sm {
|
||||
max-height: 320px !important;
|
||||
}
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: inline-block;
|
||||
color: #000; /* 默认颜色为黑色 */
|
||||
transition: color 0.3s; /* 平滑过渡 */
|
||||
white-space: nowrap; /* 防止文字换行 */
|
||||
font-size: 14px; /* 可以根据需要调整字体大小 */
|
||||
z-index: 1; /* 确保文字在进度条之上 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<div id="toolbar">
|
||||
<a style="color: red;font-size: 1.2em;" class="waves-effect waves-button" href="javascript:;" onclick="clealogAction()"><i class="zmdi zmdi-delete"></i><span class="lang" key="UISTR_ALLLOG_DELETE">clean all logs</span></a>
|
||||
<a style="font-size: 1.2em;" id="savelogs" class="waves-effect waves-button" href="javascript:;" onclick="generateAndDownloadCsv()"><i class="zmdi zmdi-download"></i><span class="lang" key="UISTR_SAVE_LOGS">save logs as csv</span></a>
|
||||
<a style="font-size: 1.2em;" id="uploadlogs" class="waves-effect waves-button" href="javascript:;" onclick="uploadlogs()"><i class="zmdi zmdi-upload"></i><span class="lang" key="UISTR_UPLOAD_LOGS">upload log from CSV</span></a>
|
||||
<a style="font-size: 1.2em;" id="saveasafd" class="waves-effect waves-button" href="javascript:;" onclick="generateAndDownloadAFD()"><i class="zmdi zmdi-download"></i><span>Save logs as AFD</span></a>
|
||||
<!-- 添加说明 -->
|
||||
<a style="color: red;font-size: 1.2em; display: block; margin-top: 0px;" class="waves-effect waves-button"><i class="zmdi zmdi-info-outline"></i><span class="lang" key="UISTR_LOGS_NOTE">logs note</span></a>
|
||||
</div>
|
||||
<table id="table"></table>
|
||||
</div>
|
||||
|
||||
<!-- 选择时间栏 -->
|
||||
<div id="createDialog" class="crudDialog" hidden>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="inputuserid" class="lang" key="UISTR_ENROLL_ID">user id</label>
|
||||
<input id="inputuserid" class="form-control" type="text" oninput="validateEnrollid(this)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputstartdate" class="lang" key="UISTR_START">start</label>
|
||||
<input id="inputstartdate" type="date" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputenddate"class="lang" key="UISTR_END">end</label>
|
||||
<input id="inputenddate" type="date" class="form-control">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- 添加模态窗口 -->
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog" aria-labelledby="uploadModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="uploadModalLabel">Upload Logs</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><span id="uploaded" class="lang" key="UISTR_UPLOADED">Uploaded</span></p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
<span class="progress-text">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="lang btn btn-secondary" data-dismiss="modal" key="UISTR_CLOSE">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="resetDialog" class="crudDialog" hidden>
|
||||
<form>
|
||||
</form>
|
||||
</div>
|
||||
<script src="resources/plugins/jquery.1.12.4.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-3.3.0/js/bootstrap.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-table-1.17.0/bootstrap-table.min.js"></script>
|
||||
<script src="resources/plugins/waves-0.7.5/waves.min.js"></script>
|
||||
<script src="resources/plugins/jquery-confirm/jquery-confirm.min.js"></script>
|
||||
|
||||
<script src="resources/plugins/jquery.cookie.js"></script>
|
||||
<script src="resources/js/notify.js"></script>
|
||||
<script src="resources/js/lang.js"></script>
|
||||
<script src="resources/js/common.js"></script>
|
||||
<script src="resources/js/admin.js"></script>
|
||||
<script>
|
||||
let deviceid = $.cookie("deviceid") || "1";
|
||||
let DI_USERID_FORMAT = parseInt($.cookie("USERID_FORMAT")) || 0;
|
||||
let fromDate = null;
|
||||
let toDate = null;
|
||||
let enrollIdstr = null;
|
||||
let poithide = $.cookie("manufacturer") !== undefined && $.cookie("manufacturer")==="Piot Robotics";
|
||||
check_permission();
|
||||
lang_load().then(() => {
|
||||
getfromtodate();
|
||||
changetitle('time',lang.UISTR_TIME_SETTING);
|
||||
changetitle('id',lang.UISTR_ENROLL_ID);
|
||||
changetitle('name',lang.UISTR_UserName);
|
||||
changetitle('mode',lang.UISTR_VERIFY_mode);
|
||||
changetitle('event',lang.UISTR_EVENT);
|
||||
changetitle('note',lang.UISTR_NOTE||'note');
|
||||
changetitle('inout',lang.UISTR_INOUT||'inout');
|
||||
changetitle('photo',lang.UISTR_Photo||'Photo');
|
||||
changetitle('photouri',lang.UISTR_Photo_Url||'Photo');
|
||||
changeSearchPlaceholder();
|
||||
if (!$.cookie("CNPJ")) {
|
||||
const element = document.getElementById("saveasafd");
|
||||
if (element) {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
});
|
||||
function convertEnrollId(strid) {
|
||||
if (typeof strid === 'number') {
|
||||
return strid > 0xFFFFFFFF ? strid.toString() : strid;
|
||||
}
|
||||
if (typeof strid === 'string' && /^\d+$/.test(strid) && DI_USERID_FORMAT != 2) {
|
||||
const num = Number(strid);
|
||||
return Number.isSafeInteger(num) ? num : strid;
|
||||
}
|
||||
return strid;
|
||||
}
|
||||
function formatNoteContent(note) {
|
||||
if (note === null || note === undefined || note === "") return "";
|
||||
if (typeof note === 'object') {
|
||||
return Object.entries(note)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join(',');
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(note);
|
||||
return Object.entries(parsed)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join(',');
|
||||
} catch (e) {
|
||||
return note;
|
||||
}
|
||||
}
|
||||
return String(note);
|
||||
}
|
||||
function get_log(index = 0,fromDate, toDate, enrollIdstr) {
|
||||
let send = new Object();
|
||||
send["password"] = $.cookie("pwd");
|
||||
send["cmd"] = 'getlog';
|
||||
send["index"]=index;
|
||||
if (enrollIdstr?.trim()) {
|
||||
send["enrollid"] = convertEnrollId(enrollIdstr);
|
||||
}
|
||||
if (fromDate) {
|
||||
send["from"] = fromDate;
|
||||
}
|
||||
if (toDate) {
|
||||
send["to"] = toDate;
|
||||
}
|
||||
console.log(send);
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
},
|
||||
success: function(json) {
|
||||
console.log("done", json);
|
||||
let records = json.record;
|
||||
let transformedData = [];
|
||||
for (let record of records) {
|
||||
let transformedRecord = {
|
||||
id: record.enrollid,
|
||||
name: record.name || "",
|
||||
time: record.time || "",
|
||||
mode: record.mode || "",
|
||||
inout: record.inout || "",
|
||||
event: record.event || "",
|
||||
note: formatNoteContent(record.note),
|
||||
photo: record.photourl ? 1 : 0,
|
||||
photouri:record.photourl || ""
|
||||
};
|
||||
transformedData.push(transformedRecord);
|
||||
}
|
||||
$table.bootstrapTable('append', transformedData);
|
||||
if (json.count > json.to) {
|
||||
var nindex=json.to+1;
|
||||
get_log(nindex);
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.log("error::::", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
function getfromtodate() {
|
||||
$.confirm({
|
||||
type: 'blue',
|
||||
animationSpeed: 300,
|
||||
title: lang.UISTR_VIEWLOG,
|
||||
content: $('#createDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text:lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
fromDate = this.$content.find('#inputstartdate').val();
|
||||
toDate = this.$content.find('#inputenddate').val();
|
||||
enrollIdstr = this.$content.find('#inputuserid').val();
|
||||
get_log(0,fromDate, toDate, enrollIdstr);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
var $table = $('#table');
|
||||
$(function() {
|
||||
$(document).on('focus', 'input[type="text"]', function() {
|
||||
$(this).parent().find('label').addClass('active');
|
||||
}).on('blur', 'input[type="text"]', function() {
|
||||
if ($(this).val() == '') {
|
||||
$(this).parent().find('label').removeClass('active');
|
||||
}
|
||||
});
|
||||
// bootstrap table初始化
|
||||
// http://bootstrap-table.wenzhixin.net.cn/zh-cn/documentation/
|
||||
$table.bootstrapTable({
|
||||
data:[],
|
||||
height: getHeight(),
|
||||
striped: true,
|
||||
search: true,
|
||||
searchOnEnterKey: true,
|
||||
showRefresh: true,
|
||||
//showToggle: true,
|
||||
showColumns: true,
|
||||
minimumCountColumns: 2,
|
||||
//showPaginationSwitch: true,
|
||||
clickToSelect: true,
|
||||
detailView: true,
|
||||
detailFormatter: 'detailFormatter',
|
||||
pagination: true,
|
||||
pageSize: 15,
|
||||
pageList: [10, 15, 30, 50, 100],
|
||||
paginationLoop: false,
|
||||
classes: 'table table-hover table-no-bordered',
|
||||
//sidePagination: 'server',
|
||||
//silentSort: false,
|
||||
smartDisplay: false,
|
||||
idField: 'time',
|
||||
sortName: 'time',
|
||||
sortOrder: 'desc',
|
||||
escape: true,
|
||||
searchOnEnterKey: true,
|
||||
maintainSelected: true,
|
||||
toolbar: '#toolbar',
|
||||
columns: [
|
||||
{field: 'time', title: 'date time', sortable: true, halign: 'center', width: '15%',align: 'center'},
|
||||
{field: 'id', title: 'user id', sortable: true, halign: 'center', width: '15%',align: 'center'},
|
||||
{field: 'name', title: 'name', sortable: true, halign: 'center', width: '15%',align: 'center'},
|
||||
{field: 'mode', title: 'verify mode', sortable: true, halign: 'center', width: '10%',align: 'center'},
|
||||
{field: 'inout', title: 'inout', sortable: true, halign: 'center', width: '10%',align: 'center'},
|
||||
{field: 'event', title: 'event', sortable: true, halign: 'center', width: '10%',align: 'center'},
|
||||
{field: 'note', title: 'note', sortable: true, halign: 'center', width: '20%',align: 'center'},
|
||||
{field: 'photo', title: 'photo', sortable: true, halign: 'center', width: '10%',align: 'center', formatter: 'actionfaceFormatter'},
|
||||
{field: 'photouri', title: 'image', visible: false}
|
||||
]
|
||||
}).on('all.bs.table', function (e, name, args) {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-toggle="popover"]').popover();
|
||||
});
|
||||
});
|
||||
|
||||
$table.on('refresh.bs.table', function () {
|
||||
$table.bootstrapTable('removeAll');
|
||||
get_log(0,fromDate, toDate, enrollIdstr);
|
||||
});
|
||||
|
||||
function validateEnrollid(inputElement) {
|
||||
let maxValue, maxLength;
|
||||
switch(DI_USERID_FORMAT) {
|
||||
case 0:
|
||||
inputElement.value = inputElement.value.replace(/\D/g, '');
|
||||
maxValue = 99999999;
|
||||
maxLength = 8;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
inputElement.value = inputElement.value.replace(/[^0-9]/g, '');
|
||||
maxValue = BigInt('9223372036854775807');
|
||||
maxLength = 19;
|
||||
break;
|
||||
case 2:
|
||||
maxLength = 32;
|
||||
break;
|
||||
}
|
||||
if (inputElement.value.length > maxLength) {
|
||||
inputElement.value = inputElement.value.slice(0, maxLength);
|
||||
}
|
||||
if (DI_USERID_FORMAT !== 2) {
|
||||
const numericValue = DI_USERID_FORMAT === 1 ?
|
||||
BigInt(inputElement.value || 0) :
|
||||
parseInt(inputElement.value || 0, 10);
|
||||
|
||||
if (numericValue > maxValue) {
|
||||
inputElement.value = maxValue.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
function generateAndDownloadCsv() {
|
||||
const data = $table.bootstrapTable('getData');
|
||||
const header = "No\tTMNo\tEnNo\tName\tEvent\tMode\tDateTime\r\n";
|
||||
const csvContent = "data:text/csv;charset=utf-8,"
|
||||
+ header
|
||||
+ data.map((row, index) => `${index + 1}\t${deviceid}\t${row.id}\t${row.name}\t${row.inout}\t${row.mode}\t${row.time}`).join("\r\n");
|
||||
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
let paddedDeviceId = String(deviceid).padStart(3, '0');
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", `alog_${paddedDeviceId}.CSV`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function updateProgressBar(total, uploaded,isdownload=false) {
|
||||
const progressBar = $('.progress-bar');
|
||||
var percentage = (uploaded / total) * 100;
|
||||
progressBar.css('width', `${percentage}%`).attr('aria-valuenow', percentage);
|
||||
|
||||
if(isdownload)
|
||||
$('#uploaded').text(lang.UISTR_DOWNLOADED+': '+uploaded+' / '+total);
|
||||
else
|
||||
$('#uploaded').text(lang.UISTR_UPLOADED+': '+uploaded+' / '+total);
|
||||
|
||||
// 更新进度条中的文字
|
||||
const progressText = $('.progress-text');
|
||||
progressText.text(`${Math.round(percentage)}%`);
|
||||
|
||||
// 根据进度调整文字颜色
|
||||
if (percentage > 52) {
|
||||
progressText.css('color', 'white');
|
||||
} else {
|
||||
progressText.css('color', 'black');
|
||||
}
|
||||
}
|
||||
function uploadlogs() {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = ".TXT, .CSV";
|
||||
fileInput.onchange = function(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const csvContent = e.target.result;
|
||||
processDataFromCSV(csvContent);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
fileInput.click();
|
||||
}
|
||||
function processDataFromCSV(csvContent) {
|
||||
var allRows = [];
|
||||
var lines = csvContent.split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (i === 0) continue;
|
||||
var columns = lines[i].split('\t');
|
||||
if (columns.length >= 6) {
|
||||
var row = {
|
||||
id: convertEnrollId(columns[2]),
|
||||
event: Number(columns[4]),
|
||||
mode: Number(columns[5]),
|
||||
time: columns[6].replace(/\r/g, '')
|
||||
};
|
||||
allRows.push(row);
|
||||
}
|
||||
}
|
||||
const totalRows = allRows.length;
|
||||
let currentIndex = 0;
|
||||
|
||||
function batchProcessRecords(startIndex, endIndex) {
|
||||
const batch = allRows.slice(startIndex, endIndex);
|
||||
set_logs(batch).then(() => {
|
||||
currentIndex = endIndex;
|
||||
updateProgressBar(totalRows, currentIndex);
|
||||
if (currentIndex < totalRows) {
|
||||
// 计算下次发送的数据量
|
||||
const nextBatchSize = Math.min(500, Math.ceil(totalRows / 50));
|
||||
const nextEndIndex = Math.min(currentIndex + nextBatchSize, totalRows);
|
||||
batchProcessRecords(currentIndex, nextEndIndex);
|
||||
} else {
|
||||
$('#uploadModal').modal('hide');
|
||||
onUploadSuccess();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error setting user info:', error);
|
||||
alert('An error occurred while processing the records.');
|
||||
$('#uploadModal').modal('hide');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化模态框
|
||||
$('#uploadModalLabel').text(lang.UISTR_UPLOAD_LOGS);
|
||||
$('#uploadModal').modal('show');
|
||||
updateProgressBar(totalRows, currentIndex);
|
||||
|
||||
// 初始化处理
|
||||
const firstBatchSize = Math.min(500, Math.ceil(totalRows / 50));
|
||||
const firstEndIndex = Math.min(firstBatchSize, totalRows);
|
||||
batchProcessRecords(0, firstEndIndex);
|
||||
}
|
||||
function set_logs(recordsArray) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let send = new Object();
|
||||
send["password"] = $.cookie("pwd");
|
||||
send["cmd"] = 'setlogs';
|
||||
send["count"] = recordsArray.length;
|
||||
send["records"] = recordsArray;
|
||||
console.log(send);
|
||||
$.ajax({
|
||||
url: debugserver + '/api',
|
||||
type: 'POST',
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function () { },
|
||||
success: function (json) {
|
||||
if (json.ret === "setlogs" && json.result === true) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(json);
|
||||
}
|
||||
},
|
||||
error: function (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function changetitle(field, newTitle) {
|
||||
var columns = $table.bootstrapTable('getOptions').columns[0];
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
if (columns[i].field === field) {
|
||||
columns[i].title = newTitle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$table.bootstrapTable('refreshOptions', {columns: columns});
|
||||
}
|
||||
function changeSearchPlaceholder() {
|
||||
const options = $table.bootstrapTable('getOptions');
|
||||
options.formatSearch = () => lang.UISTR_USER_FIND || 'Search...';
|
||||
options.formatRefresh = () => lang.UISTR_Refresh || 'Refresh';
|
||||
options.formatRefreshButton = (icon, text) =>
|
||||
`<i class="${icon}"></i> ${text || lang.UISTR_Refresh || 'Refresh'}`;
|
||||
options.formatColumns = () => lang.UISTR_Columns || 'Columns'; // [4](@ref)
|
||||
options.formatColumnsButton = (icon, text) =>
|
||||
`<i class="${icon}"></i> ${text || lang.UISTR_Columns || 'Columns'}`;
|
||||
options.formatToggle = () => lang.UISTR_Toggle || 'Toggle'; // 悬浮提示(Tooltip)
|
||||
options.formatToggleButton = (icon, text) =>
|
||||
`<i class="${icon}"></i> ${text || lang.UISTR_Toggle || 'Toggle'}`; // 按钮文本
|
||||
options.formatNoMatches = () => lang.UISTR_NoMatches || 'No matching records found';
|
||||
options.formatShowingRows = (from, to, total) => {
|
||||
const template = lang.UISTR_ShowingRows
|
||||
|| 'Showing #1 to #2 of #3 rows';
|
||||
return template
|
||||
.replace(/#1/g, from)
|
||||
.replace(/#2/g, to)
|
||||
.replace(/#3/g, total);
|
||||
};
|
||||
options.formatRecordsPerPage = (pageSize) => {
|
||||
const template = lang.UISTR_RecordsPerPage
|
||||
|| '#3 rows per page';
|
||||
return template.replace(/#3/g, pageSize);
|
||||
};
|
||||
|
||||
// 强制刷新配置
|
||||
$table.bootstrapTable('refreshOptions', options);
|
||||
}
|
||||
function actionfaceFormatter(value, row, index) {
|
||||
return value ? `<i class="glyphicon glyphicon-picture"></i>` : '';
|
||||
}
|
||||
function detailFormatter(index, row) {
|
||||
var html = [];
|
||||
var detailsHtml = [];
|
||||
// Manually add the information you want to display
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_ENROLL_ID+':</b>' + row.id + '</p>');
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_UserName+':</b>' + row.name + '</p>');
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_TIME_SETTING+':</b>' + row.time + '</p>');
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_VERIFY_mode+':</b>' + row.mode + '</p>');
|
||||
if(!poithide)
|
||||
{
|
||||
detailsHtml.push('<p><b>inout:</b>' + row.inout + '</p>');
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_EVENT+':</b>' + row.event + '</p>');
|
||||
detailsHtml.push('<p><b>note:</b>' + row.note + '</p>');
|
||||
}
|
||||
var imageUrl = '';
|
||||
if(row.photouri)
|
||||
imageUrl = debugserver+row.photouri;
|
||||
|
||||
// Use Flexbox for responsive alignment
|
||||
html.push('<div class="row d-flex flex-column flex-sm-row">'); // Use flex-column for stacking on small screens, and flex-row for larger screens
|
||||
|
||||
html.push(' <div class="col-12 col-sm-8">'); // Adjusted width for the first column
|
||||
html.push(' ' + detailsHtml.join(''));
|
||||
html.push(' </div>');
|
||||
|
||||
if (imageUrl && imageUrl.trim()) {
|
||||
// Ensure the image is always on the left on small screens
|
||||
html.push(' <div class="col-12 col-sm-4 order-first order-sm-0">'); // Use order-first for small screens, and remove it for larger screens
|
||||
html.push(' <a href="' + imageUrl+'?t=' + new Date().getTime() + '" target="_blank">'); // Added link to open image in new tab
|
||||
html.push(' <img src="' + imageUrl+'?t=' + new Date().getTime() + '" alt="User Image" class="img-max-height-lg img-max-height-sm">');
|
||||
html.push(' </a>');
|
||||
html.push(' </div>');
|
||||
}
|
||||
|
||||
|
||||
|
||||
html.push('</div>');
|
||||
|
||||
return html.join('');
|
||||
}
|
||||
function clealogAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_ALLLOG_DELETE,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'cleanlog'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
$.alert(lang.UISTR_SUCCESSFUL);
|
||||
$table.bootstrapTable('removeAll');
|
||||
get_log(0,fromDate, toDate, enrollIdstr);
|
||||
onUploadSuccess();
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS);
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$(document).ready(function() {
|
||||
$('#uploadModal').on('hidden.bs.modal', function () {
|
||||
$table.bootstrapTable('removeAll');
|
||||
get_log(0,fromDate, toDate, enrollIdstr);
|
||||
});
|
||||
|
||||
if(poithide)
|
||||
{
|
||||
const hideColumns = ['inout', 'event', 'note', 'photo', 'photouri'];
|
||||
hideColumns.forEach(col => {
|
||||
$table.bootstrapTable('hideColumn', col);
|
||||
});
|
||||
}
|
||||
});
|
||||
// 严格兼容C++的CRC16算法
|
||||
function crc16_modbus(data) {
|
||||
let crc = 0xFFFF;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
crc ^= data.charCodeAt(i);
|
||||
for (let j = 0; j < 8; j++) {
|
||||
const lsb = crc & 0x0001;
|
||||
crc = (crc >> 1) & 0x7FFF;
|
||||
if (lsb) crc ^= 0xA001;
|
||||
}
|
||||
}
|
||||
return crc.toString(16).toUpperCase().padStart(4, '0');
|
||||
}
|
||||
|
||||
// 严格字段格式化(支持对齐方式)
|
||||
function formatField(value, length, { numeric = false, alignLeft = false } = {}) {
|
||||
const str = String(value);
|
||||
const padded = numeric ?
|
||||
str.padStart(length, '0') :
|
||||
(alignLeft ? str.padEnd(length, ' ') : str.padStart(length, ' '));
|
||||
return padded.slice(0, length);
|
||||
}
|
||||
|
||||
|
||||
// 设备序列号生成(严格遵循C++格式)
|
||||
function generateEquipmentSerial(serial) {
|
||||
return [
|
||||
'00049', // 厂商编号(5位)
|
||||
'00907', // 型号编号(5位)
|
||||
'0', // 版本号(1位)
|
||||
serial.padStart(6, '0') // 序列号(6位)
|
||||
].join('').padEnd(17, ' ');
|
||||
}
|
||||
function formatAFDDateTime(input) {
|
||||
// Step 1: 统一转换为标准时间字符串
|
||||
let timeStr;
|
||||
if (typeof input === 'number') {
|
||||
// 时间戳处理(兼容Date.now())
|
||||
const d = new Date(input);
|
||||
timeStr = [
|
||||
d.getFullYear(),
|
||||
(d.getMonth() + 1).toString().padStart(2, '0'),
|
||||
d.getDate().toString().padStart(2, '0')
|
||||
].join('-') + ' ' + [
|
||||
d.getHours().toString().padStart(2, '0'),
|
||||
d.getMinutes().toString().padStart(2, '0'),
|
||||
d.getSeconds().toString().padStart(2, '0')
|
||||
].join(':');
|
||||
} else if (typeof input === 'string') {
|
||||
// 字符串直接使用
|
||||
timeStr = input;
|
||||
} else {
|
||||
throw new Error('输入类型错误,应为时间戳或YYYY-MM-DD HH:mm:ss字符串');
|
||||
}
|
||||
|
||||
// Step 2: 格式校验(更严格的正则表达式)
|
||||
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(timeStr)) {
|
||||
throw new Error(`时间格式错误,应为YYYY-MM-DD HH:mm:ss,当前输入:${timeStr}`);
|
||||
}
|
||||
|
||||
// Step 3: 构建AFD格式
|
||||
const [datePart, timePart] = timeStr.split(' ');
|
||||
const [year, month, day] = datePart.split('-');
|
||||
const [hour, minute, second] = timePart.split(':');
|
||||
|
||||
// 生成带分隔符的日期和时间[6,7](@ref)
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
const formattedTime = `${hour}:${minute}:${second}`;
|
||||
|
||||
// 拼接并填充至24位[9,10](@ref)
|
||||
return `${formattedDate}T${formattedTime}-0300`.padEnd(24, ' ');
|
||||
}
|
||||
function buildTrailer(counts) {
|
||||
return [
|
||||
'999999999', // 固定头
|
||||
formatField(counts[2] || 0, 9, { numeric: true }), // 类型2计数
|
||||
formatField(counts[3] || 0, 9, { numeric: true }), // 类型3计数
|
||||
formatField(0, 9, { numeric: true }), // 类型4
|
||||
formatField(0, 9, { numeric: true }), // 类型5
|
||||
formatField(0, 9, { numeric: true }), // 类型6
|
||||
formatField(0, 9, { numeric: true }), // 类型7
|
||||
'9' // 结束标识
|
||||
].join('');
|
||||
}
|
||||
function generateAndDownloadAFD() {
|
||||
const data = $table.bootstrapTable('getData');
|
||||
const config = {
|
||||
serialNumber: $.cookie("devicesn") || "000001",
|
||||
cnpj: $.cookie("CNPJ")||"",
|
||||
companyName: $.cookie("companyname") || "",
|
||||
manufacturerCNPJ: "44880091000172"
|
||||
};
|
||||
// 构建文件名(严格长度控制)
|
||||
const fileName = `AFD${
|
||||
formatField(config.serialNumber,17)
|
||||
}${
|
||||
formatField(config.cnpj, 14, {numeric: true})
|
||||
}REP.txt`;
|
||||
|
||||
// 生成Header记录(字段位置严格对齐)
|
||||
const headerFields = [
|
||||
'000000000', // 001-009
|
||||
'1', // 010
|
||||
'1', // 011
|
||||
formatField(config.cnpj, 14, {numeric: true}), // 012-025
|
||||
formatField(' ', 14), // 026-039
|
||||
formatField(config.companyName, 150, {alignLeft: true}), //040-189
|
||||
formatField(config.serialNumber, 17),
|
||||
formatField(data[data.length-1].time.split('T')[0], 10), //217-226
|
||||
formatField(data[0].time.split('T')[0], 10), //207-216
|
||||
formatAFDDateTime(Date.now()), //227-250
|
||||
'003', //251-253
|
||||
'1', //254
|
||||
config.manufacturerCNPJ, //255-268
|
||||
formatField('E8', 30, {alignLeft: true}) //269-298
|
||||
].join('');
|
||||
|
||||
const headerLine = headerFields +crc16_modbus(headerFields);
|
||||
|
||||
// 处理数据记录(严格类型判断)
|
||||
let counts = {2:0, 3:0};
|
||||
const records = data
|
||||
.slice() // 创建副本避免修改原数组
|
||||
.sort((a, b) => new Date(a.time) - new Date(b.time)) // 按时间升序排序(最旧在前)
|
||||
.map((row, index) => {
|
||||
const isType2 = row.id === '999999999999' && row.mode ===255;
|
||||
let baseStr;
|
||||
|
||||
if (isType2) {
|
||||
// Type2记录:9位序号 + "2" + 24位时间 + 固定CPF + "1" + 备注
|
||||
baseStr = [
|
||||
formatField(index + 1, 9, {numeric: true}), // 9位序号
|
||||
'2',
|
||||
formatAFDDateTime(row.time).slice(0, 24), // 严格24位
|
||||
'26571383063 ', // 固定14位
|
||||
'1',
|
||||
formatField(row.note, 150, {alignLeft: true})// 左对齐备注
|
||||
].join('');
|
||||
counts[2]++;
|
||||
} else {
|
||||
// Type3记录:9位序号 + "3" + 24位时间 + 12位CPF
|
||||
baseStr = [
|
||||
formatField(index + 1, 9, {numeric: true}), // 9位序号
|
||||
'3',
|
||||
formatAFDDateTime(row.time).slice(0, 24), // 24位时间
|
||||
formatField(row.id, 11, {alignLeft: true})
|
||||
].join('');
|
||||
counts[3]++;
|
||||
}
|
||||
|
||||
// 计算CRC并拼接记录
|
||||
const crc = crc16_modbus(baseStr);
|
||||
return `${baseStr} ${crc}`;
|
||||
});
|
||||
const trailerLine = buildTrailer(counts);
|
||||
// 构建完整文件内容
|
||||
const fullContent = [headerLine, ...records, trailerLine].join('\r\n');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const encoded = encoder.encode(fullContent);
|
||||
|
||||
// 文件下载
|
||||
const blob = new Blob([encoded], {type: 'text/plain;charset=iso-8859-1'});
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
function onUploadSuccess() {
|
||||
const homeChannel = new BroadcastChannel('home_refresh_channel');
|
||||
homeChannel.postMessage({type: 'refresh_home'});
|
||||
homeChannel.close();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,957 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title class="lang" key="device_setting">edit shift</title>
|
||||
<link href="resources/plugins/bootstrap-3.3.0/css/bootstrap.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/material-design-iconic-font-2.2.0/css/material-design-iconic-font.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/bootstrap-table-1.17.0/bootstrap-table.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/waves-0.7.5/waves.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/jquery-confirm/jquery-confirm.min.css" rel="stylesheet"/>
|
||||
<link href="resources/css/common.css" rel="stylesheet"/>
|
||||
<link href="resources/css/diy.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.tab {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
/*height: 90vh;*/
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-container.active {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tab-buttons button {
|
||||
background-color: #f1f1f1;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 10px 20px;
|
||||
transition: background-color 0.3s ease;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px 5px 0 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.tab-buttons button.active {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
|
||||
.tab table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.tab th, .tab td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
#Schedule.tab table {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
/* sc */
|
||||
.xl655987
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:9.0pt;
|
||||
font-weight:700;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:.5pt solid black;
|
||||
border-right:.5pt solid black;
|
||||
border-bottom:.5pt solid black;
|
||||
border-left:.5pt solid windowtext;
|
||||
background:#7fcafa;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl665987
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:windowtext;
|
||||
font-size:8.0pt;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:1.0pt solid green;
|
||||
border-right:.5pt solid green;
|
||||
border-bottom:1.0pt solid green;
|
||||
border-left:.5pt solid green;
|
||||
background:red;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl675987
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:windowtext;
|
||||
font-size:8.0pt;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:"\@";
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:1.0pt solid green;
|
||||
border-right:.5pt solid green;
|
||||
border-bottom:1.0pt solid green;
|
||||
border-left:.5pt solid green;
|
||||
background:#c5e5f9;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl685987
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:windowtext;
|
||||
font-size:8.0pt;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:"\@";
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:1.0pt solid green;
|
||||
border-right:.5pt solid green;
|
||||
border-bottom:1.0pt solid green;
|
||||
border-left:.5pt solid green;
|
||||
background:#f7f9fb;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl695987
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:windowtext;
|
||||
font-size:8.0pt;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:1.0pt solid green;
|
||||
border-right:.5pt solid green;
|
||||
border-bottom:1.0pt solid green;
|
||||
border-left:.5pt solid green;
|
||||
background:#CCFFCC;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl705987
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:24.0pt;
|
||||
font-weight:700;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
background:#0d9bf7;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl715987
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:9.0pt;
|
||||
font-weight:700;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:left;
|
||||
vertical-align:middle;
|
||||
border-top:.5pt solid black;
|
||||
border-right:.5pt solid black;
|
||||
border-bottom:.5pt solid black;
|
||||
border-left:.5pt solid windowtext;
|
||||
background:#7fcafa;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
/* 考勤汇总表 */
|
||||
.xl1518308
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:11.0pt;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:Arial, sans-serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:general;
|
||||
vertical-align:bottom;
|
||||
background:#7fcafa;
|
||||
mso-pattern:auto;
|
||||
white-space:nowrap;}
|
||||
.xl6518308
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:9.0pt;
|
||||
font-weight:700;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:.5pt solid black;
|
||||
border-right:.5pt solid black;
|
||||
border-bottom:.5pt solid black;
|
||||
border-left:.5pt solid windowtext;
|
||||
background:#7fcafa;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl6618308
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:8.0pt;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:"\@";
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:1.0pt solid green;
|
||||
border-right:.5pt solid green;
|
||||
border-bottom:1.0pt solid green;
|
||||
border-left:.5pt solid green;
|
||||
background:#c5e5f9;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl6718308
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:8.0pt;
|
||||
font-weight:400;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:"\@";
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:1.0pt solid green;
|
||||
border-right:.5pt solid green;
|
||||
border-bottom:1.0pt solid green;
|
||||
border-left:.5pt solid green;
|
||||
background:#f7f9fb;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl6818308
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:24.0pt;
|
||||
font-weight:700;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
background:#6ec2f8;
|
||||
mso-pattern:black none;
|
||||
white-space:nowrap;}
|
||||
.xl6918308
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:6.0pt;
|
||||
font-weight:700;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:"\@";
|
||||
text-align:left;
|
||||
vertical-align:middle;
|
||||
background:#6ec2f8;
|
||||
mso-pattern:black none;
|
||||
white-space:normal;}
|
||||
.xl7018308
|
||||
{padding-top:1px;
|
||||
padding-right:1px;
|
||||
padding-left:1px;
|
||||
mso-ignore:padding;
|
||||
color:black;
|
||||
font-size:8.0pt;
|
||||
font-weight:700;
|
||||
font-style:normal;
|
||||
text-decoration:none;
|
||||
font-family:"Meiryo UI", serif;
|
||||
mso-font-charset:0;
|
||||
mso-number-format:General;
|
||||
text-align:center;
|
||||
vertical-align:middle;
|
||||
border-top:1.0pt solid green;
|
||||
border-right:.5pt solid green;
|
||||
border-bottom:none;
|
||||
border-left:.5pt solid green;
|
||||
background:#99CCFF;
|
||||
mso-pattern:black none;
|
||||
white-space:normal;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="createDialog" class="crudDialog" hidden>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="inputuserid" class="lang" key="UISTR_ENROLL_ID">user id</label>
|
||||
<input id="inputuserid" class="form-control" type="text" oninput="validateEnrollid(this)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputstartdate" class="lang" key="UISTR_START">start</label>
|
||||
<input id="inputstartdate" type="date" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputenddate"class="lang" key="UISTR_END">end</label>
|
||||
<input id="inputenddate" type="date" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div id="progress-bar" class="progress">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
|
||||
0%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-container">
|
||||
<div class="tab-buttons">
|
||||
<button class="lang" key="UISTR_Report_DETAILS">Schedule</button>
|
||||
<button class="lang" key="UISTR_MonthReport">Summary</button>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button class="optbtn lang" key="UISTR_PRINT" onclick="printActiveDiv()">Print</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="Schedule" class="tab">
|
||||
</div>
|
||||
<div id="Summary" class="tab">
|
||||
<table id="Summary_table" style='width:800pt'>
|
||||
<col width=47 span=11 style='width:35pt'>
|
||||
<col width=94 span=2 style='width:71pt'>
|
||||
<tr height=43 style='height:32.25pt'>
|
||||
<td colspan=13 class=xl6818308><span class="lang" key="UISTR_MonthReport">Summary</span></td>
|
||||
</tr>
|
||||
<tr height=24 style='height:18.2pt'>
|
||||
<td colspan=13 class=xl6918308></td>
|
||||
</tr>
|
||||
<tr height=20 style='height:15.0pt'>
|
||||
<td rowspan=2 class=xl6518308 ><span class="lang" key="UISTR_ENROLL_ID">ID</span></td>
|
||||
<td rowspan=2 class=xl6518308><span class="lang" key="UISTR_UserName">username</span></td>
|
||||
<td rowspan=2 class=xl6518308><span class="lang" key="UISTR_Department">dept.</span></td>
|
||||
<td colspan=2 class=xl6518308 ><span class="lang" key="UISTR_Work_hours">work hours</span></td>
|
||||
<td colspan=2 class=xl6518308 ><span class="lang" key="UISTR_Late_minutes">late minutes</span></td>
|
||||
<td colspan=2 class=xl6518308 ><span class="lang" key="UISTR_Leave_minutes">leave minutes</span></td>
|
||||
<td colspan=2 class=xl6518308 ><span class="lang" key="UISTR_OT_hours">ot hours</span></td>
|
||||
<td rowspan=2 class=xl6518308 ><span class="lang" key="UISTR_Work_Dates">work dates</span></td>
|
||||
<td rowspan=2 class=xl6518308 ><span class="lang" key="UISTR_Truancy">truancy</span></td>
|
||||
</tr>
|
||||
<tr height=20 style='height:15.0pt'>
|
||||
<td class=xl6518308 ><span class="lang" key="UISTR_ATTNormal">normal</span></td>
|
||||
<td class=xl6518308 ><span class="lang" key="UISTR_Actual">actual</span></td>
|
||||
<td class=xl6518308 ><span class="lang" key="UISTR_TIMES">times</span></td>
|
||||
<td class=xl6518308 ><span class="lang" key="UISTR_Minute">minute</span></td>
|
||||
<td class=xl6518308 ><span class="lang" key="UISTR_TIMES">times</span></td>
|
||||
<td class=xl6518308 ><span class="lang" key="UISTR_Minute">minute</span></td>
|
||||
<td class=xl6518308 ><span class="lang" key="UISTR_ATTNormal">normal</span></td>
|
||||
<td class=xl6518308 ><span class="lang" key="UISTR_Actual">actual</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<script src="resources/plugins/jquery.1.12.4.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-3.3.0/js/bootstrap.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-table-1.17.0/bootstrap-table.min.js"></script>
|
||||
<script src="resources/plugins/waves-0.7.5/waves.min.js"></script>
|
||||
<script src="resources/plugins/jquery-confirm/jquery-confirm.min.js"></script>
|
||||
<script src="resources/plugins/jquery.cookie.js"></script>
|
||||
<link href="resources/plugins/jquery-toastmessage/resources/css/jquery.toastmessage.css" rel="stylesheet"/>
|
||||
<script src="resources/plugins/jquery-toastmessage/javascript/jquery.toastmessage.js"></script>
|
||||
<script src="resources/js/notify.js"></script>
|
||||
<script src="resources/js/lang.js"></script>
|
||||
<script src="resources/js/common.js"></script>
|
||||
<script src="resources/js/xlsx.full.min.js"></script>
|
||||
<script>
|
||||
let DI_USERID_FORMAT = parseInt($.cookie("USERID_FORMAT")) || 0;
|
||||
const tabButtons = document.querySelectorAll('.tab-buttons button');
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
check_permission();
|
||||
lang_load().then(() => {
|
||||
getfromtodate();
|
||||
}).catch(error => {
|
||||
});
|
||||
function convertEnrollId(strid) {
|
||||
if (typeof strid === 'number') {
|
||||
return strid > 0xFFFFFFFF ? strid.toString() : strid;
|
||||
}
|
||||
if (typeof strid === 'string' && /^\d+$/.test(strid) && DI_USERID_FORMAT != 2) {
|
||||
const num = Number(strid);
|
||||
return Number.isSafeInteger(num) ? num : strid;
|
||||
}
|
||||
return strid;
|
||||
}
|
||||
function set_report_input(data) {
|
||||
// 创建表格元素
|
||||
let table = document.createElement('table');
|
||||
table.style.width = '800pt';
|
||||
|
||||
// 添加表格列宽
|
||||
let colWidths = [47, 38, 47, 47, 47, 47, 42, 42, 66, 66, 66, 72];
|
||||
for (let i = 0; i < colWidths.length; i++) {
|
||||
let col = document.createElement('col');
|
||||
col.style.width = colWidths[i] + 'pt';
|
||||
table.appendChild(col);
|
||||
}
|
||||
|
||||
// 添加表头
|
||||
let thead = document.createElement('thead');
|
||||
let tr = document.createElement('tr');
|
||||
tr.height = 40;
|
||||
let th = document.createElement('th');
|
||||
th.colSpan = 13;
|
||||
th.height = 40;
|
||||
th.className = 'xl705987';
|
||||
th.textContent = lang.UISTR_Report_DETAILS;
|
||||
tr.appendChild(th);
|
||||
thead.appendChild(tr);
|
||||
|
||||
tr = document.createElement('tr');
|
||||
tr.height = 20;
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 3;
|
||||
th.height = 20;
|
||||
th.className = 'xl715987';
|
||||
th.textContent = lang.UISTR_ENROLL_ID+':'+data.enrollid;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 10;
|
||||
th.className = 'xl715987';
|
||||
th.textContent =lang.UISTR_UserName+':'+data.name;
|
||||
tr.appendChild(th);
|
||||
thead.appendChild(tr);
|
||||
|
||||
tr = document.createElement('tr');
|
||||
tr.height = 20;
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 3;
|
||||
th.height = 20;
|
||||
th.className = 'xl655987';
|
||||
th.textContent =lang.UISTR_Department+':'+data.department;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 6;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_UserShift+':'+data.shift;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 4;
|
||||
th.className = 'xl655987';
|
||||
th.textContent =lang.UISTR_Holidaysdate+':'+data.date;
|
||||
tr.appendChild(th);
|
||||
thead.appendChild(tr);
|
||||
|
||||
tr = document.createElement('tr');
|
||||
tr.height = 20;
|
||||
th = document.createElement('th');
|
||||
th.rowSpan = 2;
|
||||
th.height = 40;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_Holidaysdate;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.rowSpan = 2;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_WEEK;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 2;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_Section+'1';
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 2;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_Section+'2';
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 2;
|
||||
th.className = 'xl655987';
|
||||
th.textContent =lang.UISTR_Section+'3';
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.colSpan = 2;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_Work_hours;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.rowSpan = 2;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_OT_hours;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.rowSpan = 2;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_Late_minutes;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.rowSpan = 2;
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_Leave_minutes;
|
||||
tr.appendChild(th);
|
||||
thead.appendChild(tr);
|
||||
|
||||
tr = document.createElement('tr');
|
||||
tr.height = 20;
|
||||
th = document.createElement('th');
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_ON_DUTY;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_OFF_DUTY;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_ON_DUTY;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_OFF_DUTY;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_ON_DUTY;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_OFF_DUTY;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_ATTNormal;
|
||||
tr.appendChild(th);
|
||||
th = document.createElement('th');
|
||||
th.className = 'xl655987';
|
||||
th.textContent = lang.UISTR_Actual;
|
||||
tr.appendChild(th);
|
||||
thead.appendChild(tr);
|
||||
|
||||
table.appendChild(thead);
|
||||
|
||||
// 添加表格数据
|
||||
let tbody = document.createElement('tbody');
|
||||
for (let i = 0; i < data.days.length; i++) {
|
||||
let day = data.days[i];
|
||||
let tr = document.createElement('tr');
|
||||
tr.height = 20;
|
||||
|
||||
var vclassname=i%2==0?'xl685987':'xl675987'
|
||||
|
||||
let td = document.createElement('td');
|
||||
td.height = 20;
|
||||
td.className = vclassname;
|
||||
td.textContent = day.day;
|
||||
tr.appendChild(td);
|
||||
|
||||
td = document.createElement('td');
|
||||
td.className = vclassname;
|
||||
td.textContent = day.week;
|
||||
tr.appendChild(td);
|
||||
|
||||
for (let j = 0; j < day.time.length; j++) {
|
||||
td = document.createElement('td');
|
||||
var vcolor=vclassname;
|
||||
if(day.time[j].length !== 0){
|
||||
if (day.flag[j] == 0 ){
|
||||
vcolor='xl665987';
|
||||
}
|
||||
else if(day.flag[j]==2){
|
||||
vcolor='xl695987';
|
||||
}
|
||||
}
|
||||
td.className = vcolor;
|
||||
td.textContent = day.time[j];
|
||||
tr.appendChild(td);
|
||||
}
|
||||
|
||||
td = document.createElement('td');
|
||||
td.className = vclassname;
|
||||
td.textContent = day.fatten;
|
||||
tr.appendChild(td);
|
||||
|
||||
td = document.createElement('td');
|
||||
td.className = vclassname;
|
||||
td.textContent = day.fact_atten;
|
||||
tr.appendChild(td);
|
||||
|
||||
td = document.createElement('td');
|
||||
td.className = vclassname;
|
||||
td.textContent = day.dayot;
|
||||
tr.appendChild(td);
|
||||
|
||||
td = document.createElement('td');
|
||||
td.className = vclassname;
|
||||
td.textContent = day.daylate;
|
||||
tr.appendChild(td);
|
||||
|
||||
td = document.createElement('td');
|
||||
td.className = vclassname;
|
||||
td.textContent = day.dayLeaveearly;
|
||||
tr.appendChild(td);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
// 添加总计行
|
||||
tr = document.createElement('tr');
|
||||
tr.height = 20;
|
||||
td = document.createElement('td');
|
||||
td.colSpan = 8;
|
||||
td.height = 20;
|
||||
td.className = 'xl675987';
|
||||
td.textContent = lang.UISTR_Summary+':';
|
||||
tr.appendChild(td);
|
||||
td = document.createElement('td');
|
||||
td.className = 'xl675987';
|
||||
td.textContent = data.work_hour;
|
||||
tr.appendChild(td);
|
||||
td = document.createElement('td');
|
||||
td.className = 'xl675987';
|
||||
td.textContent = data.atten_hour;
|
||||
tr.appendChild(td);
|
||||
td = document.createElement('td');
|
||||
td.className = 'xl675987';
|
||||
td.textContent = data.ot_hour;
|
||||
tr.appendChild(td);
|
||||
td = document.createElement('td');
|
||||
td.className = 'xl675987';
|
||||
td.textContent = data.late_minute;
|
||||
tr.appendChild(td);
|
||||
td = document.createElement('td');
|
||||
td.className = 'xl675987';
|
||||
td.textContent = data.leave_minute;
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
|
||||
table.appendChild(tbody);
|
||||
|
||||
// 将表格添加到 DOM
|
||||
let reportDiv = document.getElementById('Schedule');
|
||||
reportDiv.appendChild(table);
|
||||
}
|
||||
let isDateUpdated = false;
|
||||
function validateEnrollid(inputElement) {
|
||||
let maxValue, maxLength;
|
||||
switch(DI_USERID_FORMAT) {
|
||||
case 0:
|
||||
inputElement.value = inputElement.value.replace(/\D/g, '');
|
||||
maxValue = 99999999;
|
||||
maxLength = 8;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
inputElement.value = inputElement.value.replace(/[^0-9]/g, '');
|
||||
maxValue = BigInt('9223372036854775807');
|
||||
maxLength = 19;
|
||||
break;
|
||||
case 2:
|
||||
maxLength = 32;
|
||||
break;
|
||||
}
|
||||
if (inputElement.value.length > maxLength) {
|
||||
inputElement.value = inputElement.value.slice(0, maxLength);
|
||||
}
|
||||
if (DI_USERID_FORMAT !== 2) {
|
||||
const numericValue = DI_USERID_FORMAT === 1 ?
|
||||
BigInt(inputElement.value || 0) :
|
||||
parseInt(inputElement.value || 0, 10);
|
||||
|
||||
if (numericValue > maxValue) {
|
||||
inputElement.value = maxValue.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
function set_summary_input(data) {
|
||||
const summarytable = document.getElementById('Summary_table');
|
||||
const tableBody = summarytable.querySelector('tbody');
|
||||
const rows = tableBody.rows;
|
||||
const rowindex = rows.length;
|
||||
const row = document.createElement('tr');
|
||||
row.setAttribute('height', '15.0px');
|
||||
const cssClass = rowindex % 2 === 0 ? 'xl6718308' : 'xl6618308';
|
||||
row.innerHTML = `
|
||||
<td class='${cssClass}' >${data.enrollid}</td>
|
||||
<td class='${cssClass}' >${data.name}</td>
|
||||
<td class='${cssClass}' >${data.department}</td>
|
||||
<td class='${cssClass}' >${data.work_hour}</td>
|
||||
<td class='${cssClass}' >${data.atten_hour}</td>
|
||||
<td class='${cssClass}' >${data.late_times}</td>
|
||||
<td class='${cssClass}' >${data.late_minute}</td>
|
||||
<td class='${cssClass}' >${data.leave_times}</td>
|
||||
<td class='${cssClass}' >${data.leave_minute}</td>
|
||||
<td class='${cssClass}' >${data.std_ot_hour}</td>
|
||||
<td class='${cssClass}' >${data.ot_hour}</td>
|
||||
<td class='${cssClass}' >${data.work_days}</td>
|
||||
<td class='${cssClass}' >${data.absent_days}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
// 只在第一次调用时更新日期部分
|
||||
if (!isDateUpdated) {
|
||||
const dateCell = summarytable.querySelector('tr:nth-child(2) td');
|
||||
dateCell.textContent =lang.UISTR_Holidaysdate+':'+data.date;
|
||||
isDateUpdated = true;
|
||||
}
|
||||
}
|
||||
function get_userids(fromDate, toDate) {
|
||||
let send = new Object();
|
||||
send["password"] = $.cookie("pwd");
|
||||
send["cmd"] = 'getuserids';
|
||||
$.ajax({
|
||||
url: debugserver + '/api',
|
||||
type: 'POST',
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
// 显示进度条
|
||||
$('#progress-bar .progress-bar').css('width', '0%').attr('aria-valuenow', 0);
|
||||
},
|
||||
success: function(json) {
|
||||
console.log("done", json);
|
||||
let records = json.record;
|
||||
let total = records.length;
|
||||
let completed = 0;
|
||||
for (let i = 0; i < records.length; i++){
|
||||
let record = records[i];
|
||||
report_get(fromDate, toDate, record, function() {
|
||||
completed++;
|
||||
let progress = (completed / total) * 100;
|
||||
$('#progress-bar .progress-bar').css('width', progress + '%').attr('aria-valuenow', progress).text(Math.round(progress) + '%');
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.log("error::::", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
function report_get(fromDate, toDate, enrollIdstr) {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd");
|
||||
send["cmd"] = 'getreport';
|
||||
if (enrollIdstr) {
|
||||
send["enrollid"] = convertEnrollId(enrollIdstr);
|
||||
}
|
||||
if (fromDate) {
|
||||
send["from"] = fromDate;
|
||||
}
|
||||
if (toDate) {
|
||||
send["to"] = toDate;
|
||||
}
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log("done", json);
|
||||
set_report_input(json);
|
||||
set_summary_input(json);
|
||||
const tabContainer = document.querySelector('.tab-container');
|
||||
tabContainer.classList.add('active');
|
||||
tabButtons[0].classList.add('active');
|
||||
tabs[0].classList.add('active');
|
||||
},
|
||||
error: function(error){
|
||||
console.log("error::::",error);
|
||||
}
|
||||
});
|
||||
}
|
||||
function getfromtodate() {
|
||||
$.confirm({
|
||||
type: 'blue',
|
||||
animationSpeed: 300,
|
||||
title: lang.UISTR_Report,
|
||||
content: $('#createDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let fromDate = this.$content.find('#inputstartdate').val();
|
||||
let toDate = this.$content.find('#inputenddate').val();
|
||||
let enrollIdstr = this.$content.find('#inputuserid').val();
|
||||
if (enrollIdstr === '') {
|
||||
get_userids(fromDate, toDate);
|
||||
} else {
|
||||
report_get(fromDate, toDate, enrollIdstr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tabButtons.forEach((button, index) => {
|
||||
button.addEventListener('click', () => {
|
||||
// Remove active class from all buttons and tabs
|
||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||
tabs.forEach(tab => tab.classList.remove('active'));
|
||||
|
||||
// Add active class to the clicked button and its corresponding tab
|
||||
button.classList.add('active');
|
||||
tabs[index].classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
function printActiveDiv() {
|
||||
// 获取当前激活的标签页
|
||||
const activeTabs = document.querySelectorAll('.tab.active');
|
||||
|
||||
// 检查是否有激活的标签页
|
||||
if (activeTabs.length > 0) {
|
||||
// 获取激活标签页的内容
|
||||
const activeTabContent = activeTabs[0].innerHTML;
|
||||
|
||||
// 获取当前激活的 button 文本
|
||||
const activeButton = document.querySelector('.tab-buttons .active');
|
||||
const activeButtonText = activeButton ? activeButton.textContent : 'Print Report';
|
||||
|
||||
// 创建新窗口用于打印
|
||||
const printWindow = window.open('', 'Print Window', 'width=800,height=600');
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>${activeButtonText}</title>
|
||||
<style>
|
||||
/* 添加必要的样式 */
|
||||
@media print {
|
||||
* {
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
}
|
||||
body {
|
||||
color: initial;
|
||||
background-color: initial;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
background-color: inherit; // 重置单元格背景色为继承
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
td {
|
||||
background-color: inherit; // 确保数据单元格的背景色正确打印
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${activeTabContent}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
// 打印窗口
|
||||
printWindow.document.close();
|
||||
printWindow.print();
|
||||
|
||||
// 关闭打印窗口
|
||||
printWindow.close();
|
||||
} else {
|
||||
console.log('没有找到激活的标签页。');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||
|
||||
//# sourceMappingURL=FileSaver.min.js.map
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
var click = device.mobile() ? 'touchstart' : 'click';
|
||||
$(function() {
|
||||
// 侧边栏操作按钮
|
||||
$(document).on(click, '#guide', function() {
|
||||
$(this).toggleClass('toggled');
|
||||
$('#sidebar').toggleClass('toggled');
|
||||
});
|
||||
// 侧边栏二级菜单
|
||||
$(document).on('click', '.sub-menu a', function() {
|
||||
$(this).next().slideToggle(200);
|
||||
$(this).parent().toggleClass('toggled');
|
||||
});
|
||||
// 个人资料
|
||||
$(document).on('click', '.s-profile a', function() {
|
||||
$(this).next().slideToggle(200);
|
||||
$(this).parent().toggleClass('toggled');
|
||||
});
|
||||
// Waves初始化
|
||||
Waves.displayEffect();
|
||||
// 滚动条初始化
|
||||
$('#sidebar').mCustomScrollbar({
|
||||
theme: 'minimal-dark',
|
||||
scrollInertia: 100,
|
||||
axis: 'yx',
|
||||
mouseWheel: {
|
||||
enable: true,
|
||||
axis: 'y',
|
||||
preventDefault: true
|
||||
}
|
||||
});
|
||||
// 切换系统
|
||||
$('.switch-systems').click(function () {
|
||||
var systemid = $(this).attr('systemid');
|
||||
var systemname = $(this).attr('systemname');
|
||||
var systemtitle = $(this).attr('systemtitle');
|
||||
$('.system_menus').hide(0, function () {
|
||||
$('.system_' + systemid).show();
|
||||
});
|
||||
$('body').attr("id", systemname);
|
||||
$('#system_title').text(systemtitle);
|
||||
$.cookie('zheng-upms-systemid', systemid);
|
||||
$.cookie('zheng-upms-systemname', systemname);
|
||||
$.cookie('zheng-upms-systemtitle', systemtitle);
|
||||
});
|
||||
// 显示cookie菜单
|
||||
var systemid = $.cookie('zheng-upms-systemid') || 1;
|
||||
var systemname = $.cookie('zheng-upms-systemname') || 'zheng-upms-server';
|
||||
var systemtitle = $.cookie('zheng-upms-systemtitle') || 'AI Biometric Device Management System';
|
||||
$('.system_menus').hide(0, function () {
|
||||
$('.system_' + systemid).show();
|
||||
});
|
||||
$('body').attr('id', systemname);
|
||||
$('#system_title').text(systemtitle);
|
||||
});
|
||||
// iframe高度自适应
|
||||
function changeFrameHeight(ifm) {
|
||||
ifm.height = document.documentElement.clientHeight - 118;
|
||||
}
|
||||
function resizeFrameHeight() {
|
||||
$('.tab_iframe').css('height', document.documentElement.clientHeight - 118);
|
||||
$('md-tab-content').css('left', '0');
|
||||
}
|
||||
window.onresize = function() {
|
||||
resizeFrameHeight();
|
||||
initScrollShow();
|
||||
initScrollState();
|
||||
}
|
||||
|
||||
// ========== 选项卡操作 ==========
|
||||
$(function() {
|
||||
// 选项卡点击
|
||||
$(document).on('click', '.content_tab li', function() {
|
||||
// 切换选项卡
|
||||
$('.content_tab li').removeClass('cur');
|
||||
$(this).addClass('cur');
|
||||
// 切换iframe
|
||||
$('.iframe').removeClass('cur');
|
||||
$('#iframe_' + $(this).data('index')).addClass('cur');
|
||||
var marginLeft = ($('#tabs').css('marginLeft').replace('px', ''));
|
||||
// 滚动到可视区域:在左侧
|
||||
if ($(this).position().left < marginLeft) {
|
||||
var left = $('.content_tab>ul').scrollLeft() + $(this).position().left - marginLeft;
|
||||
$('.content_tab>ul').animate({scrollLeft: left}, 200, function() {
|
||||
initScrollState();
|
||||
});
|
||||
}
|
||||
// 滚动到可视区域:在右侧
|
||||
if(($(this).position().left + $(this).width() - marginLeft) > document.getElementById('tabs').clientWidth) {
|
||||
var left = $('.content_tab>ul').scrollLeft() + (($(this).position().left + $(this).width() - marginLeft) - document.getElementById('tabs').clientWidth);
|
||||
$('.content_tab>ul').animate({scrollLeft: left}, 200, function() {
|
||||
initScrollState();
|
||||
});
|
||||
}
|
||||
});
|
||||
// 控制选项卡滚动位置
|
||||
$(document).on('click', '.tab_left>a', function() {
|
||||
$('.content_tab>ul').animate({scrollLeft: $('.content_tab>ul').scrollLeft() - 300}, 200, function() {
|
||||
initScrollState();
|
||||
});
|
||||
});
|
||||
// 向右箭头
|
||||
$(document).on('click', '.tab_right>a', function() {
|
||||
$('.content_tab>ul').animate({scrollLeft: $('.content_tab>ul').scrollLeft() + 300}, 200, function() {
|
||||
initScrollState();
|
||||
});
|
||||
});
|
||||
// 初始化箭头状态
|
||||
|
||||
// 选项卡右键菜单
|
||||
var menu = new BootstrapMenu('.tabs li', {
|
||||
fetchElementData: function(item) {
|
||||
return item;
|
||||
},
|
||||
actionsGroups: [
|
||||
['close', 'refresh'],
|
||||
['closeOther', 'closeAll'],
|
||||
['closeRight', 'closeLeft']
|
||||
],
|
||||
actions: {
|
||||
close: {
|
||||
name: lang.UISTR_CLOSE,
|
||||
iconClass: 'zmdi zmdi-close',
|
||||
onClick: function(item) {
|
||||
Tab.closeTab($(item));
|
||||
}
|
||||
},
|
||||
closeOther: {
|
||||
name: lang.UISTR_CLOSE_OTHERS,
|
||||
iconClass: 'zmdi zmdi-arrow-split',
|
||||
onClick: function(item) {
|
||||
var index = $(item).data('index');
|
||||
$('.content_tab li').each(function() {
|
||||
if ($(this).data('index') != index) {
|
||||
Tab.closeTab($(this));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
closeAll: {
|
||||
name: lang.UISTR_CLOSE_ALL,
|
||||
iconClass: 'zmdi zmdi-swap',
|
||||
onClick: function() {
|
||||
$('.content_tab li').each(function() {
|
||||
Tab.closeTab($(this));
|
||||
});
|
||||
}
|
||||
},
|
||||
closeRight: {
|
||||
name: lang.UISTR_CLOSE_RIGHT,
|
||||
iconClass: 'zmdi zmdi-arrow-right',
|
||||
onClick: function(item) {
|
||||
var index = $(item).data('index');
|
||||
$($('.content_tab li').toArray().reverse()).each(function() {
|
||||
if ($(this).data('index') != index) {
|
||||
Tab.closeTab($(this));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
closeLeft: {
|
||||
name: lang.UISTR_CLOSE_LEFT,
|
||||
iconClass: 'zmdi zmdi-arrow-left',
|
||||
onClick: function(item) {
|
||||
var index = $(item).data('index');
|
||||
$('.content_tab li').each(function() {
|
||||
if ($(this).data('index') != index) {
|
||||
Tab.closeTab($(this));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
refresh: {
|
||||
name: lang.UISTR_Refresh,
|
||||
iconClass: 'zmdi zmdi-refresh',
|
||||
onClick: function(item) {
|
||||
var index = $(item).data('index');
|
||||
var $iframe = $('#iframe_' + index).find('iframe');
|
||||
$iframe.attr('src', $iframe.attr('src'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// 选项卡对象
|
||||
var Tab = {
|
||||
addTab: function(title, url, key) {
|
||||
var index = url.replace(/\./g, '_').replace(/\//g, '_').replace(/:/g, '_').replace(/\?/g, '_').replace(/,/g, '_').replace(/=/g, '_').replace(/&/g, '_');
|
||||
// 如果存在选项卡,则激活,否则创建新选项卡
|
||||
if ($('#tab_' + index).length == 0) {
|
||||
// 添加选项卡
|
||||
$('.content_tab li').removeClass('cur');
|
||||
var tab = '<li id="tab_' + index +'" data-index="' + index + '" class="cur"><span class="waves-effect waves-light lang"' + 'key="' + key + '">' + title + '</span><i class="waves-effect waves-light zmdi zmdi-close" onclick="Tab.closeTab($(\'#tab_' + index +'\'));"></i></li>';
|
||||
// console.log(tab)
|
||||
$('.content_tab>ul').append(tab);
|
||||
// 添加iframe
|
||||
$('.iframe').removeClass('cur');
|
||||
var iframe = '<div id="iframe_' + index + '" class="iframe cur"><iframe class="tab_iframe" src="' + url + '" width="100%" frameborder="0" scrolling="auto" onload="changeFrameHeight(this)"></iframe></div>';
|
||||
$('.content_main').append(iframe);
|
||||
initScrollShow();
|
||||
$('.content_tab>ul').animate({scrollLeft: document.getElementById('tabs').scrollWidth - document.getElementById('tabs').clientWidth}, 200, function() {
|
||||
initScrollState();
|
||||
});
|
||||
} else {
|
||||
$('#tab_' + index).trigger('click');
|
||||
}
|
||||
// 关闭侧边栏
|
||||
$('#guide').trigger(click);
|
||||
},
|
||||
closeTab: function($item) {
|
||||
var closeable = $item.data('closeable');
|
||||
if (closeable != false) {
|
||||
// 如果当前时激活状态则关闭后激活左边选项卡
|
||||
if($item.hasClass('cur')) {
|
||||
$item.prev().trigger('click');
|
||||
}
|
||||
// 关闭当前选项卡
|
||||
var index = $item.data('index');
|
||||
$('#iframe_' + index).remove();
|
||||
$item.remove();
|
||||
}
|
||||
initScrollShow();
|
||||
}
|
||||
}
|
||||
function initScrollShow() {
|
||||
if (document.getElementById('tabs').scrollWidth > document.getElementById('tabs').clientWidth) {
|
||||
$('.content_tab').addClass('scroll');
|
||||
} else {
|
||||
$('.content_tab').removeClass('scroll');
|
||||
}
|
||||
}
|
||||
function initScrollState() {
|
||||
if ($('.content_tab>ul').scrollLeft() == 0) {
|
||||
$('.tab_left>a').removeClass('active');
|
||||
} else {
|
||||
$('.tab_left>a').addClass('active');
|
||||
}
|
||||
if (($('.content_tab>ul').scrollLeft() + document.getElementById('tabs').clientWidth) >= document.getElementById('tabs').scrollWidth) {
|
||||
$('.tab_right>a').removeClass('active');
|
||||
} else {
|
||||
$('.tab_right>a').addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
function fullPage() {
|
||||
|
||||
if ($.util.supportsFullScreen) {
|
||||
if ($.util.isFullScreen()) {
|
||||
$.util.cancelFullScreen();
|
||||
} else {
|
||||
$.util.requestFullScreen();
|
||||
}
|
||||
} else {
|
||||
alert("The current browser does not support the full-screen API. Please switch to the latest Chrome/Firefox/Safari browser or use the F11 shortcut key to operate.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
$(function() {
|
||||
// Waves初始化
|
||||
Waves.displayEffect();
|
||||
initMaterialInput();
|
||||
// 数据表格动态高度
|
||||
$(window).resize(function () {
|
||||
$('#table').bootstrapTable('resetView', {
|
||||
height: getHeight()
|
||||
});
|
||||
});
|
||||
// 设置input特效
|
||||
$(document).on('focus', 'input[type="text"],input[type="date"]', function() {
|
||||
$(this).parent().find('label').addClass('active');
|
||||
}).on('blur', 'input[type="text"], input[type="date"]', function() {
|
||||
if ($(this).val() == '') {
|
||||
$(this).parent().find('label').removeClass('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
// 动态高度
|
||||
function getHeight() {
|
||||
return $(window).height() - 20;
|
||||
}
|
||||
// 数据表格展开内容
|
||||
function detailFormatter(index, row) {
|
||||
var html = [];
|
||||
$.each(row, function (key, value) {
|
||||
html.push('<p><b>' + key + ':</b> ' + value + '</p>');
|
||||
});
|
||||
return html.join('');
|
||||
}
|
||||
// 初始化input特效
|
||||
function initMaterialInput() {
|
||||
$('form input[type="text"], form input[type="date"]').each(function () {
|
||||
if ($(this).val() != '') {
|
||||
$(this).parent().find('label').addClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,91 @@
|
|||
let current_lang = ''
|
||||
//let debugserver = 'http://192.168.0.22';
|
||||
let debugserver = '';
|
||||
|
||||
function lang_load() {
|
||||
return lang_change();
|
||||
}
|
||||
|
||||
function get_lang() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let send = { cmd: 'getlang' };
|
||||
console.log(send);
|
||||
|
||||
$.ajax({
|
||||
url: debugserver + '/api',
|
||||
type: 'POST',
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
success: function(json) {
|
||||
console.log("done", json);
|
||||
if (json.result) {
|
||||
resolve(json.lang);
|
||||
} else {
|
||||
reject(new Error('Request failed with result false.'));
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.log("error::::", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTitle() {
|
||||
return fetch('title.txt')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP error " + response.status);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(data => {
|
||||
return data.trim();
|
||||
})
|
||||
.catch(error => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
async function lang_change() {
|
||||
if (current_lang === '') {
|
||||
try {
|
||||
current_lang = await get_lang();
|
||||
} catch (error) {
|
||||
current_lang = 'en';
|
||||
console.error('Error getting language:', error);
|
||||
}
|
||||
}
|
||||
|
||||
let langJs = current_lang;
|
||||
$.cookie('lang', current_lang);
|
||||
console.log("language:", langJs);
|
||||
let title = await getTitle();
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: "resources/lang/" + langJs + ".js",
|
||||
dataType: "script",
|
||||
async: false,
|
||||
success: function() {
|
||||
window.lang = lang;
|
||||
if (title !== null&&title!='') {
|
||||
window.lang.UISTR_AI_BIO_ID_SYSTEM = title;
|
||||
}
|
||||
$('.lang').each(function(idx, ele) {
|
||||
let key = ele.getAttribute("key");
|
||||
let value = lang[key];
|
||||
if (typeof(value) !== "undefined") {
|
||||
$(this).text(value);
|
||||
}
|
||||
});
|
||||
console.log("reload to refresh lang");
|
||||
resolve();
|
||||
},
|
||||
error: function(error) {
|
||||
console.log("error lang", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
function notify(text, sticky) {
|
||||
$().toastmessage('showToast',{
|
||||
text: text,
|
||||
sticky : sticky || false,
|
||||
position : 'top-right',
|
||||
closeText: ''
|
||||
});
|
||||
}
|
||||
|
||||
function check_permission() {
|
||||
if (typeof ($.cookie("pwd")) == "undefined"){
|
||||
location.href = "index.html"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,2 @@
|
|||
/*! device.js 0.2.7 */
|
||||
(function(){var a,b,c,d,e,f,g,h,i,j;b=window.device,a={},window.device=a,d=window.document.documentElement,j=window.navigator.userAgent.toLowerCase(),a.ios=function(){return a.iphone()||a.ipod()||a.ipad()},a.iphone=function(){return!a.windows()&&e("iphone")},a.ipod=function(){return e("ipod")},a.ipad=function(){return e("ipad")},a.android=function(){return!a.windows()&&e("android")},a.androidPhone=function(){return a.android()&&e("mobile")},a.androidTablet=function(){return a.android()&&!e("mobile")},a.blackberry=function(){return e("blackberry")||e("bb10")||e("rim")},a.blackberryPhone=function(){return a.blackberry()&&!e("tablet")},a.blackberryTablet=function(){return a.blackberry()&&e("tablet")},a.windows=function(){return e("windows")},a.windowsPhone=function(){return a.windows()&&e("phone")},a.windowsTablet=function(){return a.windows()&&e("touch")&&!a.windowsPhone()},a.fxos=function(){return(e("(mobile;")||e("(tablet;"))&&e("; rv:")},a.fxosPhone=function(){return a.fxos()&&e("mobile")},a.fxosTablet=function(){return a.fxos()&&e("tablet")},a.meego=function(){return e("meego")},a.cordova=function(){return window.cordova&&"file:"===location.protocol},a.nodeWebkit=function(){return"object"==typeof window.process},a.mobile=function(){return a.androidPhone()||a.iphone()||a.ipod()||a.windowsPhone()||a.blackberryPhone()||a.fxosPhone()||a.meego()},a.tablet=function(){return a.ipad()||a.androidTablet()||a.blackberryTablet()||a.windowsTablet()||a.fxosTablet()},a.desktop=function(){return!a.tablet()&&!a.mobile()},a.television=function(){var a;for(television=["googletv","viera","smarttv","internet.tv","netcast","nettv","appletv","boxee","kylo","roku","dlnadoc","roku","pov_tv","hbbtv","ce-html"],a=0;a<television.length;){if(e(television[a]))return!0;a++}return!1},a.portrait=function(){return window.innerHeight/window.innerWidth>1},a.landscape=function(){return window.innerHeight/window.innerWidth<1},a.noConflict=function(){return window.device=b,this},e=function(a){return-1!==j.indexOf(a)},g=function(a){var b;return b=new RegExp(a,"i"),d.className.match(b)},c=function(a){var b=null;g(a)||(b=d.className.replace(/^\s+|\s+$/g,""),d.className=b+" "+a)},i=function(a){g(a)&&(d.className=d.className.replace(" "+a,""))},a.ios()?a.ipad()?c("ios ipad tablet"):a.iphone()?c("ios iphone mobile"):a.ipod()&&c("ios ipod mobile"):a.android()?c(a.androidTablet()?"android tablet":"android mobile"):a.blackberry()?c(a.blackberryTablet()?"blackberry tablet":"blackberry mobile"):a.windows()?c(a.windowsTablet()?"windows tablet":a.windowsPhone()?"windows mobile":"desktop"):a.fxos()?c(a.fxosTablet()?"fxos tablet":"fxos mobile"):a.meego()?c("meego mobile"):a.nodeWebkit()?c("node-webkit"):a.television()?c("television"):a.desktop()&&c("desktop"),a.cordova()&&c("cordova"),f=function(){a.landscape()?(i("portrait"),c("landscape")):(i("landscape"),c("portrait"))},h=Object.prototype.hasOwnProperty.call(window,"onorientationchange")?"orientationchange":"resize",window.addEventListener?window.addEventListener(h,f,!1):window.attachEvent?window.attachEvent(h,f):window[h]=f,f(),"function"==typeof define&&"object"==typeof define.amd&&define.amd?define(function(){return a}):"undefined"!=typeof module&&module.exports?module.exports=a:window.device=a}).call(this);
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>log view</title>
|
||||
|
||||
<link href="resources/plugins/bootstrap-3.3.0/css/bootstrap.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/material-design-iconic-font-2.2.0/css/material-design-iconic-font.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/bootstrap-table-1.17.0/bootstrap-table.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/waves-0.7.5/waves.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/jquery-confirm/jquery-confirm.min.css" rel="stylesheet"/>
|
||||
<link href="resources/css/common.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.img-max-height-lg {
|
||||
max-height: 240px !important;
|
||||
}
|
||||
.img-max-height-sm {
|
||||
max-height: 320px !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<div id="toolbar">
|
||||
</div>
|
||||
<table id="table"></table>
|
||||
</div>
|
||||
<script src="resources/plugins/jquery.1.12.4.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-3.3.0/js/bootstrap.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-table-1.17.0/bootstrap-table.min.js"></script>
|
||||
<script src="resources/plugins/waves-0.7.5/waves.min.js"></script>
|
||||
<script src="resources/plugins/jquery-confirm/jquery-confirm.min.js"></script>
|
||||
|
||||
<script src="resources/plugins/jquery.cookie.js"></script>
|
||||
<script src="resources/js/notify.js"></script>
|
||||
<script src="resources/js/lang.js"></script>
|
||||
<script src="resources/js/common.js"></script>
|
||||
<script src="resources/js/admin.js"></script>
|
||||
<script>
|
||||
let poithide = $.cookie("manufacturer") !== undefined && $.cookie("manufacturer")==="Piot Robotics";
|
||||
check_permission();
|
||||
lang_load().then(() => {
|
||||
fetchLogs();
|
||||
changetitle('time', lang.UISTR_TIME_SETTING);
|
||||
changetitle('id', lang.UISTR_ENROLL_ID);
|
||||
changetitle('name', lang.UISTR_UserName);
|
||||
changetitle('mode', lang.UISTR_VERIFY_mode);
|
||||
changetitle('event', lang.UISTR_EVENT);
|
||||
changetitle('note',lang.UISTR_NOTE||'note');
|
||||
changetitle('inout',lang.UISTR_INOUT||'inout');
|
||||
changetitle('photo',lang.UISTR_Photo||'Photo');
|
||||
changetitle('photouri',lang.UISTR_Photo_Url||'Photo');
|
||||
changeSearchPlaceholder();
|
||||
}).catch(error => {
|
||||
console.error("Language loading failed:", error);
|
||||
});
|
||||
function formatNoteContent(note) {
|
||||
if (note === null || note === undefined || note === "") return "";
|
||||
if (typeof note === 'object') {
|
||||
return Object.entries(note)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join(',');
|
||||
}
|
||||
if (typeof note === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(note);
|
||||
return Object.entries(parsed)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join(',');
|
||||
} catch (e) {
|
||||
return note;
|
||||
}
|
||||
}
|
||||
return String(note);
|
||||
}
|
||||
function get_rtlog(index = 0) {
|
||||
let send = new Object();
|
||||
send["password"] = $.cookie("pwd");
|
||||
send["cmd"] = 'getrtlog';
|
||||
send["index"] = index;
|
||||
console.log(send);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
url: debugserver + '/api',
|
||||
type: 'POST',
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
},
|
||||
success: function(json) {
|
||||
console.log("done", json);
|
||||
if(json.count&&json.count>0)
|
||||
{
|
||||
let records = json.record;
|
||||
let transformedData = [];
|
||||
for (let record of records) {
|
||||
let transformedRecord = {
|
||||
id: record.enrollid,
|
||||
name: record.name || "",
|
||||
time: record.time || "",
|
||||
mode: record.mode || "",
|
||||
inout: record.inout || "",
|
||||
event: record.event || "",
|
||||
note: formatNoteContent(record.note),
|
||||
photo: record.photourl ? 1 : 0,
|
||||
photouri: record.photourl || ""
|
||||
};
|
||||
transformedData.push(transformedRecord);
|
||||
}
|
||||
$table.bootstrapTable('append', transformedData);
|
||||
}
|
||||
if (json.count > json.to) {
|
||||
var nindex = json.to + 1;
|
||||
get_rtlog(nindex).then(resolve); // 继续处理下一个分包
|
||||
} else {
|
||||
resolve(); // 所有分包处理完毕
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
console.log("error::::", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
let isFetching = false;
|
||||
function fetchLogs() {
|
||||
if (!isFetching) {
|
||||
isFetching = true;
|
||||
get_rtlog().then(() => {
|
||||
isFetching = false;
|
||||
setTimeout(fetchLogs, 3000); // 每3秒更新一次
|
||||
}).catch(error => {
|
||||
console.error("Failed to fetch logs:", error);
|
||||
isFetching = false;
|
||||
setTimeout(fetchLogs, 3000); // 如果请求失败,3秒后重试
|
||||
});
|
||||
}
|
||||
}
|
||||
var $table = $('#table');
|
||||
$(function() {
|
||||
$(document).on('focus', 'input[type="text"]', function() {
|
||||
$(this).parent().find('label').addClass('active');
|
||||
}).on('blur', 'input[type="text"]', function() {
|
||||
if ($(this).val() == '') {
|
||||
$(this).parent().find('label').removeClass('active');
|
||||
}
|
||||
});
|
||||
// bootstrap table初始化
|
||||
// http://bootstrap-table.wenzhixin.net.cn/zh-cn/documentation/
|
||||
$table.bootstrapTable({
|
||||
data:[],
|
||||
height: getHeight(),
|
||||
striped: true,
|
||||
search: true,
|
||||
searchOnEnterKey: true,
|
||||
showRefresh: false,
|
||||
//showToggle: true,
|
||||
showColumns: true,
|
||||
minimumCountColumns: 2,
|
||||
//showPaginationSwitch: true,
|
||||
clickToSelect: true,
|
||||
detailView: true,
|
||||
detailFormatter: 'detailFormatter',
|
||||
pagination: true,
|
||||
pageSize: 15,
|
||||
pageList: [10, 15, 30, 50, 100],
|
||||
paginationLoop: false,
|
||||
classes: 'table table-hover table-no-bordered',
|
||||
//sidePagination: 'server',
|
||||
//silentSort: false,
|
||||
smartDisplay: false,
|
||||
idField: 'time',
|
||||
sortName: 'time',
|
||||
sortOrder: 'desc',
|
||||
escape: true,
|
||||
searchOnEnterKey: true,
|
||||
maintainSelected: true,
|
||||
toolbar: '#toolbar',
|
||||
columns: [
|
||||
{field: 'time', title: 'date time', sortable: true, halign: 'center', width: '15%',align: 'center'},
|
||||
{field: 'id', title: 'user id', sortable: true, halign: 'center', width: '15%',align: 'center'},
|
||||
{field: 'name', title: 'name', sortable: true, halign: 'center', width: '15%',align: 'center'},
|
||||
{field: 'mode', title: 'verify mode', sortable: true, halign: 'center', width: '10%',align: 'center'},
|
||||
{field: 'inout', title: 'inout', sortable: true, halign: 'center', width: '10%',align: 'center'},
|
||||
{field: 'event', title: 'event', sortable: true, halign: 'center', width: '10%',align: 'center'},
|
||||
{field: 'note', title: 'not', sortable: true, halign: 'center', width: '20%',align: 'center'},
|
||||
{field: 'photo', title: 'photo', sortable: true, halign: 'center', width: '10%',align: 'center', formatter: 'actionfaceFormatter'},
|
||||
{field: 'photouri', title: 'image', visible: false}
|
||||
]
|
||||
}).on('all.bs.table', function (e, name, args) {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-toggle="popover"]').popover();
|
||||
});
|
||||
});
|
||||
|
||||
$table.on('refresh.bs.table', function () {
|
||||
$table.bootstrapTable('removeAll');
|
||||
get_rtlog();
|
||||
});
|
||||
|
||||
function changetitle(field, newTitle) {
|
||||
var columns = $table.bootstrapTable('getOptions').columns[0];
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
if (columns[i].field === field) {
|
||||
columns[i].title = newTitle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$table.bootstrapTable('refreshOptions', {columns: columns});
|
||||
}
|
||||
function changeSearchPlaceholder() {
|
||||
const options = $table.bootstrapTable('getOptions');
|
||||
options.formatSearch = () => lang.UISTR_USER_FIND || 'Search...';
|
||||
options.formatRefresh = () => lang.UISTR_Refresh || 'Refresh';
|
||||
options.formatRefreshButton = (icon, text) =>
|
||||
`<i class="${icon}"></i> ${text || lang.UISTR_Refresh || 'Refresh'}`;
|
||||
options.formatColumns = () => lang.UISTR_Columns || 'Columns'; // [4](@ref)
|
||||
options.formatColumnsButton = (icon, text) =>
|
||||
`<i class="${icon}"></i> ${text || lang.UISTR_Columns || 'Columns'}`;
|
||||
options.formatToggle = () => lang.UISTR_Toggle || 'Toggle'; // 悬浮提示(Tooltip)
|
||||
options.formatToggleButton = (icon, text) =>
|
||||
`<i class="${icon}"></i> ${text || lang.UISTR_Toggle || 'Toggle'}`; // 按钮文本
|
||||
options.formatNoMatches = () => lang.UISTR_NoMatches || 'No matching records found';
|
||||
options.formatShowingRows = (from, to, total) => {
|
||||
const template = lang.UISTR_ShowingRows
|
||||
|| 'Showing #1 to #2 of #3 rows';
|
||||
return template
|
||||
.replace(/#1/g, from)
|
||||
.replace(/#2/g, to)
|
||||
.replace(/#3/g, total);
|
||||
};
|
||||
options.formatRecordsPerPage = (pageSize) => {
|
||||
const template = lang.UISTR_RecordsPerPage
|
||||
|| '#3 rows per page';
|
||||
return template.replace(/#3/g, pageSize);
|
||||
};
|
||||
|
||||
// 强制刷新配置
|
||||
$table.bootstrapTable('refreshOptions', options);
|
||||
}
|
||||
function actionfaceFormatter(value, row, index) {
|
||||
return value ? `<i class="glyphicon glyphicon-picture"></i>` : '';
|
||||
}
|
||||
function detailFormatter(index, row) {
|
||||
var html = [];
|
||||
var detailsHtml = [];
|
||||
// Manually add the information you want to display
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_ENROLL_ID+':</b>' + row.id + '</p>');
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_UserName+':</b>' + row.name + '</p>');
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_TIME_SETTING+':</b>' + row.time + '</p>');
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_VERIFY_mode+':</b>' + row.mode + '</p>');
|
||||
if(!poithide)
|
||||
{
|
||||
detailsHtml.push('<p><b>inout:</b>' + row.inout + '</p>');
|
||||
detailsHtml.push('<p><b>'+lang.UISTR_EVENT+':</b>' + row.event + '</p>');
|
||||
detailsHtml.push('<p><b>note:</b>' + row.note + '</p>');
|
||||
}
|
||||
|
||||
var imageUrl = '';
|
||||
if(row.photouri)
|
||||
imageUrl = debugserver+row.photouri;
|
||||
|
||||
// Use Flexbox for responsive alignment
|
||||
html.push('<div class="row d-flex flex-column flex-sm-row">'); // Use flex-column for stacking on small screens, and flex-row for larger screens
|
||||
|
||||
html.push(' <div class="col-12 col-sm-8">'); // Adjusted width for the first column
|
||||
html.push(' ' + detailsHtml.join(''));
|
||||
html.push(' </div>');
|
||||
|
||||
if (imageUrl && imageUrl.trim()) {
|
||||
// Ensure the image is always on the left on small screens
|
||||
html.push(' <div class="col-12 col-sm-4 order-first order-sm-0">'); // Use order-first for small screens, and remove it for larger screens
|
||||
html.push(' <a href="' + imageUrl+'?t=' + new Date().getTime() + '" target="_blank">'); // Added link to open image in new tab
|
||||
html.push(' <img src="' + imageUrl+'?t=' + new Date().getTime() + '" alt="User Image" class="img-max-height-lg img-max-height-sm">');
|
||||
html.push(' </a>');
|
||||
html.push(' </div>');
|
||||
}
|
||||
|
||||
|
||||
|
||||
html.push('</div>');
|
||||
|
||||
return html.join('');
|
||||
}
|
||||
$(document).ready(function() {
|
||||
if(poithide)
|
||||
{
|
||||
const hideColumns = ['inout', 'event', 'note', 'photo', 'photouri'];
|
||||
hideColumns.forEach(col => {
|
||||
$table.bootstrapTable('hideColumn', col);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,581 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title class="lang" key="device_setting">sys init</title>
|
||||
|
||||
<link href="resources/plugins/bootstrap-3.3.0/css/bootstrap.min.css" rel="stylesheet"/>
|
||||
<link href="resources/plugins/jquery-confirm/jquery-confirm.min.css" rel="stylesheet"/>
|
||||
|
||||
<script src="resources/plugins/jquery.1.12.4.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-3.3.0/js/bootstrap.min.js"></script>
|
||||
<script src="resources/plugins/bootstrap-table-1.17.0/bootstrap-table.min.js"></script>
|
||||
<script src="resources/plugins/waves-0.7.5/waves.min.js"></script>
|
||||
<script src="resources/plugins/jquery-confirm/jquery-confirm.min.js"></script>
|
||||
<script src="resources/plugins/jquery.cookie.js"></script>
|
||||
<link href="resources/plugins/jquery-toastmessage/resources/css/jquery.toastmessage.css" rel="stylesheet"/>
|
||||
<script src="resources/plugins/jquery-toastmessage/javascript/jquery.toastmessage.js"></script>
|
||||
<script src="resources/js/notify.js"></script>
|
||||
<script src="resources/js/common.js"></script>
|
||||
<script src="resources/js/lang.js"></script>
|
||||
<script src="resources/js/crypto-js.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.AdduserDialog {
|
||||
display: flex;
|
||||
margin-left:25%;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
max-width: 50%;
|
||||
width: 50%;
|
||||
padding: 2rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
.input-group {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.input-group, .btn {
|
||||
margin: 0 10px;
|
||||
}
|
||||
.button-spacing {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.AdduserDialog {
|
||||
display: flex;
|
||||
margin-left:25%;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
width: 700px;
|
||||
padding: 2rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="AdduserDialog d-flex flex-column align-items-center">
|
||||
<div class="panel text-center">
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="clean_all_user" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_ENROLL_DELETEALL"
|
||||
href="javascript:;" onclick="cleanuserAction()">cleanuser</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="clean_all_log" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_ALLLOG_DELETE"
|
||||
href="javascript:;" onclick="clealogAction()">clealog</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="clean_database" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_CLEAR_DATABASE"
|
||||
href="javascript:;" onclick="cleandatabaseAction()">cleandatabase</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="init_menu" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_InitSystem"
|
||||
href="javascript:;" onclick="initmenuAction()">initmenu</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="clean_admin" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_BenumbAllManager"
|
||||
href="javascript:;" onclick="cleaadminAction()">cleaadmin</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="clean_inactive_user" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_CLEAR_INACTIVE_USERS"
|
||||
href="javascript:;" onclick="cleaninactiveuserAction()">cleaninactiveuser</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="clean_all_snop" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_PHOTO_DELETE"
|
||||
href="javascript:;" onclick="cleanlogphotoAction()">cleanlogphoto</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="factory_default" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_QUESTION_SETDEFAULT"
|
||||
href="javascript:;" onclick="resetAction()">Reset to default setting</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 d-flex justify-content-center">
|
||||
<button id="factory_default" type="" class="btn btn-danger waves-effect waves-float lang button-spacing"
|
||||
key="UISTR_REBOOT"
|
||||
href="javascript:;" onclick="rebootAction()">Reboot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="resetDialog" class="crudDialog" hidden>
|
||||
<form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
check_permission();
|
||||
lang_load();
|
||||
function resetAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_QUESTION_SETDEFAULT,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'initsys'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
notify(lang.UISTR_SUCCESSFUL);
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS)
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function cleanuserAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_ENROLL_DELETEALL,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'cleanuser'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
notify(lang.UISTR_SUCCESSFUL);
|
||||
onUploadSuccess();
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS)
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function clealogAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_ALLLOG_DELETE,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'cleanlog'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
notify(lang.UISTR_SUCCESSFUL);
|
||||
onUploadSuccess();
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS)
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function cleaadminAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_BenumbAllManager,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'cleanadmin'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
notify(lang.UISTR_SUCCESSFUL);
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS)
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function cleaninactiveuserAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_CLEAR_INACTIVE_USERS,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'cleaninactiveuser'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
notify(lang.UISTR_SUCCESSFUL);
|
||||
onUploadSuccess();
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS)
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function cleanlogphotoAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_PHOTO_DELETE,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'cleanlogphoto'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
notify(lang.UISTR_SUCCESSFUL);
|
||||
onUploadSuccess();
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS)
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function initmenuAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_InitSystem,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'initmenu'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
notify(lang.UISTR_SUCCESSFUL);
|
||||
onUploadSuccess();
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS)
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function rebootAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_REBOOT,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = {
|
||||
password: $.cookie("pwd"),
|
||||
cmd: 'reboot'
|
||||
};
|
||||
console.log(send);
|
||||
|
||||
// 发送请求到服务器
|
||||
$.ajax({
|
||||
url: debugserver + '/api',
|
||||
type: 'POST',
|
||||
async: true, // 使用默认的异步请求
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function () {
|
||||
},
|
||||
always: function () {
|
||||
notify(lang.UISTR_SUCCESSFUL);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function cleandatabaseAction() {
|
||||
$.confirm({
|
||||
type: 'dark',
|
||||
animationSpeed: 300,
|
||||
title: window.lang.UISTR_CLEAR_DATABASE,
|
||||
content: $('#resetDialog').html(),
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: lang.UISTR_YES_OK,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
let send = new Object()
|
||||
send["password"] = $.cookie("pwd")
|
||||
send["cmd"] = 'cleandatebase'
|
||||
console.log(send)
|
||||
$.ajax({
|
||||
url: debugserver+'/api',
|
||||
type: 'POST',
|
||||
async: false,
|
||||
contentType:"application/json",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(send),
|
||||
beforeSend: function() {
|
||||
|
||||
},
|
||||
success: function(json){
|
||||
console.log(json)
|
||||
$.alert(lang.UISTR_SUCCESSFUL);
|
||||
},
|
||||
error: function(error){
|
||||
console.log("reset error::::",error);
|
||||
notify(lang.UISTR_ERRNONESUCCESS)
|
||||
},
|
||||
always: function () {
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel:{
|
||||
text: lang.UISTR_NO_ESC,
|
||||
btnClass: 'waves-effect waves-button',
|
||||
action: function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function onUploadSuccess() {
|
||||
const homeChannel = new BroadcastChannel('home_refresh_channel');
|
||||
homeChannel.postMessage({type: 'refresh_home'});
|
||||
homeChannel.close();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,12 @@
|
|||
@echo off
|
||||
setlocal
|
||||
set EXE=%~dp0bin\Debug\HanvonF710XAttendanceService.exe
|
||||
if not exist "%EXE%" set EXE=%~dp0bin\Release\HanvonF710XAttendanceService.exe
|
||||
if not exist "%EXE%" (
|
||||
echo Build the project first. EXE not found.
|
||||
exit /b 1
|
||||
)
|
||||
set INSTALLUTIL=%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe
|
||||
echo Installing Hanvon F710X Attendance Service from %EXE%
|
||||
"%INSTALLUTIL%" "%EXE%"
|
||||
endlocal
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="BouncyCastle" version="1.8.5" targetFramework="net472" />
|
||||
<package id="Google.Protobuf" version="3.11.4" targetFramework="net472" />
|
||||
<package id="K4os.Compression.LZ4" version="1.1.11" targetFramework="net472" />
|
||||
<package id="K4os.Compression.LZ4.Streams" version="1.1.11" targetFramework="net472" />
|
||||
<package id="K4os.Hash.xxHash" version="1.0.6" targetFramework="net472" />
|
||||
<package id="MySql.Data" version="8.0.23" targetFramework="net472" />
|
||||
<package id="SSH.NET" version="2020.0.0" targetFramework="net472" />
|
||||
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
|
||||
<package id="System.Memory" version="4.5.3" targetFramework="net472" />
|
||||
<package id="System.Numerics.Vectors" version="4.4.0" targetFramework="net472" />
|
||||
<package id="System.Runtime.CompilerServices.Unsafe" version="4.5.2" targetFramework="net472" />
|
||||
</packages>
|
||||
Loading…
Reference in New Issue