Merge branch 'feature/REQ-1507' into dev
This commit is contained in:
commit
893888c9eb
@ -7,6 +7,7 @@ import cn.axzo.msg.center.message.domain.param.PendingMessagePushParam;
|
|||||||
import cn.axzo.msg.center.message.service.PendingMessageNewService;
|
import cn.axzo.msg.center.message.service.PendingMessageNewService;
|
||||||
import cn.axzo.msg.center.service.enums.TerminalTypeEnum;
|
import cn.axzo.msg.center.service.enums.TerminalTypeEnum;
|
||||||
import cn.axzo.msg.center.service.pending.client.PendingMessageClient;
|
import cn.axzo.msg.center.service.pending.client.PendingMessageClient;
|
||||||
|
import cn.axzo.msg.center.service.pending.request.CompleteMessageRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessageCountUncompletedRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessageCountUncompletedRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessagePageRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessagePageRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessagePushRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessagePushRequest;
|
||||||
@ -15,6 +16,7 @@ import cn.axzo.msg.center.service.pending.request.PendingMessageStatisticRequest
|
|||||||
import cn.axzo.msg.center.service.pending.response.PendingMessageResponse;
|
import cn.axzo.msg.center.service.pending.response.PendingMessageResponse;
|
||||||
import cn.axzo.msg.center.service.pending.response.PendingMessageStatisticResponse;
|
import cn.axzo.msg.center.service.pending.response.PendingMessageStatisticResponse;
|
||||||
import cn.azxo.framework.common.model.CommonResponse;
|
import cn.azxo.framework.common.model.CommonResponse;
|
||||||
|
import cn.axzo.msg.center.service.pending.response.PushPendingMessageDTO;
|
||||||
import cn.azxo.framework.common.model.Page;
|
import cn.azxo.framework.common.model.Page;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@ -73,7 +75,7 @@ public class PendingMessageNewController implements PendingMessageClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResponse<String> push(PendingMessagePushRequest request) {
|
public CommonResponse<List<PushPendingMessageDTO>> push(PendingMessagePushRequest request) {
|
||||||
return CommonResponse.success(pendingMessageNewService.push(PendingMessagePushParam.from(request)));
|
return CommonResponse.success(pendingMessageNewService.push(PendingMessagePushParam.from(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,4 +88,14 @@ public class PendingMessageNewController implements PendingMessageClient {
|
|||||||
public CommonResponse<Boolean> revoke(String requestNo) {
|
public CommonResponse<Boolean> revoke(String requestNo) {
|
||||||
return CommonResponse.success(pendingMessageNewService.revoke(requestNo));
|
return CommonResponse.success(pendingMessageNewService.revoke(requestNo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonResponse<Boolean> completeByTemplateCodeBizCode(CompleteMessageRequest param) {
|
||||||
|
return CommonResponse.success(pendingMessageNewService.completeByTemplateCodeBizCode(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonResponse<Boolean> revokeByTemplateCodeBizCode(CompleteMessageRequest param) {
|
||||||
|
return CommonResponse.success(pendingMessageNewService.revokeByTemplateCodeBizCode(param));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,11 @@ package cn.axzo.msg.center.message.service;
|
|||||||
|
|
||||||
import cn.axzo.msg.center.message.domain.dto.PendingMessageDTO;
|
import cn.axzo.msg.center.message.domain.dto.PendingMessageDTO;
|
||||||
import cn.axzo.msg.center.message.domain.dto.PendingMessageStatisticDTO;
|
import cn.axzo.msg.center.message.domain.dto.PendingMessageStatisticDTO;
|
||||||
|
import cn.axzo.msg.center.service.pending.response.PushPendingMessageDTO;
|
||||||
import cn.axzo.msg.center.message.domain.param.MessageGroupNodeStatisticParam;
|
import cn.axzo.msg.center.message.domain.param.MessageGroupNodeStatisticParam;
|
||||||
import cn.axzo.msg.center.message.domain.param.PendingMessagePushParam;
|
import cn.axzo.msg.center.message.domain.param.PendingMessagePushParam;
|
||||||
import cn.axzo.msg.center.service.enums.TerminalTypeEnum;
|
import cn.axzo.msg.center.service.enums.TerminalTypeEnum;
|
||||||
|
import cn.axzo.msg.center.service.pending.request.CompleteMessageRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessagePageRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessagePageRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessageQueryRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessageQueryRequest;
|
||||||
import cn.axzo.msg.center.service.pending.response.PendingMessageResponse;
|
import cn.axzo.msg.center.service.pending.response.PendingMessageResponse;
|
||||||
@ -69,7 +71,7 @@ public interface PendingMessageNewService {
|
|||||||
* @param param 代办核心参数
|
* @param param 代办核心参数
|
||||||
* @return 代办唯一标识
|
* @return 代办唯一标识
|
||||||
*/
|
*/
|
||||||
String push(PendingMessagePushParam param);
|
List<PushPendingMessageDTO> push(PendingMessagePushParam param);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 完成代办
|
* 完成代办
|
||||||
@ -86,4 +88,20 @@ public interface PendingMessageNewService {
|
|||||||
* @return 成功返回 {@code true} 失败返回 {@code false}
|
* @return 成功返回 {@code true} 失败返回 {@code false}
|
||||||
*/
|
*/
|
||||||
Boolean revoke(String requestNo);
|
Boolean revoke(String requestNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过业务编码和模版编码完成代办
|
||||||
|
*
|
||||||
|
* @param param
|
||||||
|
* @return 成功返回 {@code true} 失败返回 {@code false}
|
||||||
|
*/
|
||||||
|
Boolean completeByTemplateCodeBizCode(CompleteMessageRequest param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过业务编码和模版编码撤销代办
|
||||||
|
*
|
||||||
|
* @param param
|
||||||
|
* @return 成功返回 {@code true} 失败返回 {@code false}
|
||||||
|
*/
|
||||||
|
Boolean revokeByTemplateCodeBizCode(CompleteMessageRequest param);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import cn.axzo.msg.center.message.domain.dto.MessageTemplateDTO;
|
|||||||
import cn.axzo.msg.center.message.domain.dto.MessageTemplateRouterDTO;
|
import cn.axzo.msg.center.message.domain.dto.MessageTemplateRouterDTO;
|
||||||
import cn.axzo.msg.center.message.domain.dto.PendingMessageDTO;
|
import cn.axzo.msg.center.message.domain.dto.PendingMessageDTO;
|
||||||
import cn.axzo.msg.center.message.domain.dto.PendingMessageStatisticDTO;
|
import cn.axzo.msg.center.message.domain.dto.PendingMessageStatisticDTO;
|
||||||
|
import cn.axzo.msg.center.service.pending.request.CompleteMessageRequest;
|
||||||
|
import cn.axzo.msg.center.service.pending.response.PushPendingMessageDTO;
|
||||||
import cn.axzo.msg.center.message.domain.param.MessageGroupNodeStatisticParam;
|
import cn.axzo.msg.center.message.domain.param.MessageGroupNodeStatisticParam;
|
||||||
import cn.axzo.msg.center.message.domain.param.PendingMessagePushParam;
|
import cn.axzo.msg.center.message.domain.param.PendingMessagePushParam;
|
||||||
import cn.axzo.msg.center.message.service.MessageGroupNodeService;
|
import cn.axzo.msg.center.message.service.MessageGroupNodeService;
|
||||||
@ -160,7 +162,7 @@ public class PendingMessageNewServiceImpl implements PendingMessageNewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String push(PendingMessagePushParam param) {
|
public List<PushPendingMessageDTO> push(PendingMessagePushParam param) {
|
||||||
MessageTemplateDTO msgTemplate = messageTemplateNewService
|
MessageTemplateDTO msgTemplate = messageTemplateNewService
|
||||||
.queryByTemplateCode(param.getTemplateCode())
|
.queryByTemplateCode(param.getTemplateCode())
|
||||||
.orElseThrow(() -> new ServiceException("not found message template."));
|
.orElseThrow(() -> new ServiceException("not found message template."));
|
||||||
@ -178,7 +180,11 @@ public class PendingMessageNewServiceImpl implements PendingMessageNewService {
|
|||||||
List<PendingMessageRecord> record = convert(param, msgTemplate, requestNo, workspace);
|
List<PendingMessageRecord> record = convert(param, msgTemplate, requestNo, workspace);
|
||||||
pendingMessageRecordDao.saveBatch(record);
|
pendingMessageRecordDao.saveBatch(record);
|
||||||
// TODO 消息推送 @luofu
|
// TODO 消息推送 @luofu
|
||||||
return requestNo;
|
return record.stream().map(e -> PushPendingMessageDTO.builder()
|
||||||
|
.executorId(e.getExecutorId())
|
||||||
|
.executorPersonId(e.getExecutorPersonId())
|
||||||
|
.requestNo(e.getRequestNo())
|
||||||
|
.build()).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -211,6 +217,30 @@ public class PendingMessageNewServiceImpl implements PendingMessageNewService {
|
|||||||
.update();
|
.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean completeByTemplateCodeBizCode(CompleteMessageRequest param) {
|
||||||
|
log.info("the [{}] record is completeByTemplateCodeBizCode retract.", param);
|
||||||
|
return pendingMessageRecordDao.lambdaUpdate()
|
||||||
|
.set(PendingMessageRecord::getState, PendingMessageStateEnum.COMPLETED)
|
||||||
|
.eq(PendingMessageRecord::getTemplateCode, param.getTemplateCode())
|
||||||
|
.eq(PendingMessageRecord::getBizCode, param.getBizCode())
|
||||||
|
.eq(PendingMessageRecord::getState, PendingMessageStateEnum.HAS_BEEN_SENT)
|
||||||
|
.eq(PendingMessageRecord::getIsDelete, TableIsDeleteEnum.NORMAL.value)
|
||||||
|
.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean revokeByTemplateCodeBizCode(CompleteMessageRequest param) {
|
||||||
|
log.info("the [{}] record is revokeByTemplateCodeBizCode retract.", param);
|
||||||
|
return pendingMessageRecordDao.lambdaUpdate()
|
||||||
|
.set(PendingMessageRecord::getState, PendingMessageStateEnum.RETRACT)
|
||||||
|
.eq(PendingMessageRecord::getTemplateCode, param.getTemplateCode())
|
||||||
|
.eq(PendingMessageRecord::getBizCode, param.getBizCode())
|
||||||
|
.eq(PendingMessageRecord::getState, PendingMessageStateEnum.HAS_BEEN_SENT)
|
||||||
|
.eq(PendingMessageRecord::getIsDelete, TableIsDeleteEnum.NORMAL.value)
|
||||||
|
.update();
|
||||||
|
}
|
||||||
|
|
||||||
private PendingMessageDTO convert(PendingMessageRecord pendingMessageRecord, List<MessageTemplateDTO> messageTemplates) {
|
private PendingMessageDTO convert(PendingMessageRecord pendingMessageRecord, List<MessageTemplateDTO> messageTemplates) {
|
||||||
PendingMessageDTO pendingMessage = PendingMessageDTO.from(pendingMessageRecord);
|
PendingMessageDTO pendingMessage = PendingMessageDTO.from(pendingMessageRecord);
|
||||||
// 对应模板的路由策略
|
// 对应模板的路由策略
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package cn.axzo.msg.center.service.pending.client;
|
|||||||
|
|
||||||
import cn.axzo.msg.center.service.enums.TerminalTypeEnum;
|
import cn.axzo.msg.center.service.enums.TerminalTypeEnum;
|
||||||
import cn.axzo.msg.center.service.pending.client.fallback.PendingMessageClientFallback;
|
import cn.axzo.msg.center.service.pending.client.fallback.PendingMessageClientFallback;
|
||||||
|
import cn.axzo.msg.center.service.pending.request.CompleteMessageRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessageCountUncompletedRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessageCountUncompletedRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessagePageRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessagePageRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessagePushRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessagePushRequest;
|
||||||
@ -9,6 +10,7 @@ import cn.axzo.msg.center.service.pending.request.PendingMessageQueryRequest;
|
|||||||
import cn.axzo.msg.center.service.pending.request.PendingMessageStatisticRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessageStatisticRequest;
|
||||||
import cn.axzo.msg.center.service.pending.response.PendingMessageResponse;
|
import cn.axzo.msg.center.service.pending.response.PendingMessageResponse;
|
||||||
import cn.axzo.msg.center.service.pending.response.PendingMessageStatisticResponse;
|
import cn.axzo.msg.center.service.pending.response.PendingMessageStatisticResponse;
|
||||||
|
import cn.axzo.msg.center.service.pending.response.PushPendingMessageDTO;
|
||||||
import cn.azxo.framework.common.model.CommonResponse;
|
import cn.azxo.framework.common.model.CommonResponse;
|
||||||
import cn.azxo.framework.common.model.Page;
|
import cn.azxo.framework.common.model.Page;
|
||||||
import org.springframework.cloud.openfeign.FeignClient;
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
@ -89,7 +91,7 @@ public interface PendingMessageClient {
|
|||||||
* @return 代办唯一标识
|
* @return 代办唯一标识
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/pending-message/push", produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PostMapping(value = "/pending-message/push", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
CommonResponse<String> push(@RequestBody @Valid PendingMessagePushRequest request);
|
CommonResponse<List<PushPendingMessageDTO>> push(@RequestBody @Valid PendingMessagePushRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 完成代办
|
* 完成代办
|
||||||
@ -108,4 +110,22 @@ public interface PendingMessageClient {
|
|||||||
*/
|
*/
|
||||||
@PostMapping(value = "/pending-message/revoke", produces = {MediaType.APPLICATION_JSON_VALUE})
|
@PostMapping(value = "/pending-message/revoke", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
CommonResponse<Boolean> revoke(@RequestParam("requestNo") String requestNo);
|
CommonResponse<Boolean> revoke(@RequestParam("requestNo") String requestNo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过模版编号和业务编号完成代办
|
||||||
|
*
|
||||||
|
* @param param
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/pending-message/completeByTemplateCodeBizCode", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
|
CommonResponse<Boolean> completeByTemplateCodeBizCode(@RequestBody @Valid CompleteMessageRequest param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过模版编号和业务编号撤销代办
|
||||||
|
*
|
||||||
|
* @param param
|
||||||
|
* @return 成功返回 {@code true} 失败返回 {@code false}
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/pending-message/revokeByTemplateCodeBizCode", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
|
CommonResponse<Boolean> revokeByTemplateCodeBizCode(@RequestBody @Valid CompleteMessageRequest param);
|
||||||
}
|
}
|
||||||
@ -2,6 +2,7 @@ package cn.axzo.msg.center.service.pending.client.fallback;
|
|||||||
|
|
||||||
import cn.axzo.msg.center.service.enums.TerminalTypeEnum;
|
import cn.axzo.msg.center.service.enums.TerminalTypeEnum;
|
||||||
import cn.axzo.msg.center.service.pending.client.PendingMessageClient;
|
import cn.axzo.msg.center.service.pending.client.PendingMessageClient;
|
||||||
|
import cn.axzo.msg.center.service.pending.request.CompleteMessageRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessageCountUncompletedRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessageCountUncompletedRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessagePageRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessagePageRequest;
|
||||||
import cn.axzo.msg.center.service.pending.request.PendingMessagePushRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessagePushRequest;
|
||||||
@ -9,6 +10,7 @@ import cn.axzo.msg.center.service.pending.request.PendingMessageQueryRequest;
|
|||||||
import cn.axzo.msg.center.service.pending.request.PendingMessageStatisticRequest;
|
import cn.axzo.msg.center.service.pending.request.PendingMessageStatisticRequest;
|
||||||
import cn.axzo.msg.center.service.pending.response.PendingMessageResponse;
|
import cn.axzo.msg.center.service.pending.response.PendingMessageResponse;
|
||||||
import cn.axzo.msg.center.service.pending.response.PendingMessageStatisticResponse;
|
import cn.axzo.msg.center.service.pending.response.PendingMessageStatisticResponse;
|
||||||
|
import cn.axzo.msg.center.service.pending.response.PushPendingMessageDTO;
|
||||||
import cn.azxo.framework.common.model.CommonResponse;
|
import cn.azxo.framework.common.model.CommonResponse;
|
||||||
import cn.azxo.framework.common.model.Page;
|
import cn.azxo.framework.common.model.Page;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -59,7 +61,7 @@ public class PendingMessageClientFallback implements PendingMessageClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResponse<String> push(PendingMessagePushRequest request) {
|
public CommonResponse<List<PushPendingMessageDTO>> push(PendingMessagePushRequest request) {
|
||||||
log.error("fall back while push pending message. request:{}", request);
|
log.error("fall back while push pending message. request:{}", request);
|
||||||
return CommonResponse.error("fall back while push pending message");
|
return CommonResponse.error("fall back while push pending message");
|
||||||
}
|
}
|
||||||
@ -75,4 +77,16 @@ public class PendingMessageClientFallback implements PendingMessageClient {
|
|||||||
log.error("fall back while revoking pending message. msgIdentityCode:[{}]", msgIdentityCode);
|
log.error("fall back while revoking pending message. msgIdentityCode:[{}]", msgIdentityCode);
|
||||||
return CommonResponse.error("fall back while revoking pending message");
|
return CommonResponse.error("fall back while revoking pending message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonResponse<Boolean> completeByTemplateCodeBizCode(CompleteMessageRequest param) {
|
||||||
|
log.error("fall back while completing pending message by biz code. request:[{}]", param);
|
||||||
|
return CommonResponse.error("fall back while completing pending message by biz code");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonResponse<Boolean> revokeByTemplateCodeBizCode(CompleteMessageRequest param) {
|
||||||
|
log.error("fall back while revoking pending message by biz code. msgIdentityCode:[{}]", param);
|
||||||
|
return CommonResponse.error("fall back while revoking pending message by biz code");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package cn.axzo.msg.center.service.pending.request;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author haiyangjin
|
||||||
|
* @date 2023/11/15
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class CompleteMessageRequest implements Serializable {
|
||||||
|
/**
|
||||||
|
* 关联业务主键
|
||||||
|
*/
|
||||||
|
@NotNull(message = "bizCode is required")
|
||||||
|
private String bizCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模版code
|
||||||
|
*/
|
||||||
|
@NotNull(message = "templateCode is required")
|
||||||
|
private String templateCode;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package cn.axzo.msg.center.service.pending.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author haiyangjin
|
||||||
|
* @date 2023/11/15
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PushPendingMessageDTO {
|
||||||
|
/**
|
||||||
|
* 请求批次号
|
||||||
|
*/
|
||||||
|
private String requestNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行者ID
|
||||||
|
*/
|
||||||
|
private Long executorId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行者的自然人ID
|
||||||
|
*/
|
||||||
|
private Long executorPersonId;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user