diff --git a/.gitignore b/.gitignore
index 0f35273..86b114d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,6 @@
/common/target/
/merchant-service/target/
/distribution-service/target/
-./merchant-service/target/
\ No newline at end of file
+./merchant-service/target/
+/identity-service/target/
+/identity-domain-api/target/
diff --git a/common/src/main/java/com/tus/coupon/common/context/HeaderUserContextResolver.java b/common/src/main/java/com/tus/coupon/common/context/HeaderUserContextResolver.java
new file mode 100644
index 0000000..cca3292
--- /dev/null
+++ b/common/src/main/java/com/tus/coupon/common/context/HeaderUserContextResolver.java
@@ -0,0 +1,47 @@
+package com.tus.coupon.common.context;
+
+import cn.hutool.core.util.StrUtil;
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.util.UUID;
+
+public class HeaderUserContextResolver implements UserContextResolver {
+ public static final String HEADER_USER_ID = "X-User-Id";
+ public static final String HEADER_USERNAME = "X-Username";
+ public static final String HEADER_SHOP_NUMBER = "X-Shop-Number";
+ public static final String HEADER_TENANT_ID = "X-Tenant-Id";
+ public static final String HEADER_TRACE_ID = "X-Trace-Id";
+
+
+ @Override
+ public UserInfoDTO resolve(HttpServletRequest request) {
+ String userId = request.getHeader(HEADER_USER_ID);
+ String username = request.getHeader(HEADER_USERNAME);
+ String shopNumberStr = request.getHeader(HEADER_SHOP_NUMBER);
+ String tenantId = request.getHeader(HEADER_TENANT_ID);
+ String traceId = request.getHeader(HEADER_TRACE_ID);
+
+ Long shopNumber = null;
+ if (StrUtil.isNotBlank(shopNumberStr) && StrUtil.isNumeric(shopNumberStr)) {
+ shopNumber = Long.parseLong(shopNumberStr);
+ }
+
+ if (StrUtil.isBlank(userId)) {
+ userId = "anonymous";
+ }
+ if (StrUtil.isBlank(username)) {
+ username = "anonymous";
+ }
+ if (StrUtil.isBlank(traceId)) {
+ traceId = UUID.randomUUID().toString();
+ }
+
+ return UserInfoDTO.builder()
+ .userId(userId)
+ .username(username)
+ .shopNumber(shopNumber)
+ .tenantId(tenantId)
+ .traceId(traceId)
+ .build();
+ }
+}
diff --git a/common/src/main/java/com/tus/coupon/common/context/UserContext.java b/common/src/main/java/com/tus/coupon/common/context/UserContext.java
index fea4352..df6560d 100644
--- a/common/src/main/java/com/tus/coupon/common/context/UserContext.java
+++ b/common/src/main/java/com/tus/coupon/common/context/UserContext.java
@@ -1,6 +1,7 @@
package com.tus.coupon.common.context;
import com.alibaba.ttl.TransmittableThreadLocal;
+import com.tus.coupon.common.exception.ClientException;
import java.util.Optional;
@@ -13,6 +14,15 @@ public static void setUser(UserInfoDTO user) {
USER_THREAD_LOCAL.set(user);
}
+ public static UserInfoDTO getUser() {
+ return USER_THREAD_LOCAL.get();
+ }
+
+ public static UserInfoDTO requireUser() {
+ return Optional.ofNullable(USER_THREAD_LOCAL.get())
+ .orElseThrow(() -> new ClientException("User context is required but missing"));
+ }
+
// fetch user id from user context
public static String getUserId() {
UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();
@@ -31,6 +41,17 @@ public static Long getShopNumber() {
return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getShopNumber).orElse(null);
}
+ public static String getTenantId() {
+ UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();
+ return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getTenantId).orElse(null);
+ }
+
+ public static String getTraceId() {
+ UserInfoDTO userInfoDTO = USER_THREAD_LOCAL.get();
+ return Optional.ofNullable(userInfoDTO).map(UserInfoDTO::getTraceId).orElse(null);
+ }
+
+
// clean user context
public static void removeUser() {
USER_THREAD_LOCAL.remove();
diff --git a/common/src/main/java/com/tus/coupon/common/context/UserContextAutoConfiguration.java b/common/src/main/java/com/tus/coupon/common/context/UserContextAutoConfiguration.java
new file mode 100644
index 0000000..a1d1bae
--- /dev/null
+++ b/common/src/main/java/com/tus/coupon/common/context/UserContextAutoConfiguration.java
@@ -0,0 +1,30 @@
+package com.tus.coupon.common.context;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@AutoConfiguration
+public class UserContextAutoConfiguration {
+
+ @Bean
+ public UserContextResolver userContextResolver() {
+ return new HeaderUserContextResolver();
+ }
+
+ @Bean
+ public UserContextInterceptor userContextInterceptor(UserContextResolver userContextResolver) {
+ return new UserContextInterceptor(userContextResolver);
+ }
+
+ @Bean
+ public WebMvcConfigurer userContextWebMvcConfigurer(UserContextInterceptor userContextInterceptor) {
+ return new WebMvcConfigurer() {
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(userContextInterceptor).addPathPatterns("/**");
+ }
+ };
+ }
+}
diff --git a/common/src/main/java/com/tus/coupon/common/context/UserContextInterceptor.java b/common/src/main/java/com/tus/coupon/common/context/UserContextInterceptor.java
new file mode 100644
index 0000000..1978d2a
--- /dev/null
+++ b/common/src/main/java/com/tus/coupon/common/context/UserContextInterceptor.java
@@ -0,0 +1,32 @@
+package com.tus.coupon.common.context;
+
+import jakarta.annotation.Nullable;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+public class UserContextInterceptor implements HandlerInterceptor {
+ private final UserContextResolver userContextResolver;
+
+ public UserContextInterceptor(UserContextResolver userContextResolver) {
+ this.userContextResolver = userContextResolver;
+ }
+
+ @Override
+ public boolean preHandle(@Nullable HttpServletRequest request,
+ @Nullable HttpServletResponse response,
+ @Nullable Object handler) throws Exception {
+ if (request != null) {
+ UserContext.setUser(userContextResolver.resolve(request));
+ }
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(@Nullable HttpServletRequest request,
+ @Nullable HttpServletResponse response,
+ @Nullable Object handler,
+ Exception ex) throws Exception {
+ UserContext.removeUser();
+ }
+}
diff --git a/common/src/main/java/com/tus/coupon/common/context/UserContextResolver.java b/common/src/main/java/com/tus/coupon/common/context/UserContextResolver.java
new file mode 100644
index 0000000..58e7ff0
--- /dev/null
+++ b/common/src/main/java/com/tus/coupon/common/context/UserContextResolver.java
@@ -0,0 +1,7 @@
+package com.tus.coupon.common.context;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+public interface UserContextResolver {
+ UserInfoDTO resolve(HttpServletRequest request);
+}
diff --git a/common/src/main/java/com/tus/coupon/common/context/UserInfoDTO.java b/common/src/main/java/com/tus/coupon/common/context/UserInfoDTO.java
index 0c7dda2..b1c133f 100644
--- a/common/src/main/java/com/tus/coupon/common/context/UserInfoDTO.java
+++ b/common/src/main/java/com/tus/coupon/common/context/UserInfoDTO.java
@@ -18,4 +18,10 @@ public class UserInfoDTO {
// shop number
private Long shopNumber;
+
+ // tenant id
+ private String tenantId;
+
+ // trace id
+ private String traceId;
}
diff --git a/identity-domain-api/pom.xml b/identity-domain-api/pom.xml
new file mode 100644
index 0000000..83fda1a
--- /dev/null
+++ b/identity-domain-api/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+ com.tus.coupon
+ coupon-all
+ 0.0.1-SNAPSHOT
+
+
+ coupon-identity-domain-api
+
+
+
+ org.springframework
+ spring-web
+
+
+
+ ${project.groupId}
+ coupon-common
+ ${project.version}
+
+
+
diff --git a/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/IdentityUserApi.java b/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/IdentityUserApi.java
new file mode 100644
index 0000000..633b258
--- /dev/null
+++ b/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/IdentityUserApi.java
@@ -0,0 +1,32 @@
+package com.tus.coupon.identity.api.dto;
+
+import com.tus.coupon.common.web.Result;
+import com.tus.coupon.identity.api.dto.req.IdentityUserQueryReqDTO;
+import com.tus.coupon.identity.api.dto.resp.IdentityUserQueryRespDTO;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+public interface IdentityUserApi {
+
+ @GetMapping("/api/identity/users/query")
+ Result queryUsers(
+ @RequestParam(required = false) Long taskId,
+ @RequestParam(required = false) String shopNumber,
+ @RequestParam(required = false) Long cursor,
+ @RequestParam(required = false) Integer limit
+ );
+
+ static IdentityUserQueryReqDTO buildReq(
+ Long taskId,
+ String shopNumber,
+ Long cursor,
+ Integer limit
+ ) {
+ IdentityUserQueryReqDTO request = new IdentityUserQueryReqDTO();
+ request.setTaskId(taskId);
+ request.setShopNumber(shopNumber);
+ request.setCursor(cursor);
+ request.setLimit(limit);
+ return request;
+ }
+}
diff --git a/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/req/IdentityUserQueryReqDTO.java b/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/req/IdentityUserQueryReqDTO.java
new file mode 100644
index 0000000..9029301
--- /dev/null
+++ b/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/req/IdentityUserQueryReqDTO.java
@@ -0,0 +1,18 @@
+package com.tus.coupon.identity.api.dto.req;
+
+import lombok.Data;
+
+@Data
+public class IdentityUserQueryReqDTO {
+ // optional: query users by coupon task id
+ private Long taskId;
+
+ // optional: query users by merchant shop number
+ private String shopNumber;
+
+ // cursor based pagination, use user table id
+ private Long cursor;
+
+ // page size
+ private Integer limit;
+}
diff --git a/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/resp/IdentityUserItemRespDTO.java b/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/resp/IdentityUserItemRespDTO.java
new file mode 100644
index 0000000..94bbc5d
--- /dev/null
+++ b/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/resp/IdentityUserItemRespDTO.java
@@ -0,0 +1,15 @@
+package com.tus.coupon.identity.api.dto.resp;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class IdentityUserItemRespDTO {
+ private Long id;
+ private String userId;
+ private String username;
+ private String shopNumber;
+ private String phone;
+ private String mail;
+}
diff --git a/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/resp/IdentityUserQueryRespDTO.java b/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/resp/IdentityUserQueryRespDTO.java
new file mode 100644
index 0000000..39c76db
--- /dev/null
+++ b/identity-domain-api/src/main/java/com/tus/coupon/identity/api/dto/resp/IdentityUserQueryRespDTO.java
@@ -0,0 +1,14 @@
+package com.tus.coupon.identity.api.dto.resp;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Builder
+public class IdentityUserQueryRespDTO {
+ private List users;
+ private Long nextCursor;
+ private Boolean hasMore;
+}
diff --git a/identity-service/pom.xml b/identity-service/pom.xml
new file mode 100644
index 0000000..fcf0406
--- /dev/null
+++ b/identity-service/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ com.tus.coupon
+ coupon-all
+ 0.0.1-SNAPSHOT
+
+
+ coupon-identity-service
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+ ${project.groupId}
+ coupon-common
+ ${project.version}
+
+
+
+ ${project.groupId}
+ coupon-identity-domain-api
+ ${project.version}
+
+
+ io.swagger.core.v3
+ swagger-annotations-jakarta
+ 2.2.22
+ compile
+
+
+
\ No newline at end of file
diff --git a/identity-service/src/main/java/com/tus/coupon/identity/IdentityApplication.java b/identity-service/src/main/java/com/tus/coupon/identity/IdentityApplication.java
new file mode 100644
index 0000000..12ee1fe
--- /dev/null
+++ b/identity-service/src/main/java/com/tus/coupon/identity/IdentityApplication.java
@@ -0,0 +1,16 @@
+package com.tus.coupon.identity;
+
+import com.tus.coupon.common.context.UserContextAutoConfiguration;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Import;
+
+@SpringBootApplication
+@MapperScan("com.tus.coupon.common.dao.mapper")
+@Import(UserContextAutoConfiguration.class)
+public class IdentityApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(IdentityApplication.class, args);
+ }
+}
diff --git a/identity-service/src/main/java/com/tus/coupon/identity/controller/IdentityUserController.java b/identity-service/src/main/java/com/tus/coupon/identity/controller/IdentityUserController.java
new file mode 100644
index 0000000..e8312b5
--- /dev/null
+++ b/identity-service/src/main/java/com/tus/coupon/identity/controller/IdentityUserController.java
@@ -0,0 +1,32 @@
+package com.tus.coupon.identity.controller;
+
+import com.tus.coupon.common.web.Result;
+import com.tus.coupon.common.web.Results;
+import com.tus.coupon.identity.api.dto.IdentityUserApi;
+import com.tus.coupon.identity.api.dto.req.IdentityUserQueryReqDTO;
+import com.tus.coupon.identity.api.dto.resp.IdentityUserQueryRespDTO;
+import com.tus.coupon.identity.service.IdentityUserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@Tag(name = "Identity User Controller")
+public class IdentityUserController implements IdentityUserApi {
+ private final IdentityUserService identityUserService;
+
+ @Override
+ @Operation(summary = "Query users for coupon distribution")
+ public Result queryUsers(
+ @RequestParam(required = false) Long taskId,
+ @RequestParam(required = false) String shopNumber,
+ @RequestParam(required = false) Long cursor,
+ @RequestParam(required = false) Integer limit
+ ) {
+ IdentityUserQueryReqDTO request = IdentityUserApi.buildReq(taskId, shopNumber, cursor, limit);
+ return Results.success(identityUserService.queryUsers(request));
+ }
+}
\ No newline at end of file
diff --git a/identity-service/src/main/java/com/tus/coupon/identity/service/IdentityUserService.java b/identity-service/src/main/java/com/tus/coupon/identity/service/IdentityUserService.java
new file mode 100644
index 0000000..7d92c22
--- /dev/null
+++ b/identity-service/src/main/java/com/tus/coupon/identity/service/IdentityUserService.java
@@ -0,0 +1,8 @@
+package com.tus.coupon.identity.service;
+
+import com.tus.coupon.identity.api.dto.req.IdentityUserQueryReqDTO;
+import com.tus.coupon.identity.api.dto.resp.IdentityUserQueryRespDTO;
+
+public interface IdentityUserService {
+ IdentityUserQueryRespDTO queryUsers(IdentityUserQueryReqDTO requestParam);
+}
diff --git a/identity-service/src/main/java/com/tus/coupon/identity/service/impl/IdentityUserServiceImpl.java b/identity-service/src/main/java/com/tus/coupon/identity/service/impl/IdentityUserServiceImpl.java
new file mode 100644
index 0000000..e6aa417
--- /dev/null
+++ b/identity-service/src/main/java/com/tus/coupon/identity/service/impl/IdentityUserServiceImpl.java
@@ -0,0 +1,78 @@
+package com.tus.coupon.identity.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.tus.coupon.common.dao.entity.UserDO;
+import com.tus.coupon.common.dao.mapper.UserMapper;
+import com.tus.coupon.common.exception.ClientException;
+import com.tus.coupon.identity.api.dto.req.IdentityUserQueryReqDTO;
+import com.tus.coupon.identity.api.dto.resp.IdentityUserItemRespDTO;
+import com.tus.coupon.identity.api.dto.resp.IdentityUserQueryRespDTO;
+import com.tus.coupon.identity.service.IdentityUserService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class IdentityUserServiceImpl implements IdentityUserService {
+ private static final int DEFAULT_LIMIT = 100;
+ private static final int MAX_LIMIT = 500;
+
+ private final UserMapper userMapper;
+
+ @Override
+ public IdentityUserQueryRespDTO queryUsers(IdentityUserQueryReqDTO requestParam) {
+ if (requestParam == null) {
+ throw new ClientException("Identity user query request should not be null");
+ }
+
+ Integer limit = requestParam.getLimit();
+ if (limit == null || limit <= 0) {
+ limit = DEFAULT_LIMIT;
+ }
+
+ if (limit > MAX_LIMIT) {
+ throw new ClientException("Requested limit exceeds max allowed size: " + MAX_LIMIT);
+ }
+
+ long cursor = requestParam.getCursor() == null ? 0L : requestParam.getCursor();
+ LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(UserDO.class)
+ .gt(UserDO::getId, cursor)
+ .eq(UserDO::getDelFlag, 0)
+ .eq(StrUtil.isNotBlank(requestParam.getShopNumber()), UserDO::getShopNumber, requestParam.getShopNumber())
+ .orderByAsc(UserDO::getId)
+ .last("LIMIT " + limit);
+
+ List userDOList = userMapper.selectList(queryWrapper);
+ if (userDOList == null || userDOList.isEmpty()) {
+ return IdentityUserQueryRespDTO.builder()
+ .users(Collections.emptyList())
+ .hasMore(Boolean.FALSE)
+ .nextCursor(cursor)
+ .build();
+ }
+
+ List users = userDOList.stream()
+ .map(each -> IdentityUserItemRespDTO.builder()
+ .id(each.getId())
+ .userId(String.valueOf(each.getId()))
+ .username(each.getUsername())
+ .shopNumber(each.getShopNumber())
+ .phone(each.getPhone())
+ .mail(each.getMail())
+ .build())
+ .toList();
+
+ Long nextCursor = userDOList.get(userDOList.size() - 1).getId();
+ boolean hasMore = userDOList.size() >= limit;
+ return IdentityUserQueryRespDTO.builder()
+ .users(users)
+ .nextCursor(nextCursor)
+ .hasMore(hasMore)
+ .build();
+ }
+}
diff --git a/identity-service/src/main/resources/application.yaml b/identity-service/src/main/resources/application.yaml
new file mode 100644
index 0000000..c2f8118
--- /dev/null
+++ b/identity-service/src/main/resources/application.yaml
@@ -0,0 +1,10 @@
+server:
+ port: 10030
+
+spring:
+ application:
+ name: coupon-identity
+
+mybatis-plus:
+ configuration:
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
diff --git a/merchant-service/src/main/java/com/tus/coupon/merchant/config/UserConfiguration.java b/merchant-service/src/main/java/com/tus/coupon/merchant/config/UserConfiguration.java
index dd37c01..833d754 100644
--- a/merchant-service/src/main/java/com/tus/coupon/merchant/config/UserConfiguration.java
+++ b/merchant-service/src/main/java/com/tus/coupon/merchant/config/UserConfiguration.java
@@ -32,7 +32,8 @@ static class UserTransmitInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Object handler) throws Exception {
UserInfoDTO userInfoDTO = new UserInfoDTO(UUID.randomUUID().toString(),
- UUID.randomUUID().toString(), UUID.randomUUID().node());
+ UUID.randomUUID().toString(), UUID.randomUUID().node(),
+ UUID.randomUUID().toString(), UUID.randomUUID().toString());
UserContext.setUser(userInfoDTO);
return true;
}
diff --git a/pom.xml b/pom.xml
index 3b27281..c745af4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,6 +11,8 @@
merchant-service
common
distribution-service
+ identity-service
+ identity-domain-api