24 Commits
dev ... 2025.4

Author SHA1 Message Date
0c98628d03 Merge pull request 'dev-2025' (#2) from dev-2025 into main
Reviewed-on: #2
2025-04-11 16:37:08 +08:00
4c658628be Merge branch 'main' into dev-2025 2025-04-11 16:36:40 +08:00
e0a34dbb9c 🆗 给dialog加边框 2025-03-31 18:24:25 +08:00
45cd64b284 🆗 隐藏头像 2025-03-31 18:20:16 +08:00
9cf8e0a47f 🆗 修复异常打卡边框的圆角 2025-03-31 17:15:02 +08:00
49e4b473c6 🆗 给打卡时间异常和补签卡加边框样式展示 2025-03-31 17:05:06 +08:00
1a06ab6807 🆗 优化日志打印, 页面显示打卡位置 2025-03-31 10:56:18 +08:00
615eec3b7a 🆗 移除餐补标志, 桌面端显示优化 2025-03-31 10:19:13 +08:00
fdc23e4326 🆗 修复日志打印错误 2025-03-26 10:56:21 +08:00
0c0cb8607e 删除多余的index.0.html 2025-03-24 01:33:13 +08:00
7a2506ff98 Merge pull request 'dev-2025' (#1) from dev-2025 into main
Reviewed-on: https://2ha.me/jimlee/hoperun-custom-sign/pulls/1
2025-03-21 07:26:29 +00:00
b782e398fd 🆗 修复移动端展示错误 2025-03-21 15:24:48 +08:00
f789b3b255 🆗 增加dockerFile用于直接部署到docker 2025-03-21 15:16:09 +08:00
25d326e4a7 🆗 添加2025年调休与节假日配置
🆗 调整样式兼容手机端访问
2024-11-29 16:04:56 +08:00
1d94c923bd 🆗 添加2024年调休与节假日配置 2024-01-01 15:46:19 +08:00
83fe28b76d 🆗 同步修改餐补时间位21点之后 2023-07-24 10:28:28 +08:00
jimlee
24cf5c7035 🆗 增加定时打卡前模拟重新登录 2023-04-26 13:09:45 +08:00
jimlee
e26ef9fa3b 🆗 修改日志打印为slf4j 2023-04-23 10:25:54 +08:00
jimlee
dc9ab0d4ee 🆗 修复生成定时打卡任务时,没有根据状态判断重复生成 2023-04-13 10:55:57 +08:00
jimlee
1bdc5e5e3c 🆗 修复定时打卡缺少id空指针 2023-04-11 11:04:00 +08:00
jimlee
9fabaa9bee 🆗 切换回老的打卡模式,移除jsessionid登录 2023-04-10 23:14:27 +08:00
jimlee
dae6cb5522 🆗 兼容yxl项目
🆗 兼容多次不打卡的清空,默认取最近一次打卡的信息
2023-03-22 23:46:42 +08:00
jimlee
8e9842b4f9 🆗 由于默认密码变更,已无法后台登录,改为使用jsessionID登录
🆗 兼容新的打卡
🆗 增加自动打卡开关
2023-03-22 19:34:53 +08:00
jimlee
becdffe1d4 🆗 由于默认密码变更,已无法后台登录,改为使用jsessionID登录
🆗 兼容新的打卡
🆗 增加自动打卡开关
2023-03-22 18:10:03 +08:00
24 changed files with 1493 additions and 383 deletions

4
.gitignore vendored
View File

@@ -29,3 +29,7 @@ logs
**/target
**/logs
*.sh
.idea

8
DockerFile Normal file
View File

@@ -0,0 +1,8 @@
FROM openjdk:8-jre
LABEL name="hoperun-custom-sign"
MAINTAINER li@2ha.me
WORKDIR /
ADD target/hoperun-sign-1.0-SNAPSHOT.jar app.jar
EXPOSE 8982
ENTRYPOINT ["java", "-jar"]
CMD ["app.jar"]

11
pom.xml
View File

@@ -21,7 +21,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.code.style>official</kotlin.code.style>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
<kotlin.version>1.7.22</kotlin.version>
<kotlin.version>2.0.20</kotlin.version>
</properties>
<build>
@@ -34,7 +34,7 @@
<configuration>
<fork>true</fork>
<outputDirectory>./target</outputDirectory>
<classifier>executable</classifier>
<!-- <classifier>executable</classifier>-->
</configuration>
<executions>
<execution>
@@ -89,6 +89,7 @@
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
</plugins>
</build>
@@ -116,6 +117,12 @@
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
</dependency>
</dependencies>
<distributionManagement>

309
settings.xml Normal file
View File

@@ -0,0 +1,309 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<!--
| This is the configuration file for Maven. It can be specified at two levels:
|
| 1. User Level. This settings.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -s /path/to/user/settings.xml
|
| 2. Global Level. This settings.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.conf}/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gs /path/to/global/settings.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation. Where appropriate, the default
| values (values used when the setting is not specified) are provided.
|
|-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->
<!-- interactiveMode
| This will determine whether maven prompts you when it needs input. If set to false,
| maven will use a sensible default value, perhaps based on some other setting, for
| the parameter in question.
|
| Default: true
<interactiveMode>true</interactiveMode>
-->
<!-- offline
| Determines whether maven should attempt to connect to the network when executing a build.
| This will have an effect on artifact downloads, artifact deployment, and others.
|
| Default: false
<offline>false</offline>
-->
<!-- pluginGroups
| This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
| when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
| "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
|-->
<pluginGroups>
<!-- pluginGroup
| Specifies a further group identifier to use for plugin lookup.
<pluginGroup>com.your.plugins</pluginGroup>
-->
</pluginGroups>
<!-- TODO Since when can proxies be selected as depicted? -->
<!-- proxies
| This is a list of proxies which can be used on this machine to connect to the network.
| Unless otherwise specified (by system property or command-line switch), the first proxy
| specification in this list marked as active will be used.
|-->
<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->
</proxies>
<!-- servers
| This is a list of authentication profiles, keyed by the server-id used within the system.
| Authentication profiles can be used whenever maven must make a connection to a remote server.
|-->
<servers>
<!-- server
| Specifies the authentication information to use when connecting to a particular server, identified by
| a unique name within the system (referred to by the 'id' attribute below).
|
| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are
| used together.
|
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>
-->
<!-- Another sample, using keys to authenticate.
<server>
<id>siteServer</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>optional; leave empty if not used.</passphrase>
</server>
-->
<server>
<id>2ha</id>
<username>jimlee</username>
<password>wysnih-dybbyQ-0rigti</password>
</server>
<server>
<id>maven-releases</id>
<username>jimlee</username>
<password>wysnih-dybbyQ-0rigti</password>
</server>
<server>
<id>maven-snapshots</id>
<username>jimlee</username>
<password>wysnih-dybbyQ-0rigti</password>
</server>
</servers>
<!-- mirrors
| This is a list of mirrors to be used in downloading artifacts from remote repositories.
|
| It works like this: a POM may declare a repository to use in resolving certain artifacts.
| However, this repository may have problems with heavy traffic at times, so people have mirrored
| it to several places.
|
| That repository definition will have a unique id, so we can create a mirror reference for that
| repository, to be used as an alternate download site. The mirror site will be the preferred
| server for that repository.
|-->
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
<mirror>
<id>mirror-all</id>
<mirrorOf>*</mirrorOf>
<name>2ha mirror</name>
<url>https://2ha.me:18082/repository/maven-public/</url>
</mirror>
</mirrors>
<!-- profiles
| This is a list of profiles which can be activated in a variety of ways, and which can modify
| the build process. Profiles provided in the settings.xml are intended to provide local machine-
| specific paths and repository locations which allow the build to work in the local environment.
|
| For example, if you have an integration testing plugin - like cactus - that needs to know where
| your Tomcat instance is installed, you can provide a variable here such that the variable is
| dereferenced during the build process to configure the cactus plugin.
|
| As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
| section of this document (settings.xml) - will be discussed later. Another way essentially
| relies on the detection of a property, either matching a particular value for the property,
| or merely testing its existence. Profiles can also be activated by JDK version prefix, where a
| value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
| Finally, the list of active profiles can be specified directly from the command line.
|
| NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
| repositories, plugin repositories, and free-form properties to be used as configuration
| variables for plugins in the POM.
|
|-->
<profiles>
<!-- profile
| Specifies a set of introductions to the build process, to be activated using one or more of the
| mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
| or the command line, profiles have to have an ID that is unique.
|
| An encouraged best practice for profile identification is to use a consistent naming convention
| for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
| This will make it more intuitive to understand what the set of introduced profiles is attempting
| to accomplish, particularly when you only have a list of profile id's for debug.
|
| This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
<profile>
<id>jdk-1.4</id>
<activation>
<jdk>1.4</jdk>
</activation>
<repositories>
<repository>
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<url>http://www.myhost.com/maven/jdk14</url>
<layout>default</layout>
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>
-->
<!--
| Here is another profile, activated by the property 'target-env' with a value of 'dev', which
| provides a specific path to the Tomcat instance. To use this, your plugin configuration might
| hypothetically look like:
|
| ...
| <plugin>
| <groupId>org.myco.myplugins</groupId>
| <artifactId>myplugin</artifactId>
|
| <configuration>
| <tomcatLocation>${tomcatPath}</tomcatLocation>
| </configuration>
| </plugin>
| ...
|
| NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
| anything, you could just leave off the <value/> inside the activation-property.
|
<profile>
<id>env-dev</id>
<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>
<properties>
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>
-->
<profile>
<id>jimlee-dev</id>
<repositories>
<repository>
<id>2ha</id>
<name>2ha.me nexus private</name>
<url>https://2ha.me:18082/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
<repository>
<id>maven-releases</id>
<name>Nexus Release Repository</name>
<url>https://2ha.me:18082/repository/private/</url>
</repository>
<repository>
<id>maven-snapshots</id>
<name>Nexus Snapshot Repository</name>
<url>https://2ha.me:18082/repository/private/</url>
</repository>
</repositories>
</profile>
</profiles>
<!-- activeProfiles
| List of profiles that are active for all builds.
|
<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>
-->
<activeProfiles>
<activeProfile>jimlee-dev</activeProfile>
</activeProfiles>
</settings>

View File

@@ -1,5 +1,6 @@
package com.pomelotea.hoperun.sign
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@@ -14,8 +15,29 @@ import java.util.*
* 启动入口
**/
@SpringBootApplication
open class DakaApplication {
}
open class DakaApplication
val <T : Any> T.logger: Logger
get() = LoggerFactory.getLogger(this::class.java)
val holidays: List<String> = listOf(
// 元旦
"2025-01-01",
// 春节
"2025-01-28", "2025-01-29", "2025-01-30", "2025-01-31", "2025-02-01", "2025-02-02", "2025-02-03", "2025-02-04",
// 清明
"2025-04-04", "2025-04-05", "2025-04-06",
// 劳动节
"2025-05-01", "2025-05-02", "2025-05-05", "2025-05-04", "2025-05-05",
// 端午节
"2025-05-31", "2025-06-01", "2025-06-02",
// 中秋
// 国庆
"2025-10-01", "2025-10-02", "2025-10-03", "2025-10-04", "2025-10-05", "2025-10-06", "2025-10-07", "2025-10-08")
// 周末要上班的日期
val workdays: List<String> = listOf("2025-01-26", "2025-02-08", "2025-04-27", "2025-10-11", "2025-09-28", "2025-10-12")
private val logger = LoggerFactory.getLogger("APPLICATION-STARTER")
fun main(args: Array<String>) {
SpringApplication.run(DakaApplication::class.java, *args)

View File

@@ -3,81 +3,66 @@ package com.pomelotea.hoperun.sign.api
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import com.alibaba.fastjson.TypeReference
import com.pomelotea.hoperun.sign.config.HoperunUserConfig
import com.pomelotea.hoperun.sign.common.*
import com.pomelotea.hoperun.sign.config.HoperunUserConfig.deviceMap
import com.pomelotea.hoperun.sign.config.HoperunUserConfig.getUserConfig
import com.pomelotea.hoperun.sign.config.HoperunUserConfig.userConfigMap
import com.pomelotea.hoperun.sign.config.UserConfig
import com.pomelotea.hoperun.sign.scheduler.AutoDakaScheduler
import com.pomelotea.hoperun.sign.scheduler.AutoDakaScheduler.Companion.dakaQueue
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
import java.text.SimpleDateFormat
import java.time.Duration
import java.time.LocalDate
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
/**
*
* @version 0.0.1
* @author jimlee
* date 2022-07-15 11:01
* update: 2023/3/22
* hoperun打卡服务接口
**/
@RestController
@RequestMapping("/api/daka")
class HoperunSignController(
private val hoperunUserConfig: HoperunUserConfig
) {
companion object {
const val DAKA_URL = "http://pom.hoperun.com:8187/attm/attence/recordAttendance"
const val MONTH_ATT_URL = "http://pom.hoperun.com:8187/attm/calendar/monthAtt"
const val LOGIN_URL = "http://pom.hoperun.com:8187/attm/login/login"
val client = OkHttpClient()
.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.callTimeout(Duration.ofSeconds(10))
// .addInterceptor(LogInterceptor())
.build()
val sessionMap: MutableMap<String, String?> = HashMap()
val expireMap: MutableMap<String, Long> = HashMap()
class HoperunSignController {
fun getNowDateyyyy_MM(): String {
val localDate = LocalDate.now()
return "${localDate.year}-${pad(localDate.monthValue)}"
init {
AutoDakaScheduler()
// AutoRenewSessionScheduler()
val yxl = UserConfig(
project_id = "U2103S000112",
projectcode = "U2103S000112",
projectname = "JRKF-浙江网商-技术服务外包",
device = "Android 12;Redmi;M2007J3SC;deviceId:OAIDe7fa6084205e9a22d8f6f71bc91893ff;deviceName:Android"
)
userConfigMap["16638"] = yxl
}
fun getLastDateyyyy_MM(): String {
val localDate = LocalDate.now().minusMonths(1)
return "${localDate.year}-${pad(localDate.monthValue)}"
}
// @GetMapping("/username/{employeeNo}/{checked}/{jsessionId}")
@GetMapping("/username/{employeeNo}/{checked}")
fun getUsername(
@PathVariable employeeNo: String,
@PathVariable checked: Boolean
): WebResult<LoginResponse?> {
/*
if (jsessionId != null) {
getJsessionIdAutoLogin(employeeNo, jsessionId)
}*/
fun getNowDateyyyy_MM_dd(): String {
val localDate = LocalDate.now()
return "${localDate.year}-${pad(localDate.monthValue)}-${pad(localDate.dayOfMonth)}"
}
private fun pad(num: Int): String {
return if (num < 10) "0$num" else "$num"
}
}
@GetMapping("/username/{employeeNo}")
fun getUsername(@PathVariable employeeNo: String): WebResult<LoginResponse?> {
val userConfig = getUserConfig(employeeNo)
if (userConfig == null) {
return WebResult.getFailed("登陆失败")
val userConfig = getUserConfig(employeeNo).let {
defaultLogin(employeeNo)
getUserConfig(employeeNo)
}
userConfig ?: return WebResult.getFailed("登陆失败")
userConfig.autoDaka = checked
userConfig.device = deviceMap[employeeNo]
return WebResult.getSuccess(
LoginResponse(
userConfig.username,
@@ -89,19 +74,26 @@ class HoperunSignController(
)
}
@GetMapping("/auto/{employeeNo}/{checked}")
fun autoDaka(@PathVariable employeeNo: String, @PathVariable checked: Boolean): WebResult<Any?> {
val userConfig = getUserConfig(employeeNo) ?: return WebResult.getFailed("需要重新登录")
userConfig.autoDaka = checked
return WebResult.getSuccess(checked)
}
@GetMapping("/last/{employeeNo}")
fun getLast5DaysRecord(@PathVariable employeeNo: String): WebResult<Any?> {
val jsessionId = getJsessionIdAutoLogin(employeeNo) ?: return WebResult.getFailed("登陆失败")
val jsessionId = sessionMap.get(employeeNo) ?: return WebResult.getFailed("登陆失败")
return WebResult.getSuccess(monthAtt(employeeNo, jsessionId))
}
@PostMapping("/endTime")
fun endTime(@RequestBody request: DakaRequest): WebResult<Any?> {
val userConfig = hoperunUserConfig.userConfigMap.get(request.employeeNo)
val userConfig = userConfigMap.get(request.employeeNo)
if (userConfig?.device == null) {
return WebResult.getFailed("用户没有配置的deviceUA")
}
val jsessionId = getJsessionIdAutoLogin(request.employeeNo)
val jsessionId = sessionMap.get(request.employeeNo)
if (jsessionId == null) {
return WebResult.getFailed("登陆失败")
}
@@ -126,14 +118,11 @@ class HoperunSignController(
@PostMapping("/beginTime")
fun beginTime(@RequestBody request: DakaRequest): WebResult<Any?> {
val userConfig = hoperunUserConfig.userConfigMap.get(request.employeeNo)
val userConfig = userConfigMap.get(request.employeeNo)
if (userConfig?.device == null) {
return WebResult.getFailed("用户没有配置的deviceUA")
}
val jsessionId = getJsessionIdAutoLogin(request.employeeNo)
if (jsessionId == null) {
return WebResult.getFailed("登陆失败")
}
val jsessionId = sessionMap.get(request.employeeNo)
val date = if (request.date == "今天") SimpleDateFormat("yyyy-MM-dd").format(Date()) else request.date
val dakaRequest = Request.Builder()
.url(DAKA_URL)
@@ -152,20 +141,6 @@ class HoperunSignController(
return WebResult.getSuccess(JSONObject.parseObject(result, DakaResponse::class.java))
}
private fun getJsessionIdAutoLogin(employeeNo: String): String? {
if (sessionMap.get(employeeNo) == null || expireMap.get(employeeNo) == null || expireMap.get(employeeNo)!! < System.currentTimeMillis()) {
login(employeeNo)
}
return sessionMap.get(employeeNo)
}
private fun getUserConfig(employeeNo: String): UserConfig? {
val userConfig = hoperunUserConfig.userConfigMap.get(employeeNo)
if (userConfig?.username == null) {
login(employeeNo)
}
return hoperunUserConfig.userConfigMap.get(employeeNo)
}
/**
* 查询近两个月的
@@ -181,6 +156,24 @@ class HoperunSignController(
queryMonthAttData(employeeNo, jsessionId, getLastDateyyyy_MM() + "-01")
val lastMonthAttLogs = lastMonthAttList.sortedByDescending { it.yearmonth }.filter { it.dateType == "1" }
monthAttResult.addAll(lastMonthAttLogs)
val monthAttLog = monthAttResult.find { it.yearmonth == getNowDateyyyy_MM_dd() }
val autoDaka =
dakaQueue.filter { it.dakaDate == getNowDateyyyy_MM_dd() }.findLast { it.employeeNo == employeeNo }
if (autoDaka != null && monthAttLog != null) {
monthAttLog.autoDakaBeginTime = autoDaka.beginTime
monthAttLog.autoDakaEndTime = autoDaka.endTime
}
monthAttResult.forEach {
it.actual_area_begin =
if (!it.actual_area_begin.isNullOrBlank()) {
if (it.actual_area_begin == "buqianka") "补签卡" else
area_regex.find(it.actual_area_begin!!)?.groups?.get(0)?.value
} else null
it.actual_area_end = if (!it.actual_area_end.isNullOrBlank()) {
if (it.actual_area_end == "buqianka") "补签卡" else
area_regex.find(it.actual_area_end!!)?.groups?.get(0)?.value
} else null
}
return monthAttResult
}
@@ -201,30 +194,77 @@ class HoperunSignController(
object : TypeReference<List<MonthAttLog>?>() {})
}
private fun login(employeeNo: String) {
val jsessionId: String?
val loginRequest = Request.Builder()
.url(LOGIN_URL)
.post(
FormBody.Builder()
.add("login_id", padEmployeeNumber(employeeNo))
.add("password", "123456")
.build()
)
.build()
val response = client.newCall(loginRequest).execute()
jsessionId = response.request.url.pathSegments[2].substring(19)
sessionMap.put(employeeNo, jsessionId)
expireMap.put(employeeNo, System.currentTimeMillis() + 300000)
// 读取员工姓名
if (hoperunUserConfig.userConfigMap[employeeNo]?.username == null) {
private fun defaultLogin(employeeNo: String) {
resetJSessionId(employeeNo)
setUserConfig(employeeNo)
}
/*
private fun setUserConfig(employeeNo: String, jsessionId: String) {
val attendancesDetailRequest = Request.Builder()
.url("http://pom.hoperun.com:8187/attm/calendar/monthAtt?staff_code=${padEmployeeNumber(employeeNo)}&yearmonth=${getNowDateyyyy_MM()}-01")
.get()
.addHeader("Cookie", "JSESSIONID=$jsessionId")
.addHeader(
"User-Agent",
"Qing/0.9.113;iOS 16.4.1;Apple;iPhone13,2;deviceId:a8baf66f-fdeb-4f4d-b1e5-9fafcd5045b6;deviceName:iOS;clientId:10200;os:iOS 16.3.1;brand:Apple;model:iPhone13,2;lang:zh-CN;fontNum:0;fontScale:1.0;ver:10.7.14;Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
)
.addHeader("accept", "*")
.addHeader("Origin", "http://pom.hoperun.com:8187")
.addHeader("Referer", "http://pom.hoperun.com:8187/attm/attence/getInfo")
.addHeader("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
.build()
val attendancesDetailResp = client.newCall(attendancesDetailRequest).execute()
val bodyString = attendancesDetailResp.body?.string()
val attendancesDetailResponse = JSONObject.parseObject(bodyString, AttendancesDetailResponse::class.java)
val dataList: List<AttendancesDetail> =
JSONObject.parseObject(attendancesDetailResponse.data, object : TypeReference<List<AttendancesDetail>>() {})
val userConfig: UserConfig = userConfigMap.get(employeeNo) ?: UserConfig()
if (dataList.isNotEmpty()) {
val lastDakaInfo: AttendancesDetail = dataList.sortedByDescending { it.yearmonth }
.filter { it.project_id != "-1" }
.filter { it.begin_time != null }
.firstOrNull()!!
if (userConfig.projectcode == null) {
userConfig.projectcode = lastDakaInfo.projectcode
}
if (userConfig.projectname == null) {
userConfig.projectname = lastDakaInfo.projectname
}
if (userConfig.project_id == null) {
userConfig.project_id = lastDakaInfo.project_id
}
}
// username 要从主页的html元素中获取
val indexRequest = Request.Builder()
.url("http://pom.hoperun.com:8187/attm/attence/getInfo")
.get()
.addHeader("Cookie", "JSESSIONID=$jsessionId")
.addHeader(
"User-Agent",
"Qing/0.9.113;iOS 16.3.1;Apple;iPhone13,2;deviceId:a8baf66f-fdeb-4f4d-b1e5-9fafcd5045b6;deviceName:iOS;clientId:10200;os:iOS 16.3.1;brand:Apple;model:iPhone13,2;lang:zh-CN;fontNum:0;fontScale:1.0;ver:10.7.14;Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
)
.addHeader("accept", "*")
.addHeader("Origin", "http://pom.hoperun.com:8187")
.addHeader("Referer", "http://pom.hoperun.com:8187/attm/attence/getInfo")
.addHeader("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
.build()
val indexResp = client.newCall(indexRequest).execute()
val indexHtml = indexResp.body?.string()
val doc = Jsoup.parse(indexHtml!!)
userConfig.username =
doc.select("#attendance-detail-content > div.container.none-padding > div > div:nth-child(1) > div:nth-child(4)")
.text()
userConfig.employeeNo = employeeNo
HoperunUserConfig.addUserConfig(
employeeNo, userConfig
)
}*/
private fun setUserConfig(employeeNo: String) {
if (userConfigMap[employeeNo] != null
&& userConfigMap[employeeNo]!!.timeout > System.currentTimeMillis()) return
// 获取deviceua
val loginRequest = Request.Builder()
.url("http://pom.hoperun.com:8187/attp/login/login.do")
@@ -260,8 +300,8 @@ class HoperunSignController(
val dakaInfo = dakaJsonArray.getJSONObject(dakaJsonArray.size - 1)
val dakaList = dakaInfo.getJSONArray("list")
var lastDakaInfo = dakaList.getJSONObject(dakaList.size - 1)
val userConfig: UserConfig = hoperunUserConfig.userConfigMap.get(employeeNo) ?: UserConfig()
val userConfig: UserConfig = userConfigMap.get(employeeNo) ?: UserConfig()
userConfig.employeeNo = employeeNo
if (lastDakaInfo.getString("begin_time") == null) {
for (i in dakaList.size - 1 downTo 0) {
lastDakaInfo = dakaList.getJSONObject(i)
@@ -273,7 +313,8 @@ class HoperunSignController(
val username: String = lastDakaInfo.getString("staff_name")
userConfig.username = username
if (userConfig.device == null) {
if (lastDakaInfo.getString("actual_area_end") != null) {
if (lastDakaInfo.getString("actual_area_end") != null
&& lastDakaInfo.getString("actual_area_end") != "buqianka") {
val area: String = lastDakaInfo.getString("actual_area_end")
userConfig.device = area.substring(area.lastIndexOf("Qing") + 13)
} else if (lastDakaInfo.getString("actual_area_begin") != null) {
@@ -292,59 +333,8 @@ class HoperunSignController(
if (userConfig.project_id == null) {
userConfig.project_id = dakaInfo.getString("pro_id")
}
hoperunUserConfig.addUserConfig(
employeeNo, userConfig
)
userConfigMap[employeeNo] = userConfig
}
private fun beginTimeHoperunDakaRequest(
employeeNo: String,
yearmonth: String,
begin_time: String?
): HoperunDakaRequest {
val ua = hoperunUserConfig.getUA(employeeNo)
val userConfig: UserConfig = getUserConfig(employeeNo)!!
val hoperunDakaRequest = HoperunDakaRequest(
staff_code = padEmployeeNumber(employeeNo),
yearmonth = yearmonth,
userConfig.project_id!!,
userConfig.projectname!!,
userConfig.projectcode!!,
actualArea = ua
)
hoperunDakaRequest.begin_time = begin_time
return hoperunDakaRequest
}
private fun endTimeHoperunDakaRequest(
employeeNo: String,
yearmonth: String,
end_time: String?
): HoperunDakaRequest {
val ua = hoperunUserConfig.getUA(employeeNo)
val userConfig: UserConfig = getUserConfig(employeeNo)!!
val hoperunDakaRequest = HoperunDakaRequest(
staff_code = padEmployeeNumber(employeeNo),
yearmonth = yearmonth,
userConfig.project_id!!,
userConfig.projectname!!,
userConfig.projectcode!!,
actualArea = ua
)
hoperunDakaRequest.end_time = end_time
return hoperunDakaRequest
}
fun padEmployeeNumber(employeeNumber: String): String {
return when (employeeNumber.length) {
5 -> "0000" + employeeNumber
4 -> "00000" + employeeNumber
else -> employeeNumber
}
}
}
@@ -358,7 +348,13 @@ data class DakaResponse(
var result: String? = null,
var comment: String? = null,
var data: String? = null
)
) {
override fun toString(): String {
return "DakaResponse(result=$result, comment=$comment, data=$data)"
}
}
class WebResult<T> protected constructor() : java.io.Serializable {
var data: T? = null
@@ -389,6 +385,8 @@ class WebResult<T> protected constructor() : java.io.Serializable {
}
}
val area_regex = Regex(".*[\\u4e00-\\u9fa5]|[0-9][\\u4e00-\\u9fa5]")
data class MonthAttLog(
var area_id: String? = null,
var area_id_begin: String? = null,
@@ -402,7 +400,11 @@ data class MonthAttLog(
var project_id: String? = null,
var projectcode: String? = null,
var staff_code: String? = null,
var yearmonth: String? = null
var yearmonth: String? = null,
var autoDakaBeginTime: String? = null,
var autoDakaEndTime: String? = null,
var actual_area_begin: String? = null,
var actual_area_end: String? = null
)
data class HoperunDakaRequest(

View File

@@ -0,0 +1,30 @@
package com.pomelotea.hoperun.sign.api.model
/**
*
* @version 0.0.1
* @author jimlee
* date 2023-03-22 13:52
*
**/
data class AttendancesDetailResponse(
val data: String,
val success: Boolean = false
)
data class AttendancesDetail(
val area_id: String? = null,
val area_id_begin: String? = null,
val area_id_end: String? = null,
val attState: String? = null,
val att_type: String? = null,
val begin_time: String? = null,
val dateType: String? = null,
val departmentcode: String? = null,
val end_time: String? = null,
val project_id: String? = null,
val projectcode: String? = null,
val projectname: String? = null,
val staff_code: String? = null,
val yearmonth: String? = null,
)

View File

@@ -0,0 +1,188 @@
package com.pomelotea.hoperun.sign.common
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import com.pomelotea.hoperun.sign.api.DakaResponse
import com.pomelotea.hoperun.sign.api.HoperunDakaRequest
import com.pomelotea.hoperun.sign.config.HoperunUserConfig
import com.pomelotea.hoperun.sign.config.HoperunUserConfig.getUserConfig
import com.pomelotea.hoperun.sign.config.UserConfig
import com.pomelotea.hoperun.sign.holidays
import com.pomelotea.hoperun.sign.workdays
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.time.DayOfWeek
import java.time.Duration
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
/**
*
* @version 0.0.1
* @author jimlee
* date 2023-03-22 15:45
*
**/
const val DAKA_URL = "http://pom.hoperun.com:8187/attm/attence/recordAttendance"
const val MONTH_ATT_URL = "http://pom.hoperun.com:8187/attm/calendar/monthAtt"
val DEFAULT_ZONE: ZoneOffset = ZoneOffset.of("+8")
val client = OkHttpClient()
.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.callTimeout(Duration.ofSeconds(10))
// .addInterceptor(LogInterceptor())
.build()
val sessionMap: MutableMap<String, String?> = HashMap()
fun getNowDateyyyy_MM(): String {
val localDate = LocalDate.now()
return "${localDate.year}-${pad(localDate.monthValue)}"
}
fun getLastDateyyyy_MM(): String {
val localDate = LocalDate.now().minusMonths(1)
return "${localDate.year}-${pad(localDate.monthValue)}"
}
fun getNowDateyyyy_MM_dd(): String {
val localDate = LocalDate.now()
return "${localDate.year}-${pad(localDate.monthValue)}-${pad(localDate.dayOfMonth)}"
}
fun getYesterdayyyyy_MM_dd(): String {
val localDate = LocalDate.now()
localDate.plusDays(-1)
return "${localDate.year}-${pad(localDate.monthValue)}-${pad(localDate.dayOfMonth)}"
}
private fun pad(num: Int): String {
return if (num < 10) "0$num" else "$num"
}
fun isWeekend(dakaDate: String): Boolean {
val date = LocalDate.parse(dakaDate, DateTimeFormatter.ISO_LOCAL_DATE)
return date.dayOfWeek == DayOfWeek.SATURDAY || date.dayOfWeek == DayOfWeek.SUNDAY
}
fun isWeekday(dakaDate: String): Boolean {
val date = LocalDate.parse(dakaDate, DateTimeFormatter.ISO_LOCAL_DATE)
return date.dayOfWeek == DayOfWeek.MONDAY || date.dayOfWeek == DayOfWeek.TUESDAY
|| date.dayOfWeek == DayOfWeek.WEDNESDAY || date.dayOfWeek == DayOfWeek.THURSDAY || date.dayOfWeek == DayOfWeek.FRIDAY
}
fun beginTime(employeeNo: String, date: String, time: String, jsessionId: String = sessionMap.get(employeeNo)!!): DakaResponse {
val dakaRequest = Request.Builder()
.url(DAKA_URL)
.post(
JSON.toJSONString(
beginTimeHoperunDakaRequest(
employeeNo,
date,
time
)
).toRequestBody("application/json;charset=utf-8".toMediaTypeOrNull())
)
.addHeader("Cookie", "JSESSIONID=$jsessionId")
.build()
val result: String? = client.newCall(dakaRequest).execute().body?.string()
return JSONObject.parseObject(result, DakaResponse::class.java)
}
fun beginTimeHoperunDakaRequest(
employeeNo: String,
yearmonth: String,
begin_time: String?
): HoperunDakaRequest {
val ua = HoperunUserConfig.getUA(employeeNo)
val userConfig: UserConfig = HoperunUserConfig.getUserConfig(employeeNo)!!
val hoperunDakaRequest = HoperunDakaRequest(
staff_code = padEmployeeNumber(employeeNo),
yearmonth = yearmonth,
userConfig.project_id!!,
userConfig.projectname!!,
userConfig.projectcode!!,
actualArea = ua
)
hoperunDakaRequest.begin_time = begin_time
return hoperunDakaRequest
}
fun endTime(employeeNo: String, date: String, time: String, jsessionId: String = sessionMap.get(employeeNo)!!): DakaResponse {
val dakaRequest = Request.Builder()
.url(DAKA_URL)
.post(
JSON.toJSONString(
endTimeHoperunDakaRequest(
employeeNo,
date,
time
)
).toRequestBody("application/json;charset=utf-8".toMediaTypeOrNull())
)
.addHeader("Cookie", "JSESSIONID=$jsessionId")
.build()
val result: String? = client.newCall(dakaRequest).execute().body?.string()
return JSONObject.parseObject(result, DakaResponse::class.java)
}
fun padEmployeeNumber(employeeNumber: String): String {
return when (employeeNumber.length) {
5 -> "0000" + employeeNumber
4 -> "00000" + employeeNumber
else -> employeeNumber
}
}
fun endTimeHoperunDakaRequest(
employeeNo: String,
yearmonth: String,
end_time: String?
): HoperunDakaRequest {
val ua = HoperunUserConfig.getUA(employeeNo)
val userConfig: UserConfig = getUserConfig(employeeNo)!!
val hoperunDakaRequest = HoperunDakaRequest(
staff_code = padEmployeeNumber(employeeNo),
yearmonth = yearmonth,
userConfig.project_id!!,
userConfig.projectname!!,
userConfig.projectcode!!,
actualArea = ua
)
hoperunDakaRequest.end_time = end_time
return hoperunDakaRequest
}
const val LOGIN_URL = "http://pom.hoperun.com:8187/attm/login/login"
fun resetJSessionId(employeeNo: String) {
val jsessionId: String?
val loginRequest = Request.Builder()
.url(LOGIN_URL)
.post(
FormBody.Builder()
.add("login_id", padEmployeeNumber(employeeNo))
.add("password", "123456")
.build()
)
.build()
val response = client.newCall(loginRequest).execute()
jsessionId = response.request.url.pathSegments[2].substring(19)
sessionMap.put(employeeNo, jsessionId)
}
fun randowSecond(): String {
val second = Random().nextInt(59)
return if (second < 10) "0$second" else "$second"
}
fun isNeedDaka(dakaDate: String): Boolean {
return workdays.contains(dakaDate) || (isWeekday(dakaDate) && !holidays.contains(dakaDate))
}

View File

@@ -1,10 +1,5 @@
package com.pomelotea.hoperun.sign.config
import com.pomelotea.hoperun.sign.api.HoperunSignController.Companion.expireMap
import com.pomelotea.hoperun.sign.api.HoperunSignController.Companion.sessionMap
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
/**
*
* @version 0.0.1
@@ -12,21 +7,23 @@ import org.springframework.context.annotation.Configuration
* date 2022-07-18 09:56
* 用户配置
**/
@Configuration
@ConfigurationProperties("hoperun")
open class HoperunUserConfig {
val userConfigMap: MutableMap<String, UserConfig> = HashMap()
var address: String = "浙江省杭州市西湖区万塘路18号黄龙时代广场B座"
var longitueHead: String = "120.131"
var latitudeHead: String = "30.279"
var longitueShort: String = "120.136679"
var latitudeShort: String = "30.279766"
var qingUa: String = "Qing/0.9.101"
object HoperunUserConfig {
init {
expireMap.put("9119", System.currentTimeMillis() + 300000)
addUserConfig("9119", UserConfig(username = "李建明", device = "iOS 16.2;Apple;iPhone13,2;deviceId:a8baf66f-fdeb-4f4d-b1e5-9fafcd5045b6(", projectcode = "U2103S000078", project_id = "U2103S000078", projectname = "JRKF-银河资产对接合作平台贷项目"))
sessionMap.put("9119", "887B892ABF482A65AD9EFA4254250DFE")
val deviceMap = mapOf<String, String>(
"16638" to "Android 12;Redmi;M2007J3SC;deviceId:OAIDe7fa6084205e9a22d8f6f71bc91893ff;deviceName:Android",
"9119" to "iOS 16.4.1;Apple;iPhone13,2;deviceId:a8baf66f-fdeb-4f4d-b1e5-9fafcd5045b6;deviceName:iOS;clientId:10200;os:iOS 16.3.1;brand:Apple;model:iPhone13,2;lang:zh-CN;fontNum:0;fontScale:1.0;ver:10.7.14;Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
)
val userConfigMap: MutableMap<String, UserConfig> = HashMap()
var address: String = "浙江省杭州市西湖区飞天园区1号楼"
var longitueHead: String = "120.085"
var latitudeHead: String = "30.138"
var longitueShort: String = "120.0845715522375"
var latitudeShort: String = "30.140219809955912"
var qingUa: String = "Qing/0.9.113"
fun getUserConfig(employeeNo: String): UserConfig? {
return userConfigMap.get(employeeNo)
}
fun getUA(emplotyeeNo: String): String {
@@ -36,7 +33,7 @@ open class HoperunUserConfig {
"$longitueShort," +
"$latitudeShort;" +
"$qingUa;" +
(userConfigMap.get(emplotyeeNo)!!.device ?: "")
(deviceMap.get(emplotyeeNo) ?: "")
}
fun addUserConfig(emplotyeeNo: String, userConfig: UserConfig) {
@@ -60,8 +57,11 @@ open class HoperunUserConfig {
data class UserConfig(
var username: String? = null,
var employeeNo: String? = null,
var device: String? = null,
var project_id: String? = null,
var projectname: String? = null,
var projectcode: String? = null
var projectcode: String? = null,
var autoDaka: Boolean = false,
val timeout: Long = System.currentTimeMillis() + 1000 * 60 * 30
)

View File

@@ -0,0 +1,22 @@
package com.pomelotea.hoperun.sign.config
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
/**
*
* @author jimlee
* @create 2024/11/1 13:15
*/
@Configuration
open class CorsConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedMethods("*")
.allowedHeaders("*")
.allowedOriginPatterns("*")
.allowCredentials(true)
.maxAge(3600)
}
}

View File

@@ -0,0 +1,177 @@
package com.pomelotea.hoperun.sign.scheduler
import com.pomelotea.hoperun.sign.api.DakaResponse
import com.pomelotea.hoperun.sign.common.*
import com.pomelotea.hoperun.sign.config.HoperunUserConfig
import com.pomelotea.hoperun.sign.config.UserConfig
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.TimeUnit
/**
*
* @version 0.0.1
* @author jimlee
* date 2023-03-22 14:50
* 自动打卡定时任务
**/
class AutoDakaScheduler {
val timeThreadPool: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
val schedulerThreadPool: ScheduledExecutorService = Executors.newScheduledThreadPool(10)
init {
timeThreadPool.scheduleAtFixedRate({ addAutoDakaScheduled() }, 1, 60, TimeUnit.MINUTES)
}
companion object {
val dakaQueue: MutableList<Daka> = LinkedList()
val logger: Logger = LoggerFactory.getLogger("DAKA-SHCEDULER")
}
private fun addAutoDakaScheduled(dakaDate: String = getNowDateyyyy_MM_dd()) {
// 调休的工作日
if (isNeedDaka(dakaDate)) {
HoperunUserConfig.userConfigMap.forEach { (_, v) ->
if (v.autoDaka) {
// 没有当日的打卡信息才插入
var daka = dakaQueue.find { it.dakaDate == dakaDate && it.employeeNo == v.employeeNo }
if (daka == null) {
daka = generateDakaInfo(v, dakaDate)
dakaQueue.add(daka)
logger.info("添加打卡任务: ${v.username}, $daka")
}
}
}
}
dakaQueue.forEach {
if (!it.added) {
it.added = true
val beginDate = LocalDateTime.parse(
it.dakaDate + "T" + it.beginTime + ":" + randowSecond(),
DateTimeFormatter.ISO_LOCAL_DATE_TIME
)
val endDate = LocalDateTime.parse(
it.dakaDate + "T" + it.endTime + ":" + randowSecond(),
DateTimeFormatter.ISO_LOCAL_DATE_TIME
)
val beginSeconds = beginDate.toEpochSecond(DEFAULT_ZONE) - (System.currentTimeMillis() / 1000)
val endSeconds = endDate.toEpochSecond(DEFAULT_ZONE) - (System.currentTimeMillis() / 1000)
if (beginSeconds > 0) {
logger.info("[ADD-SCHEDULE]BEGIN:${it.employeeNo}:DATE:${it.dakaDate}TIME:${it.beginTime}")
schedulerThreadPool.schedule({ beginDaka(it) }, beginSeconds, TimeUnit.SECONDS)
}
if (endSeconds > 0) {
logger.info("[ADD-SCHEDULE]END:${it.employeeNo}:DATE:${it.dakaDate}TIME:${it.endTime}")
schedulerThreadPool.schedule({ endDaka(it) }, endSeconds, TimeUnit.SECONDS)
}
}
// schedulerThreadPool.schedule({beginDaka(it)}, 5, TimeUnit.SECONDS)
// schedulerThreadPool.schedule({ endDaka(it) }, 10, TimeUnit.SECONDS)
}
dakaQueue.removeIf { it.dakaDate.compareTo(dakaDate) == -1 }
printScheduler()
dakaQueue.forEach {
logger.info("Task: ${it.toCsv()}")
}
}
private fun beginDaka(daka: Daka) {
// println("begin:${daka.toCsv()}")
try {
logger.info("[EXECUTE]BEGIN:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.beginTime}")
// 模拟重新登录打卡
logger.info("${daka.employeeNo}-OLD-JSESSIONID: ${sessionMap[daka.employeeNo]}")
resetJSessionId(employeeNo = daka.employeeNo)
logger.info("${daka.employeeNo}-NEW-JSESSIONID: ${sessionMap[daka.employeeNo]}")
val resp: DakaResponse =
beginTime(employeeNo = daka.employeeNo, date = daka.dakaDate, time = daka.beginTime)
if (resp.result != "success") {
logger.error("打上班卡失败")
} else {
logger.info("打上班卡成功")
}
} catch (e: Exception) {
logger.error(e.message, e)
}
}
private fun endDaka(daka: Daka) {
// println("end:${daka.toCsv()}")
try {
logger.info("[EXECUTE]END:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.endTime}")
// 模拟重新登录打卡
logger.info("${daka.employeeNo}-OLD-JSESSIONID: ${sessionMap[daka.employeeNo]}")
resetJSessionId(employeeNo = daka.employeeNo)
logger.info("${daka.employeeNo}-NEW-JSESSIONID: ${sessionMap[daka.employeeNo]}")
val resp: DakaResponse = endTime(employeeNo = daka.employeeNo, date = daka.dakaDate, time = daka.endTime)
if (resp.result != "success") {
logger.error("打下班卡失败")
} else {
logger.info("打下班卡成功")
}
} catch (e: Exception) {
logger.error(e.message, e)
}
}
private fun generateDakaInfo(userConfig: UserConfig, dakaDate: String): Daka {
return Daka(
beginTime = "09:" + (10 + Random().nextInt(19)),
endTime = getRandomEndTime(),
employeeNo = userConfig.employeeNo!!,
dakaDate = dakaDate
)
}
fun printScheduler() {
schedulerThreadPool as ScheduledThreadPoolExecutor
val queue = schedulerThreadPool.queue
logger.info("任务队列数量: ${queue.size}")
dakaQueue.forEach {
logger.info(it.toString())
}
}
private fun getRandomEndTime(): String {
val hourArray = intArrayOf(18, 19, 20, 19, 19, 20, 19, 20, 20, 19, 18, 20)
val randomHour = hourArray[Math.round(Math.random() * 9).toInt()]
val randomMinute = if (randomHour == 18) {
30 + Random().nextInt(19)
} else {
val min = Random().nextInt(59)
if (min < 10) {
"0$min"
} else {
min
}
}
return "$randomHour:$randomMinute"
}
}
data class Daka(
val beginTime: String,
val endTime: String,
val employeeNo: String,
val dakaDate: String,
var added: Boolean = false
) {
fun toCsv(): String {
return "$employeeNo,$dakaDate,$beginTime,$endTime,$added"
}
override fun toString(): String {
return "工号: $employeeNo, 日期: $dakaDate, 上班卡: $beginTime, 下班卡: $endTime, 定时开关: $added"
}
}

View File

@@ -0,0 +1,70 @@
package com.pomelotea.hoperun.sign.scheduler
import com.pomelotea.hoperun.sign.common.client
import com.pomelotea.hoperun.sign.common.sessionMap
import com.pomelotea.hoperun.sign.logger
import okhttp3.Request
import org.jsoup.Jsoup
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
/**
*
* @version 0.0.1
* @author jimlee
* date 2023-03-23 08:54
*
**/
class AutoRenewSessionScheduler {
val schedulerThreadPool: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
init {
schedulerThreadPool.schedule({renewSession()}, 2, TimeUnit.MINUTES)
}
private fun renewSession() {
val iterator = sessionMap.iterator()
while (iterator.hasNext()) {
val item = iterator.next()
item.value?.let {
val result = index(it)
if (result != null) {
logger.info("[RENEW-SESSION]:SUCCESS:USER:$result")
} else {
logger.info("[REMOVE-SESSION]:USER:$result")
iterator.remove()
}
}
}
schedulerThreadPool.schedule({renewSession()}, (30 + Random().nextInt(30)).toLong(), TimeUnit.MINUTES)
}
private fun index(jsessionId: String): String? {
val indexRequest = Request.Builder()
.url("http://pom.hoperun.com:8187/attm/attence/getInfo")
.get()
.addHeader("Cookie", "JSESSIONID=$jsessionId")
.addHeader(
"User-Agent",
"Qing/0.9.113;iOS 16.3.1;Apple;iPhone13,2;deviceId:a8baf66f-fdeb-4f4d-b1e5-9fafcd5045b6;deviceName:iOS;clientId:10200;os:iOS 16.3.1;brand:Apple;model:iPhone13,2;lang:zh-CN;fontNum:0;fontScale:1.0;ver:10.7.14;Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
)
.addHeader("accept", "*/*")
.addHeader("Origin", "http://pom.hoperun.com:8187")
.addHeader("Referer", "http://pom.hoperun.com:8187/attm/attence/getInfo")
.addHeader("content-type", "application/x-www-form-urlencoded; charset=UTF-8")
.build()
val indexResp = client.newCall(indexRequest).execute()
val indexHtml = indexResp.body?.string()
val username = try {
Jsoup.parse(indexHtml!!).select("#attendance-detail-content > div.container.none-padding > div > div:nth-child(1) > div:nth-child(4)")
.text()
} catch (e: Exception) {
null
}
return username
}
}

View File

@@ -1,5 +1,5 @@
server:
port: 8080
port: 8982
hoperun:
address: "浙江省杭州市西湖区转塘街道凌家桥路飞天园区120"

View File

@@ -1,5 +1,5 @@
server:
port: 8080
port: 8082
error:
path: /error
@@ -18,7 +18,12 @@ spring:
name: 打卡平台
profiles:
active: dev
thymeleaf:
cache: false
deviceMap:
"16638": "Android 12;Redmi;M2007J3SC;deviceId:OAIDe7fa6084205e9a22d8f6f71bc91893ff;deviceName:Android"
"9119": "Qing/0.9.113;iOS 16.3.1;Apple;iPhone13,2;deviceId:a8baf66f-fdeb-4f4d-b1e5-9fafcd5045b6;deviceName:iOS;clientId:10200;os:iOS 16.3.1;brand:Apple;model:iPhone13,2;lang:zh-CN;fontNum:0;fontScale:1.0;ver:10.7.14;Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
hoperun:
address: "浙江省杭州市西湖区转塘街道凌家桥路飞天园区120"
longitueHead: "120.085"

View File

@@ -58,18 +58,19 @@
</encoder>
</appender>
<!-- REST接口日志文件 -->
<appender name="REST-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 默认系统日志文件 -->
<appender name="SCHEDULER-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>${logging.level}</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<file>${logging.path}/rest.log</file>
<file>${logging.path}/scheduler.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<maxFileSize>${logging.size}</maxFileSize>
<FileNamePattern>${logging.path}/rest.log.%d{yyyy-MM-dd}.%i</FileNamePattern>
<FileNamePattern>${logging.path}/scheduler.log.%d{yyyy-MM-dd}.%i</FileNamePattern>
<MaxHistory>${logging.maxHistory}</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
@@ -78,14 +79,6 @@
</encoder>
</appender>
<!--==================async appender======================= -->
<appender name="ASYNC-REST-APPENDER" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>256</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>true</neverBlock>
<appender-ref ref="REST-APPENDER"/>
</appender>
<!--==================logger======================= -->
<!-- boot logger -->
<logger name="APPLICATION-STARTER" level="${logging.level}" additivity="false">
@@ -93,37 +86,22 @@
<appender-ref ref="ROOT-APPENDER"/>
</logger>
<!-- scheduler logger -->
<logger name="DAKA-SHCEDULER" level="${logging.level}">
<appender-ref ref="SCHEDULER-APPENDER"/>
<appender-ref ref="STDOUT" />
</logger>
<!-- application logger -->
<logger name="com.pomelotea.hoperun.sign" level="${logging.level}" additivity="false">
<appender-ref ref="ROOT-APPENDER"/>
<appender-ref ref="ERROR-APPENDER"/>
<appender-ref ref="STDOUT" />
</logger>
<root level="${logging.level}">
<appender-ref ref="ROOT-APPENDER"/>
<appender-ref ref="ERROR-APPENDER"/>
<appender-ref ref="STDOUT" />
</root>
<!-- 本地调试日志输出到控制台 -->
<springProfile name="dev">
<!-- boot logger -->
<logger name="APPLICATION-STARTER" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERROR-APPENDER"/>
<appender-ref ref="ROOT-APPENDER"/>
</logger>
<logger name="com.pomelotea.hoperun.sign" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ROOT-APPENDER"/>
<appender-ref ref="ERROR-APPENDER"/>
</logger>
<root level="debug">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ROOT-APPENDER"/>
<appender-ref ref="ERROR-APPENDER"/>
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>404 Not Found</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<style>
body {
color: #A1A2AF;
font-family: "Microsoft YaHei", "Segoe UI", "Lucida Grande", Helvetica,
Arial, sans-serif;
}
.layout {
width: 1190px;
margin: 150px auto;
}
.err {
position: relative;
width: 568px;
height: 306px;
margin: 100px auto 40px;
background: url("/img/airplane-404page.jpg") no-repeat 21px 18px;
font-size: 14px;
}
.err_text {
position: absolute;
top: 246px;
left: 239px;
}
.err_back {
position: absolute;
top: 257px;
left: 353px;
width: 154px;
height: 38px;
text-indent: -999px;
overflow: hidden;
}
</style>
</head>
<body>
<div class="layout">
<div class="main">
<div class="err">
<p class="err_text">
非常抱歉,您访
<br>
问的页面不存在
</p>
<a href="/index.html" class="err_back">返回首页</a>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,4 +1,4 @@
x<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="ico_checkon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 42 42" style="enable-background:new 0 0 42 42;" xml:space="preserve">

Before

Width:  |  Height:  |  Size: 665 B

After

Width:  |  Height:  |  Size: 664 B

View File

@@ -1,112 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>打卡</title>
<link rel="stylesheet" href="./normalize.min.css">
<link rel="stylesheet" href="./style.css">
</head>
<body><!--partial:index.partial.html-->
<div class="job">
<div class="header">
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path xmlns="http://www.w3.org/2000/svg"
d="M512 503.5H381.7a48 48 0 01-45.3-32.1L265 268.1l-9-25.5 2.7-124.6L338.2 8.5l23.5 67.1L512 503.5z"
fill="#0473ff" data-original="#28b446"/>
<path xmlns="http://www.w3.org/2000/svg" fill="#0473ff" data-original="#219b38"
d="M361.7 75.6L265 268.1l-9-25.5 2.7-124.6L338.2 8.5z"/>
<path xmlns="http://www.w3.org/2000/svg"
d="M338.2 8.5l-82.2 234-80.4 228.9a48 48 0 01-45.3 32.1H0l173.8-495h164.4z" fill="#0473ff"
data-original="#518ef8"/>
</svg>
打卡
</div>
<div class="user-settings">
<div class="dark-light">
<svg viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"></path>
</svg>
</div>
<img class="user-profile" id="user-head" src="img/tx.png" alt="">
<div id="user-div">
<div id="username">未登陆</div>
<span id="login-span"><div class="alert-inline"><div><input class="login-input" id="employee-number"
type="text" placeholder="输入工号"></div><div><button
class="random-buttons" id="login">登陆</button></div></div></span>
<div id="logout-div" style="display: none">
<button class="random-buttons" id="logout">登出</button>
</div>
</div>
</div>
</div>
<div class="dialog" style="display: none">
<div class="alert">
<div class="job-card-title" id="sign-date"><span style="font-size: 24px"></span></div>
<div class="alert-inline">
<div><input id="sign-begin-time" type="text" placeholder="输入上班时间"></div>
<div>
<button class="random-buttons" id="sign-begin-random">随机</button>
</div>
</div>
<div class="alert-inline">
<div><input id="sign-end-time" type="text" placeholder="输入下班时间"></div>
<div>
<button class="random-buttons" id="sign-end-random">随机</button>
</div>
</div>
<button class="search-buttons card-buttons begin-buttons" id="save-sign">保存</button>
</div>
</div>
<div class="login_dialog" style="display: none">
<div class="alert">
<div class="job-card-title" ><span style="font-size: 24px">打卡信息</span></div>
<div class="alert-inline">
<div>
<label for="_user_input">用 户 名 : </label>
<input id="_user_input" readonly="readonly" type="text">
</div>
</div>
<div class="alert-inline">
<div>
<label for="_project_id_input">项 目 I D: </label>
<input id="_project_id_input" readonly="readonly" type="text">
</div>
</div>
<div class="alert-inline">
<div>
<label for="_project_name_input">项目名称: </label>
<input id="_project_name_input" readonly="readonly" type="text">
</div>
</div>
<div class="alert-inline">
<div>
<label for="_area_input">打卡地区: </label>
<input id="_area_input" readonly="readonly" type="text">
</div>
</div>
<div class="alert-inline">
<div>
<label for="_device_input">设备信息: </label>
<input id="_device_input" readonly="readonly" type="text">
</div>
</div>
</div>
</div>
<div class="wrapper">
<div class="main-container">
<div class="searched-jobs">
<div class="searched-bar">
<div class="searched-show">打卡记录</div>
<!--<button class="search-button">餐补<span class="canbu-span"></span></button>--></div>
<div class="job-cards"></div>
</div>
</div>
</div>
<script src="./coco-message.js"></script>
<script src="./jquery.min.js"></script>
<script src="./script.js"></script>
</div>
</body>
</html>

View File

@@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>打卡</title><link rel="stylesheet"href="./normalize.min.css"><link rel="stylesheet"href="./style.css"></head><body><!--partial:index.partial.html--><div class="job"><div class="header"><div class="logo"><svg xmlns="http://www.w3.org/2000/svg"viewBox="0 0 512 512"><path xmlns="http://www.w3.org/2000/svg"d="M512 503.5H381.7a48 48 0 01-45.3-32.1L265 268.1l-9-25.5 2.7-124.6L338.2 8.5l23.5 67.1L512 503.5z"fill="#0473ff"data-original="#28b446"/><path xmlns="http://www.w3.org/2000/svg"fill="#0473ff"data-original="#219b38"d="M361.7 75.6L265 268.1l-9-25.5 2.7-124.6L338.2 8.5z"/><path xmlns="http://www.w3.org/2000/svg"d="M338.2 8.5l-82.2 234-80.4 228.9a48 48 0 01-45.3 32.1H0l173.8-495h164.4z"fill="#0473ff"data-original="#518ef8"/></svg>打卡</div><div class="user-settings"><div class="dark-light"><svg viewBox="0 0 24 24"stroke="currentColor"stroke-width="1.5"fill="none"stroke-linecap="round"stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"></path></svg></div><img class="user-profile"id="user-head"src="img/tx.png"alt=""><div id="user-div"><div id="username">未登陆</div><span id="login-span"><div class="alert-inline"><div><input class="login-input"id="employee-number"type="text"placeholder="输入工号"></div><div><button class="random-buttons"id="login">登陆</button></div></div></span><div id="logout-div"style="display: none"><button class="random-buttons"id="logout">登出</button></div></div></div></div><div class="dialog"style="display: none"><div class="alert"><div class="job-card-title"id="sign-date"><span style="font-size: 24px"></span></div><div class="alert-inline"><div><input id="sign-begin-time"type="text"placeholder="输入上班时间"></div><div><button class="random-buttons"id="sign-begin-random">随机</button></div></div><div class="alert-inline"><div><input id="sign-end-time"type="text"placeholder="输入下班时间"></div><div><button class="random-buttons"id="sign-end-random">随机</button></div></div><button class="search-buttons card-buttons begin-buttons"id="save-sign">保存</button></div></div><div class="login_dialog"style="display: none"><div class="alert"><div class="job-card-title"><span style="font-size: 24px">打卡信息</span></div><div class="alert-inline"><div><label for="_user_input"> 户 名 :</label><input id="_user_input"readonly="readonly"type="text"></div></div><div class="alert-inline"><div><label for="_project_id_input">I D:</label><input id="_project_id_input"readonly="readonly"type="text"></div></div><div class="alert-inline"><div><label for="_project_name_input">项目名称:</label><input id="_project_name_input"readonly="readonly"type="text"></div></div><div class="alert-inline"><div><label for="_area_input">打卡地区:</label><input id="_area_input"readonly="readonly"type="text"></div></div><div class="alert-inline"><div><label for="_device_input">设备信息:</label><input id="_device_input"readonly="readonly"type="text"></div></div></div></div><div class="wrapper"><div class="main-container"><div class="searched-jobs"><div class="searched-bar"><div class="searched-show">打卡记录</div><!--<button class="search-button">餐补<span class="canbu-span"></span></button>--></div><div class="job-cards"></div></div></div></div><script src="./coco-message.js"></script><script src="./jquery.min.js"></script><script src="./script.js"></script></div></body></html>
<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1" /><title>打卡</title><link rel="stylesheet"href="./normalize.min.css"><link rel="stylesheet"href="./style.css"></head><body class="dark-mode"><!--partial:index.partial.html--><div class="job"><div class="header"><div class="logo"><svg xmlns="http://www.w3.org/2000/svg"viewBox="0 0 512 512"><path xmlns="http://www.w3.org/2000/svg"d="M512 503.5H381.7a48 48 0 01-45.3-32.1L265 268.1l-9-25.5 2.7-124.6L338.2 8.5l23.5 67.1L512 503.5z"fill="#0473ff"data-original="#28b446"/><path xmlns="http://www.w3.org/2000/svg"fill="#0473ff"data-original="#219b38"d="M361.7 75.6L265 268.1l-9-25.5 2.7-124.6L338.2 8.5z"/><path xmlns="http://www.w3.org/2000/svg"d="M338.2 8.5l-82.2 234-80.4 228.9a48 48 0 01-45.3 32.1H0l173.8-495h164.4z"fill="#0473ff"data-original="#518ef8"/></svg>打卡</div><div class="user-settings"><div class="dark-light"><svg viewBox="0 0 24 24"stroke="currentColor"stroke-width="1.5"fill="none"stroke-linecap="round"stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"></path></svg></div><img class="user-profile"id="user-head"src="img/tx.png"alt=""><div id="user-div"><div id="username">未登陆</div><span id="login-span"><div class="alert-inline"><div><input class="login-input"id="employee-number"type="text"placeholder="输入工号"></div><!--<div><input class="login-input"style="width: 280px"id="jsessionId"type="text"placeholder="输入SESSIONID"></div>--><div><div id="autodaka">自动打卡开关</div></div><div><div class="toggle toggle--switch"><input type="checkbox"id="toggle--switch"class="toggle--checkbox"><label class="toggle--btn"for="toggle--switch"><span class="toggle--feature"data-label-on="on"data-label-off="off"></span></label></div></div><div><button class="random-buttons"id="login">登陆</button></div></div></span><div id="logout-div"style="display: none"><button class="random-buttons"id="logout">登出</button></div></div></div></div><div class="dialog rainbow"style="display: none"><div class="alert"><div class="job-card-title"id="sign-date"><span style="font-size: 24px"></span></div><div class="alert-inline"><div><input id="sign-begin-time"type="text"placeholder="输入上班时间"></div><div><button class="random-buttons"id="sign-begin-random">随机</button></div></div><div class="alert-inline"><div><input id="sign-end-time"type="text"placeholder="输入下班时间"></div><div><button class="random-buttons"id="sign-end-random">随机</button></div></div><button class="search-buttons card-buttons begin-buttons"id="save-sign">保存</button></div></div><div class="userinfo_dialog rainbow"style="display: none"><div class="alert"><div class="job-card-title"><span style="font-size: 24px">打卡信息</span></div><div class="alert-inline"><div><label for="_user_input">户名:</label><input id="_user_input"readonly="readonly"type="text"></div></div><div class="alert-inline"><div><label for="_project_id_input">I D:</label><input id="_project_id_input"readonly="readonly"type="text"></div></div><div class="alert-inline"><div><label for="_project_name_input">项目名称:</label><input id="_project_name_input"readonly="readonly"type="text"></div></div><div class="alert-inline"><div><label for="_area_input">打卡地区:</label><input id="_area_input"readonly="readonly"type="text"></div></div><div class="alert-inline"><div><label for="_device_input">设备信息:</label><input id="_device_input"readonly="readonly"type="text"></div></div></div></div><div class="wrapper"><div class="main-container"><div class="searched-jobs"><div class="searched-bar"><div class="searched-show">打卡记录</div><!--<button class="search-button">餐补<span class="canbu-span"></span></button>--></div><div class="job-cards"></div></div></div></div><script src="./coco-message.js"></script><script src="./jquery.min.js"></script><script src="./script.js"></script></div></body></html>

View File

@@ -187,4 +187,6 @@ summary {
display: none
}
button,hr,input{overflow:visible}audio,canvas,progress,video{display:inline-block}progress,sub,sup{vertical-align:baseline}html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} menu,article,aside,details,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{}button,select{text-transform:none}[type=submit], [type=reset],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}/*# sourceMappingURL=normalize.min.css.map */
/*# sourceMappingURL=normalize.min.css.map */

View File

@@ -1,5 +1,4 @@
const toggleButton = document.querySelector(".dark-light");
toggleButton.addEventListener("click", () => {
document.body.classList.toggle("dark-mode");
});
@@ -18,6 +17,9 @@ document.querySelector('#logout').addEventListener("click", () => {
$('#username').text("未登陆");
$("#logout-div").hide();
$("#login-span").show();
$("#login").css("display", "block");
$("#jsessionId").css("display", "block");
$("#employee-number").css("display", "block");
})
@@ -25,15 +27,16 @@ let employeeNumberInput = document.querySelector('#employee-number');
if ($('#login-span').css('display') !== 'none') {
employeeNumberInput.focus()
}
employeeNumberInput.addEventListener("keypress", (e) => {
if (e.keyCode === 13) {
window.localStorage.setItem("employeeNo", employeeNumberInput.value);
loginAndLoadRecord();
}
})
//let jsessonIdInput = document.querySelector('#jsessionId');
let autoDaka = document.querySelector("#toggle--switch")
autoDaka.checked = window.localStorage.getItem("autodaka")
document.querySelector('#login').addEventListener("click", () => {
window.localStorage.setItem("employeeNo", employeeNumberInput.value);
// window.localStorage.setItem("jsessionId", jsessonIdInput.value);
window.localStorage.setItem("autodaka", autoDaka.checked);
loginAndLoadRecord();
})
@@ -41,10 +44,13 @@ function loginAndLoadRecord() {
$.ajax({
method: "get",
url: "/api/daka/username/" + window.localStorage.getItem("employeeNo"),
url: "/api/daka/username/" + window.localStorage.getItem("employeeNo") + "/" + window.localStorage.getItem("autodaka"),
success: function (result) {
if (result.success) {
$("#login-span").css("display", "none");
// $("#login-span").css("display", "none");
$("#login").css("display", "none");
//$("#jsessionId").css("display", "none");
$("#employee-number").css("display", "none");
$('#username').text(result.data.username);
window.localStorage.setItem("username", result.data.username);
$("#logout-div").css("display", "block");
@@ -55,18 +61,18 @@ function loginAndLoadRecord() {
$('#_project_name_input').val(result.data.projectname)
$('#_area_input').val(result.data.area)
$('#_device_input').val(result.data.device)
let loginDialog = $('.login_dialog')
loginDialog.show();
let userInfoDialog = $('.userinfo_dialog')
userInfoDialog.show();
setTimeout(
() => {
loginDialog.hide();
userInfoDialog.hide();
}, 3000
)
$('#user-head').off('click').on("click", () => {
if (loginDialog.css("display") === 'block') {
$('.login_dialog').hide()
if (userInfoDialog.css("display") === 'block') {
$('.userinfo_dialog').hide()
} else {
$('.login_dialog').show()
$('.userinfo_dialog').show()
}
})
loadDakaList();
@@ -75,7 +81,7 @@ function loginAndLoadRecord() {
cocoMessage.error("登陆失败!" + result.message, 3000);
}
},
error: function (e) {
error: function () {
cocoMessage.error("请求失败!", 3000);
}
})
@@ -99,7 +105,15 @@ function loadDakaList() {
dateBeginTimeMap[signlog.yearmonth] = (signlog.begin_time == null ? "未打卡" : signlog.begin_time.substr(11, 5));
dateEndTimeMap[signlog.yearmonth] = (signlog.end_time == null ? "未打卡" : signlog.end_time.substr(11, 5));
index++;
$('.job-cards').append('<div class="job-card">' +
let beginWarn = (signlog.begin_time != null && signlog.begin_time.substr(11, 5) > '09:30')
let endWarn = (signlog.end_time != null && signlog.end_time.substr(11, 5) < '18:30')
let buqianWarn = (signlog.actual_area_begin === '补签卡' || signlog.actual_area_end === '补签卡')
let weidakaWarn = (signlog.begin_time == null || signlog.end_time == null)
$('.job-cards').append('<div class="job-card ' +
(beginWarn || endWarn || weidakaWarn || buqianWarn ? 'rainbow' : '') + '" '+
// (beginWarn || endWarn || weidakaWarn || buqianWarn ? 'style="border: 8px; border-color: #ff8611; border-radius: 8px; border-style: solid;"' : "") +
'>' +
'<div class="job-card-title">' + (nowDate === signlog.yearmonth ? "今天" : signlog.yearmonth) + '</div>' +
'<div class="job-card-subtitle">' +
signlog.area_id +
@@ -107,13 +121,23 @@ function loadDakaList() {
'<div class="job-detail-buttons">' +
'<button class="search-buttons detail-button">' + (signlog.begin_time == null ? "未打卡" : signlog.begin_time.substr(11, 5)) + '</button>' +
'<button class="search-buttons detail-button">' + (signlog.end_time == null ? "未打卡" : signlog.end_time.substr(11, 5)) + '</button>' +
(signlog.end_time != null && signlog.end_time.substr(11, 5) > "20:30" ? '<button class="search-buttons detail-button">餐补</button>' : '') +
(signlog.autoDakaBeginTime == null ? "" : '<button class="search-buttons detail-button" style="font-size: 10px">定时上班卡时间: ' + signlog.autoDakaBeginTime + '</button>') +
(signlog.autoDakaEndTime == null ? "" : '<button class="search-buttons detail-button" style="font-size: 10px">定时下班卡时间: ' + signlog.autoDakaEndTime + '</button>') +
// (signlog.end_time != null && signlog.end_time.substr(11, 5) > "21:00" ? '<button class="search-buttons detail-button">餐补</button>' : '') +
'</div>' +
(index <= 3 ? '<div class="job-card-buttons">' +
'<button class="search-buttons card-buttons daka-buttons"' +
'">打卡</button>' +
'<button class="search-buttons card-buttons auto-daka-buttons">自动补卡</button>' +
'</div>' : '') +
(index > 3 ?
'<div class="job-card-subtitle">' +
signlog.actual_area_begin +
'</div>' +
(signlog.actual_area_begin !== signlog.actual_area_end ? '<div class="job-card-subtitle">' +
signlog.actual_area_end +
'</div>' : '')
: '') +
'</div>'
);
}
@@ -122,12 +146,13 @@ function loadDakaList() {
bindRandomBeginTime();
bindRandomEndTime();
bindSaveDakaTime();
bindAutoDakaToggle();
} else {
cocoMessage.error("加载打卡记录失败!" + result.message, 3000);
}
closeLoading()
},
error: function (e) {
error: function () {
cocoMessage.error("请求失败!", 3000);
closeLoading()
}
@@ -200,6 +225,26 @@ function bindSaveDakaTime() {
})
}
function bindAutoDakaToggle() {
document.querySelectorAll("#toggle--switch").forEach((dakaButton) => {
$(dakaButton).off('change').on('change', function () {
window.localStorage.setItem("autodaka", this.checked);
$.ajax({
method: "get",
url: "/api/daka/auto/" + window.localStorage.getItem("employeeNo") + "/" + autoDaka.checked,
success: function (result) {
if (!result.success) {
cocoMessage.error("登陆失败!" + result.message, 3000);
}
},
error: function () {
cocoMessage.error("请求失败!", 3000);
}
})
})
})
}
function saveDakaTime(signDate, beginTime, endTime) {
let messageType = 0;
let beginTimeMessage = dateBeginTimeMap[signDate] !== beginTime;
@@ -238,7 +283,7 @@ function saveDakaTime(signDate, beginTime, endTime) {
}
}
},
error: function (e) {
error: function () {
cocoMessage.error("请求失败!", 3000);
}
});
@@ -268,7 +313,7 @@ function saveDakaTime(signDate, beginTime, endTime) {
}
}
},
error: function (e) {
error: function () {
cocoMessage.error("请求失败!", 3000);
}
});
@@ -284,10 +329,10 @@ function randomBeginTime() {
}
function randomEndTime() {
let hourArray = [20, 21, 22, 23, 20, 21, 22, 20, 20, 21];
let hourArray = [18, 19, 20, 21, 22, 23, 20, 21, 22, 20, 20, 21];
let randomHour = hourArray[Math.round(Math.random() * 9)];
let randomMinute = 0;
if (randomHour === 20) {
let randomMinute;
if (randomHour === 18) {
randomMinute = Math.round(Math.random() * 30) + 29;
} else {
randomMinute = Math.round(Math.random() * 59);
@@ -304,7 +349,7 @@ $(document).click(function(e) {
if (!$target.is('.dialog *') && !$target.is('.daka-buttons')) {
$('.dialog').hide();
}
if (!$target.is('.login_dialog *') && !$target.is('#user-head')) {
$('.login_dialog').hide();
if (!$target.is('.userinfo_dialog *') && !$target.is('#user-head')) {
$('.userinfo_dialog').hide();
}
})

View File

@@ -11,6 +11,7 @@
--border-color: #d8d8d8;
--alert-bg-color: #e8f2ff;
--subtitle-color: #83838e;
--subtext-color: #d5d5d5;
--inactive-color: #f0f0f0;
--placeholder-color: #9b9ba5;
--time-button: #fc5757;
@@ -95,7 +96,7 @@ body {
}
.dark-mode .detail-button {
background-color: var(--inactive-color);
color: var(--subtitle-color);
color: var(--subtext-color);
}
.job {
@@ -188,6 +189,7 @@ body {
}
.user-profile {
display: none;
width: 32px;
height: 32px;
border-radius: 50%;
@@ -208,6 +210,7 @@ body {
scroll-behavior: smooth;
padding: 30px 40px;
overflow: auto;
scrollbar-width: none;
}
.search-menu {
@@ -291,7 +294,7 @@ body {
color: var(--button-color);
background-color: var(--active-color);
padding: 8px 10px;
border-radius: 0 8px 8px 0;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
}
@@ -361,41 +364,56 @@ body {
margin-left: -100px;
margin-top: -100px;
position: absolute;
box-shadow: 0px 2px 4px hsl(0 0% 0% / 25%);
animation: border-angle-rotate 2s infinite linear;
border: 0.5rem solid transparent;
border-radius: 16px;
}
.login_dialog {
.userinfo_dialog {
width: 50%;
opacity: 1;
visibility: visible;
z-index: 101;
left: 33%;
left: 50%;
top: 40%;
margin-left: -100px;
margin-top: -100px;
position: absolute;
margin: auto 0;
transform: translate(-50%, -50%);
box-shadow: 0px 2px 4px hsl(0 0% 0% / 25%);
animation: border-angle-rotate 2s infinite linear;
border: 0.5rem solid transparent;
border-radius: 16px;
}
.login_dialog > div > .alert-inline {
.userinfo_dialog > div > .alert-inline {
display: flex;
padding: 2px 0;
}
.login_dialog > div > div > div > label {
.userinfo_dialog > div > div > div > label {
font-weight: 600;
font-size: 16px;
align-self: center;
padding-right: 10px;
}
body > div > div.login_dialog > div > div > div {
display: inline-flex;
body > div > div.userinfo_dialog > div > div > div {
display: inline-grid;
grid-template-columns: 20% 80%;
width: 100%;
}
#username {
padding: 13px;
}
#autodaka {
padding: 11px;
}
.alert {
background-color: var(--alert-bg-color);
padding: 24px 24px;
@@ -430,8 +448,12 @@ body > div > div.login_dialog > div > div > div {
color: var(--body-color)
}
body > div > div.login_dialog > div > div > div input {
width: 85%;
#username {
/*width: 64px;*/
}
body > div > div.userinfo_dialog > div > div > div input {
width: 100%;
padding: 10px;
display: block;
border-radius: 8px 8px 8px 8px;
@@ -444,8 +466,9 @@ body > div > div.login_dialog > div > div > div input {
.login-input {
width: 85px;
padding: 10px;
margin-right: 8px;
display: block;
border-radius: 8px 0 0 8px;
border-radius: 8px;
background-color: var(--header-bg-color);
border: none;
font-size: 13px;
@@ -460,7 +483,7 @@ body > div > div.login_dialog > div > div > div input {
border-radius: 6px;
font-size: 13px;
font-weight: 600;
margin-top: 14px;
margin-top: 6px;
}
.job-wrapper {
@@ -588,37 +611,92 @@ body > div > div.login_dialog > div > div > div input {
.job-cards {
padding-top: 20px;
padding-right: 80px;
/*padding-right: 80px;*/
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(6, 1fr);
grid-column-gap: 25px;
grid-row-gap: 25px;
-webkit-animation: slideY .6s both;
animation: slideY .6s both;
}
@media screen and (max-width: 1212px) {
@media screen and (max-width: 480px) {
.job-cards {
grid-template-columns: repeat(1, 1fr);
}
#autodaka {
display: none;
}
@media screen and (max-width: 930px) {
#user-head {
display: none;
}
.userinfo_dialog {
width: 88% !important;
}
.wrapper {
padding: 0 20px;
}
}
@media screen and (max-width: 1080px) {
.job-cards {
grid-template-columns: repeat(1, 1fr);
grid-template-columns: repeat(2, 1fr);
}
.userinfo_dialog {
width: 60%;
}
}
@property --border-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes border-angle-rotate {
from { --border-angle: 0deg; }
to { --border-angle: 360deg; }
}
.rainbow {
background: linear-gradient(var(--header-bg-color), var(--header-bg-color)) padding-box,
conic-gradient(
from var(--border-angle),
oklch(100% 100% 0deg),
oklch(100% 100% 45deg),
oklch(100% 100% 90deg),
oklch(100% 100% 135deg),
oklch(100% 100% 180deg),
oklch(100% 100% 225deg),
oklch(100% 100% 270deg),
oklch(100% 100% 315deg),
oklch(100% 100% 360deg)
)
border-box;
}
.job-card {
padding: 20px 16px;
background-color: var(--header-bg-color);
border-radius: 8px;
border-radius: 16px;
cursor: pointer;
-webkit-transition: .2s;
transition: .2s;
box-shadow: 0px 2px 4px hsl(0 0% 0% / 25%);
animation: border-angle-rotate 2s infinite linear;
border: 0.5rem solid transparent;
}
.job-card:hover {
-webkit-transform: scale(1.02);
transform: scale(1.02);
}
.job-card svg {
width: 46px;
padding: 10px;
@@ -683,19 +761,29 @@ body > div > div.login_dialog > div > div > div input {
padding: 6px 8px;
border-radius: 4px;
}
.detail-button + .detail-button {
margin-left: 4px;
/*.detail-button + .detail-button {*/
/* margin-left: 4px;*/
/*}*/
.job-detail-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 10px;
grid-row-gap: 4px;
}
.job-card-buttons {
display: -webkit-box;
display: flex;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 10px;
grid-row-gap: 25px;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: justify;
justify-content: space-between;
width: 100%;
margin-top: 4px;
flex-flow: column;
}
#logout-div .random-buttons {
border-radius: 8px;
@@ -708,14 +796,15 @@ body > div > div.login_dialog > div > div > div input {
.card-buttons,
.card-buttons-msg {
padding: 10px;
width: 100%;
min-width: 46%;
font-size: 12px;
cursor: pointer;
}
.card-buttons {
margin-right: 12px;
}
/*.card-buttons {*/
/* margin-right: 4%;*/
/* margin-left: 4%;*/
/*}*/
.card-buttons-msg {
background-color: var(--inactive-color);
color: var(--subtitle-color);
@@ -1114,3 +1203,173 @@ body > div > div.login_dialog > div > div > div input {
padding: 0 20px;
}
}
@import url(https://fonts.googleapis.com/css?family=Francois+One);
@import url(https://fonts.googleapis.com/css?family=PT+Sans);
@font-face {
font-family: 'Audiowide';
font-style: normal;
font-weight: 400;
src: local("Audiowide"), local("Audiowide-Regular"), url(https://themes.googleusercontent.com/static/fonts/audiowide/v2/8XtYtNKEyyZh481XVWfVOj8E0i7KZn-EPnyo3HZu7kw.woff) format("woff");
}
body {
font-size: 62.5%;
background-color: #fff;
border: 1px solid #333;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
h1, h2, h3 {
font-family: 'PT Sans', sans-serif;
text-transform: uppercase;
}
h1 {
font-size: 2.4em;
background-color: #292929;
text-align: center;
padding: 20px;
margin: 0;
color: #fff;
}
h1 a {
display: block;
margin-top: 10px;
text-transform: none;
color: #aaa;
font-size: 16px;
text-decoration: none;
}
.headingOuter {
background: #f1f1f1;
text-align: center;
margin-top: 50px;
margin-bottom: 0;
padding: 10px;
border-top: 1px solid #333;
border-bottom: 1px solid #333;
font-size: 1.6em;
}
.headingOuter:first-child {
margin-top: 0;
}
h1 + .headingOuter {
margin-top: 0;
}
/* =====================================================
Some defaults across all toggle demos
===================================================== */
.toggle {
display: block;
text-align: center;
user-select: none;
margin-right: 5px;
}
.toggle--checkbox {
display: none;
}
.toggle--btn {
display: block;
margin: 0 auto;
font-size: 1.4em;
transition: all 350ms ease-in;
}
.toggle--btn:hover {
cursor: pointer;
}
.toggle--btn, .toggle--btn:before, .toggle--btn:after,
.toggle--checkbox,
.toggle--checkbox:before,
.toggle--checkbox:after,
.toggle--feature,
.toggle--feature:before,
.toggle--feature:after {
transition: all 250ms ease-in;
}
.toggle--btn:before, .toggle--btn:after,
.toggle--checkbox:before,
.toggle--checkbox:after,
.toggle--feature:before,
.toggle--feature:after {
content: '';
display: block;
}
/* =====================================================
Toggle - switch stylee
===================================================== */
.toggle--switch .toggle--btn {
position: relative;
width: 100px;
height: 35px;
font-family: 'PT Sans', Sans Serif;
text-transform: uppercase;
color: #fff;
background: linear-gradient(90deg, #a4bf4d 0%, #a4bf4d 50%, #ca5046 50%, #ca5046 200%);
background-position: -80px 0;
background-size: 200% 100%;
box-shadow: inset 0 0px 22px -8px #111;
}
.toggle--switch .toggle--btn, .toggle--switch .toggle--btn:before {
border-radius: 4px;
}
.toggle--switch .toggle--btn:before {
display: block;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: 52px;
height: 41px;
border: 2px solid #202027;
background-image: linear-gradient(90deg, transparent 50%, rgba(255, 255, 255, 0.15) 100%);
background-color: #2b2e3a;
background-size: 5px 5px;
text-indent: -100%;
}
.toggle--switch .toggle--feature {
position: relative;
display: block;
overflow: hidden;
height: 44px;
text-shadow: 0 1px 2px #666;
}
.toggle--switch .toggle--feature:before, .toggle--switch .toggle--feature:after {
position: absolute;
top: 45%;
transform: translateY(-50%);
}
.toggle--switch .toggle--feature:before {
content: attr(data-label-on);
left: -60%;
}
.toggle--switch .toggle--feature:after {
content: attr(data-label-off);
right: 9%;
}
.toggle--switch .toggle--checkbox:checked + .toggle--btn {
background-position: 0 0;
}
.toggle--switch .toggle--checkbox:checked + .toggle--btn:before {
left: calc(100% - 52px);
}
.toggle--switch .toggle--checkbox:checked + .toggle--btn .toggle--feature:before {
left: 10%;
}
.toggle--switch .toggle--checkbox:checked + .toggle--btn .toggle--feature:after {
right: -60%;
}
.coco-msg-stage {
top: 80px !important;
}

38
src/test/kotlin/Test.kt Normal file
View File

@@ -0,0 +1,38 @@
import java.util.*
fun main() {
var ti = TreeNode(2)
var v = ti.`val`
ti.right = TreeNode(4)
ti.left = TreeNode(3)
ti.left!!.left = TreeNode(1)
println(inorderTraversal(ti))
}
fun inorderTraversal(root: TreeNode?): List<Int> {
var copyroot = root
val list: MutableList<Int> = ArrayList()
val stack = Stack<TreeNode>()
while (copyroot != null || !stack.isEmpty()) {
while (copyroot != null) {
stack.push(copyroot)
copyroot = copyroot.left
}
copyroot = stack.pop()
list.add(copyroot.`val`)
copyroot = copyroot.right
}
return list
}
class TreeNode(var `val`: Int) {
var left: TreeNode? = null
var right: TreeNode? = null
}