Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
涂亚平
/
subsidy
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit b06c404d
authored
Feb 17, 2023
by
涂亚平
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
人社局测试
1 parent
27a4934c
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
86 additions
and
284 deletions
src/main/java/com/subsidy/common/interceptor/AuthenticationInterceptor.java
src/main/java/com/subsidy/jobs/RenSheJuJob.java
src/main/java/com/subsidy/mapper/RenSheJuMapper.java
src/main/java/com/subsidy/service/impl/MemberServiceImpl.java
src/main/java/com/subsidy/service/impl/RenSheJuServiceImpl.java
src/main/java/com/subsidy/util/RenSheJuConstant.java
src/main/java/com/subsidy/util/websocket/ReConnectWebSocketClient.java
src/main/java/com/subsidy/vo/renshe/ClassImageChecksVO.java
src/main/java/com/subsidy/vo/renshe/DailyActivitiesVO.java
src/main/resources/mapper/RenSheJuMapper.xml
src/main/java/com/subsidy/common/interceptor/AuthenticationInterceptor.java
View file @
b06c404
...
@@ -11,9 +11,11 @@ import com.subsidy.common.exception.HttpException;
...
@@ -11,9 +11,11 @@ import com.subsidy.common.exception.HttpException;
import
com.subsidy.mapper.AdministerMapper
;
import
com.subsidy.mapper.AdministerMapper
;
import
com.subsidy.mapper.MemberMapper
;
import
com.subsidy.mapper.MemberMapper
;
import
com.subsidy.mapper.MemberTokensMapper
;
import
com.subsidy.mapper.MemberTokensMapper
;
import
com.subsidy.mapper.OprMemDictMapper
;
import
com.subsidy.model.AdministerDO
;
import
com.subsidy.model.AdministerDO
;
import
com.subsidy.model.MemberDO
;
import
com.subsidy.model.MemberDO
;
import
com.subsidy.model.MemberTokensDO
;
import
com.subsidy.model.MemberTokensDO
;
import
com.subsidy.model.OprMemDictDO
;
import
com.subsidy.util.ConstantUtils
;
import
com.subsidy.util.ConstantUtils
;
import
com.subsidy.util.JwtUtil
;
import
com.subsidy.util.JwtUtil
;
import
com.subsidy.util.Localstorage
;
import
com.subsidy.util.Localstorage
;
...
@@ -55,6 +57,9 @@ public class AuthenticationInterceptor implements HandlerInterceptor {
...
@@ -55,6 +57,9 @@ public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
@Autowired
private
MemberTokensMapper
memberTokensMapper
;
private
MemberTokensMapper
memberTokensMapper
;
@Autowired
private
OprMemDictMapper
oprMemDictMapper
;
@Override
@Override
@CrossOrigin
()
@CrossOrigin
()
public
boolean
preHandle
(
HttpServletRequest
request
,
HttpServletResponse
response
,
Object
handler
)
{
public
boolean
preHandle
(
HttpServletRequest
request
,
HttpServletResponse
response
,
Object
handler
)
{
...
@@ -67,13 +72,6 @@ public class AuthenticationInterceptor implements HandlerInterceptor {
...
@@ -67,13 +72,6 @@ public class AuthenticationInterceptor implements HandlerInterceptor {
Method
method
=
handlerMethod
.
getMethod
();
Method
method
=
handlerMethod
.
getMethod
();
TimeRequired
timeRequired
=
method
.
getAnnotation
(
TimeRequired
.
class
);
TimeRequired
timeRequired
=
method
.
getAnnotation
(
TimeRequired
.
class
);
if
(
timeRequired
!=
null
){
Calendar
calendar
=
Calendar
.
getInstance
();
int
hour
=
calendar
.
get
(
Calendar
.
HOUR_OF_DAY
);
if
(
hour
<
6
){
throw
new
HttpException
(
17001
);
}
}
LoginRequired
methodAnnotation
=
method
.
getAnnotation
(
LoginRequired
.
class
);
LoginRequired
methodAnnotation
=
method
.
getAnnotation
(
LoginRequired
.
class
);
if
(
methodAnnotation
!=
null
)
{
if
(
methodAnnotation
!=
null
)
{
...
@@ -100,6 +98,19 @@ public class AuthenticationInterceptor implements HandlerInterceptor {
...
@@ -100,6 +98,19 @@ public class AuthenticationInterceptor implements HandlerInterceptor {
}
}
}
else
if
(
ConstantUtils
.
MOBILE_TERMINATE
.
equals
(
type
))
{
}
else
if
(
ConstantUtils
.
MOBILE_TERMINATE
.
equals
(
type
))
{
MemberDO
memberDO
=
memberMapper
.
selectById
(
claimMap
.
get
(
"id"
).
asLong
());
MemberDO
memberDO
=
memberMapper
.
selectById
(
claimMap
.
get
(
"id"
).
asLong
());
if
(
timeRequired
!=
null
){
Calendar
calendar
=
Calendar
.
getInstance
();
int
hour
=
calendar
.
get
(
Calendar
.
HOUR_OF_DAY
);
if
(
hour
<
6
){
OprMemDictDO
oprMemDictDO
=
new
OprMemDictDO
();
oprMemDictDO
.
setUserId
(
memberDO
.
getId
());
oprMemDictDO
.
setOprType
(
"登出"
);
oprMemDictDO
.
setResult
(
1
);
oprMemDictMapper
.
insert
(
oprMemDictDO
);
throw
new
HttpException
(
17001
);
}
}
if
(
memberDO
!=
null
)
{
if
(
memberDO
!=
null
)
{
Localstorage
.
setUser
(
memberDO
);
Localstorage
.
setUser
(
memberDO
);
return
true
;
return
true
;
...
...
src/main/java/com/subsidy/jobs/RenSheJuJob.java
View file @
b06c404
package
com
.
subsidy
.
jobs
;
package
com
.
subsidy
.
jobs
;
import
com.alibaba.fastjson.JSONObject
;
import
com.subsidy.common.ResponseData
;
import
com.subsidy.service.RenSheJuService
;
import
com.subsidy.service.RenSheJuService
;
import
com.subsidy.service.RenshejuHistoryService
;
import
com.subsidy.service.RenshejuHistoryService
;
import
com.subsidy.service.impl.RenshejuHistoryServiceImpl
;
import
com.subsidy.service.impl.RenshejuHistoryServiceImpl
;
import
com.subsidy.util.websocket.WebSocketUtil
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.scheduling.annotation.Scheduled
;
import
org.springframework.scheduling.annotation.Scheduled
;
import
org.springframework.stereotype.Component
;
import
org.springframework.stereotype.Component
;
import
org.springframework.web.socket.TextMessage
;
import
org.springframework.web.socket.WebSocketSession
;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.util.concurrent.ConcurrentHashMap
;
/**
/**
* 人社局数据对接
* 人社局数据对接
...
@@ -83,5 +89,12 @@ public class RenSheJuJob {
...
@@ -83,5 +89,12 @@ public class RenSheJuJob {
// renSheJuService.getErrorClass();
// renSheJuService.getErrorClass();
// }
// }
@Scheduled
(
cron
=
"00 58 23 * * ?"
)
public
void
cancelLogin
()
throws
IOException
{
ConcurrentHashMap
<
Long
,
WebSocketSession
>
webSocketMap
=
WebSocketUtil
.
webSocketMap
;
for
(
Long
key
:
webSocketMap
.
keySet
()){
webSocketMap
.
get
(
key
).
sendMessage
(
new
TextMessage
(
JSONObject
.
toJSONString
(
ResponseData
.
generateCreatedResponse
(
17001
))));
}
}
}
}
src/main/java/com/subsidy/mapper/RenSheJuMapper.java
View file @
b06c404
...
@@ -98,7 +98,7 @@ public interface RenSheJuMapper {
...
@@ -98,7 +98,7 @@ public interface RenSheJuMapper {
List
<
Long
>
checkClassIds
();
List
<
Long
>
checkClassIds
();
/**
/**
* 查看某个班级的成员做过的人脸识别记录
* 查看某个班级的成员做过的人脸识别记录
弃用
*/
*/
List
<
ClassImageChecksVO
>
classImageChecks
(
Long
classId
);
List
<
ClassImageChecksVO
>
classImageChecks
(
Long
classId
);
...
@@ -107,5 +107,9 @@ public interface RenSheJuMapper {
...
@@ -107,5 +107,9 @@ public interface RenSheJuMapper {
*/
*/
List
<
ClassImageChecksVO
>
passExamCheck
(
Long
classId
);
List
<
ClassImageChecksVO
>
passExamCheck
(
Long
classId
);
/**
* 进入到课程第二次做人脸识别
*/
List
<
ClassImageChecksVO
>
studyCheck
(
Long
classId
);
}
}
src/main/java/com/subsidy/service/impl/MemberServiceImpl.java
View file @
b06c404
...
@@ -1157,6 +1157,7 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, MemberDO> imple
...
@@ -1157,6 +1157,7 @@ public class MemberServiceImpl extends ServiceImpl<MemberMapper, MemberDO> imple
return
polyvInfoVO
;
return
polyvInfoVO
;
}
}
@Transactional
(
rollbackFor
=
Exception
.
class
)
public
String
logout
(
MemberDO
memberDO
)
{
public
String
logout
(
MemberDO
memberDO
)
{
memberTokensMapper
.
delete
(
new
QueryWrapper
<
MemberTokensDO
>()
memberTokensMapper
.
delete
(
new
QueryWrapper
<
MemberTokensDO
>()
.
lambda
()
.
lambda
()
...
...
src/main/java/com/subsidy/service/impl/RenSheJuServiceImpl.java
View file @
b06c404
This diff is collapsed.
Click to expand it.
src/main/java/com/subsidy/util/RenSheJuConstant.java
View file @
b06c404
...
@@ -47,5 +47,23 @@ public class RenSheJuConstant {
...
@@ -47,5 +47,23 @@ public class RenSheJuConstant {
*/
*/
public
static
final
String
POST_10
=
"POST_10_clear"
;
public
static
final
String
POST_10
=
"POST_10_clear"
;
/**
* 调取失败
*/
public
static
final
String
API_ERROR
=
"调取失败"
;
/**
* 当天没数据
*/
public
static
final
String
NO_DATA
=
"NO_DATA"
;
/**
* 推送失败
*/
public
static
final
String
PUSH_FAIL
=
"推送失败"
;
/**
* 推送成功
*/
public
static
final
String
PUSH_SUCCESS
=
"推送成功"
;
}
}
src/main/java/com/subsidy/util/websocket/ReConnectWebSocketClient.java
deleted
100644 → 0
View file @
27a4934
package
com
.
subsidy
.
util
.
websocket
;
import
cn.hutool.core.thread.ThreadUtil
;
import
cn.hutool.core.util.StrUtil
;
import
lombok.extern.slf4j.Slf4j
;
import
org.java_websocket.WebSocket
;
import
org.java_websocket.client.WebSocketClient
;
import
org.java_websocket.framing.Framedata
;
import
org.java_websocket.handshake.ServerHandshake
;
import
javax.net.ssl.*
;
import
java.net.Socket
;
import
java.net.URI
;
import
java.nio.ByteBuffer
;
import
java.security.cert.CertificateException
;
import
java.security.cert.X509Certificate
;
import
java.util.concurrent.TimeUnit
;
import
java.util.concurrent.atomic.AtomicBoolean
;
import
java.util.concurrent.atomic.AtomicInteger
;
import
java.util.function.Consumer
;
/** @Author huyi @Date 2021/10/15 20:03 @Description: 重连websocket客户端 */
@Slf4j
public
class
ReConnectWebSocketClient
{
/** 字符串消息回调 */
private
Consumer
<
String
>
msgStr
;
/** 字节流消息回调 */
private
Consumer
<
ByteBuffer
>
msgByte
;
/** 异常回调 */
private
Consumer
<
Exception
>
error
;
/** 连接标识 */
private
String
key
;
/** ws服务端连接 */
private
URI
serverUri
;
/** 尝试重连标识 */
private
AtomicBoolean
tryReconnect
;
/** 需要ping标识 */
private
AtomicBoolean
needPing
;
/** websocket连接实体 */
private
WebSocketClient
webSocketClient
;
/** 重连次数 */
private
AtomicInteger
reConnectTimes
;
/** 连接结束标识 */
private
AtomicBoolean
end
;
/** 连接后初始发送报文,这里也可以不需要,如果服务端主动断开连接,重连后可以继续推送报文的话。 */
private
String
initReConnectReq
;
/** 结束回调 */
private
Consumer
<
String
>
endConsumer
;
public
ReConnectWebSocketClient
(
URI
serverUri
,
String
key
,
Consumer
<
String
>
msgStr
,
Consumer
<
ByteBuffer
>
msgByte
,
Consumer
<
Exception
>
error
)
{
this
.
msgStr
=
msgStr
;
this
.
msgByte
=
msgByte
;
this
.
error
=
error
;
this
.
key
=
key
;
this
.
serverUri
=
serverUri
;
this
.
tryReconnect
=
new
AtomicBoolean
(
false
);
this
.
needPing
=
new
AtomicBoolean
(
true
);
this
.
reConnectTimes
=
new
AtomicInteger
(
0
);
this
.
end
=
new
AtomicBoolean
(
false
);
this
.
endConsumer
=
this
::
close
;
init
();
}
/** 初始化连接 */
public
void
init
()
{
// 创建连接
createWebSocketClient
();
// ping线程
circlePing
();
}
private
void
needReconnect
()
throws
Exception
{
ThreadUtil
.
sleep
(
10
,
TimeUnit
.
SECONDS
);
int
cul
=
reConnectTimes
.
incrementAndGet
();
if
(
cul
>
3
)
{
close
(
"real stop"
);
throw
new
Exception
(
"服务端断连,3次重连均失败"
);
}
log
.
warn
(
"[{}]第[{}]次断开重连"
,
key
,
cul
);
if
(
tryReconnect
.
get
())
{
log
.
error
(
"[{}]第[{}]次断开重连结果 -> 连接正在重连,本次重连请求放弃"
,
key
,
cul
);
needReconnect
();
return
;
}
try
{
tryReconnect
.
set
(
true
);
if
(
webSocketClient
.
isOpen
())
{
log
.
warn
(
"[{}]第[{}]次断开重连,关闭旧连接"
,
key
,
cul
);
webSocketClient
.
closeConnection
(
2
,
"reconnect stop"
);
}
webSocketClient
=
null
;
createWebSocketClient
();
connect
();
if
(!
StrUtil
.
hasBlank
(
initReConnectReq
))
{
send
(
initReConnectReq
);
}
}
catch
(
Exception
exception
)
{
log
.
error
(
"[{}]第[{}]次断开重连结果 -> 连接正在重连,重连异常:[{}]"
,
key
,
cul
,
exception
.
getMessage
());
needReconnect
();
}
finally
{
tryReconnect
.
set
(
false
);
}
}
private
void
createWebSocketClient
()
{
webSocketClient
=
new
WebSocketClient
(
serverUri
)
{
@Override
public
void
onOpen
(
ServerHandshake
serverHandshake
)
{
log
.
info
(
"[{}]ReConnectWebSocketClient [onOpen]连接成功{}"
,
key
,
getRemoteSocketAddress
());
tryReconnect
.
set
(
false
);
}
@Override
public
void
onMessage
(
String
text
)
{
log
.
info
(
"[{}]ReConnectWebSocketClient [onMessage]接收到服务端数据:text={}"
,
key
,
text
);
msgStr
.
accept
(
text
);
}
@Override
public
void
onMessage
(
ByteBuffer
bytes
)
{
log
.
info
(
"[{}]ReConnectWebSocketClient [onMessage]接收到服务端数据:bytes={}"
,
key
,
bytes
);
msgByte
.
accept
(
bytes
);
}
@Override
public
void
onWebsocketPong
(
WebSocket
conn
,
Framedata
f
)
{
log
.
info
(
"[{}]ReConnectWebSocketClient [onWebsocketPong]接收到服务端数据:opcode={}"
,
key
,
f
.
getOpcode
());
}
@Override
public
void
onClose
(
int
i
,
String
s
,
boolean
b
)
{
log
.
info
(
"[{}]ReConnectWebSocketClient [onClose]关闭,s={},b={}"
,
key
,
s
,
b
);
if
(
StrUtil
.
hasBlank
(
s
)
||
s
.
contains
(
"https"
))
{
if
(
end
.
get
())
{
return
;
}
try
{
needReconnect
();
}
catch
(
Exception
exception
)
{
endConsumer
.
accept
(
"reconnect error"
);
error
.
accept
(
exception
);
}
}
}
@Override
public
void
onError
(
Exception
e
)
{
log
.
info
(
"[{}]ReConnectWebSocketClient [onError]异常,e={}"
,
key
,
e
);
endConsumer
.
accept
(
"error close"
);
error
.
accept
(
e
);
}
};
if
(
serverUri
.
toString
().
contains
(
"wss://"
))
{
trustAllHosts
(
webSocketClient
);
}
}
public
void
circlePing
()
{
new
Thread
(
()
->
{
while
(
needPing
.
get
())
{
if
(
webSocketClient
.
isOpen
())
{
webSocketClient
.
sendPing
();
}
ThreadUtil
.
sleep
(
5
,
TimeUnit
.
SECONDS
);
}
log
.
warn
(
"[{}]Ping循环关闭"
,
key
);
})
.
start
();
}
/**
* 连接
*
* @throws Exception 异常
*/
public
void
connect
()
throws
Exception
{
webSocketClient
.
connectBlocking
(
10
,
TimeUnit
.
SECONDS
);
}
/**
* 发送
*
* @param msg 消息
* @throws Exception 异常
*/
public
void
send
(
String
msg
)
throws
Exception
{
this
.
initReConnectReq
=
msg
;
if
(
webSocketClient
.
isOpen
())
{
webSocketClient
.
send
(
msg
);
}
}
/**
* 关闭
*
* @param msg 关闭消息
*/
public
void
close
(
String
msg
)
{
needPing
.
set
(
false
);
end
.
set
(
true
);
if
(
webSocketClient
!=
null
)
{
webSocketClient
.
closeConnection
(
3
,
msg
);
}
}
/**
* 忽略证书
*
* @param client
*/
public
void
trustAllHosts
(
WebSocketClient
client
)
{
TrustManager
[]
trustAllCerts
=
new
TrustManager
[]
{
new
X509ExtendedTrustManager
()
{
@Override
public
void
checkClientTrusted
(
X509Certificate
[]
x509Certificates
,
String
s
,
Socket
socket
)
throws
CertificateException
{}
@Override
public
void
checkServerTrusted
(
X509Certificate
[]
x509Certificates
,
String
s
,
Socket
socket
)
throws
CertificateException
{}
@Override
public
void
checkClientTrusted
(
X509Certificate
[]
x509Certificates
,
String
s
,
SSLEngine
sslEngine
)
throws
CertificateException
{}
@Override
public
void
checkServerTrusted
(
X509Certificate
[]
x509Certificates
,
String
s
,
SSLEngine
sslEngine
)
throws
CertificateException
{}
@Override
public
void
checkClientTrusted
(
X509Certificate
[]
x509Certificates
,
String
s
)
throws
CertificateException
{}
@Override
public
void
checkServerTrusted
(
X509Certificate
[]
x509Certificates
,
String
s
)
throws
CertificateException
{}
@Override
public
X509Certificate
[]
getAcceptedIssuers
()
{
return
null
;
}
}
};
try
{
SSLContext
ssl
=
SSLContext
.
getInstance
(
"SSL"
);
ssl
.
init
(
null
,
trustAllCerts
,
new
java
.
security
.
SecureRandom
());
SSLSocketFactory
socketFactory
=
ssl
.
getSocketFactory
();
client
.
setSocketFactory
(
socketFactory
);
}
catch
(
Exception
e
)
{
log
.
error
(
"ReConnectWebSocketClient trustAllHosts 异常,e={0}"
,
e
);
}
}
}
\ No newline at end of file
src/main/java/com/subsidy/vo/renshe/ClassImageChecksVO.java
View file @
b06c404
...
@@ -5,6 +5,10 @@ import lombok.Data;
...
@@ -5,6 +5,10 @@ import lombok.Data;
@Data
@Data
public
class
ClassImageChecksVO
{
public
class
ClassImageChecksVO
{
Long
classId
;
Long
memberId
;
/**
/**
* 检测时间,【13位时间戳】【精确到毫秒】如检测类型为活跃度检测则与【学时信息采集接口】、【考试信息采集接口】activityTime时间保持一致
* 检测时间,【13位时间戳】【精确到毫秒】如检测类型为活跃度检测则与【学时信息采集接口】、【考试信息采集接口】activityTime时间保持一致
*/
*/
...
...
src/main/java/com/subsidy/vo/renshe/DailyActivitiesVO.java
View file @
b06c404
...
@@ -9,5 +9,6 @@ public class DailyActivitiesVO {
...
@@ -9,5 +9,6 @@ public class DailyActivitiesVO {
private
Long
activityTime
;
private
Long
activityTime
;
private
Integer
checkType
;
}
}
src/main/resources/mapper/RenSheJuMapper.xml
View file @
b06c404
...
@@ -168,7 +168,8 @@
...
@@ -168,7 +168,8 @@
<select
id=
"dailyActivities"
resultType=
"com.subsidy.vo.renshe.DailyActivitiesVO"
>
<select
id=
"dailyActivities"
resultType=
"com.subsidy.vo.renshe.DailyActivitiesVO"
>
SELECT
SELECT
t1.`status` as access,
t1.`status` as access,
unix_timestamp( t1.create_date )*1000 as activityTime
unix_timestamp( t1.create_date )*1000 as activityTime,
t1.check_type
FROM
FROM
activity_detection t1
activity_detection t1
WHERE
WHERE
...
@@ -176,7 +177,6 @@
...
@@ -176,7 +177,6 @@
AND t1.delete_date IS NULL
AND t1.delete_date IS NULL
AND t1.member_id = #{memberId}
AND t1.member_id = #{memberId}
AND t1.class_id = #{classId}
AND t1.class_id = #{classId}
and t1.check_type= 0
</select>
</select>
<select
id=
"examActivities"
resultType=
"com.subsidy.vo.renshe.ExamActivitiesVO"
>
<select
id=
"examActivities"
resultType=
"com.subsidy.vo.renshe.ExamActivitiesVO"
>
...
@@ -353,7 +353,7 @@
...
@@ -353,7 +353,7 @@
SELECT
SELECT
distinct class_id
distinct class_id
FROM
FROM
class_member_mapping
image_check_record
WHERE
WHERE
delete_date IS NULL
delete_date IS NULL
AND DATE_FORMAT( DATE_ADD( update_date, INTERVAL 1 DAY ), '%Y-%m-%d' ) = DATE_FORMAT(
AND DATE_FORMAT( DATE_ADD( update_date, INTERVAL 1 DAY ), '%Y-%m-%d' ) = DATE_FORMAT(
...
@@ -401,4 +401,27 @@
...
@@ -401,4 +401,27 @@
AND t1.result = 1
AND t1.result = 1
</select>
</select>
<select
id=
"studyCheck"
parameterType=
"long"
resultType=
"com.subsidy.vo.renshe.ClassImageChecksVO"
>
SELECT
t1.class_id,
t1.member_id,
t1.photo AS image,
t2.user_name AS studentName,
t2.id_card AS identity,
t2.telephone AS phone,
UNIX_TIMESTAMP( t1.create_date )* 1000 AS activityTime
FROM
image_check_record t1
LEFT JOIN member t2 ON t1.member_id = t2.id
WHERE
class_id = #{classId}
AND DATE_FORMAT( DATE_ADD( t1.create_date, INTERVAL 1 DAY ), '%Y-%m-%d' ) = DATE_FORMAT( NOW(), '%Y-%m-%d' )
AND t1.paper_id IS NULL
AND t1.result = 1
and t1.member_id
GROUP BY t1.class_id,t1.member_id
ORDER BY t1.create_date desc
</select>
</mapper>
</mapper>
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment