This commit is contained in:
2026-08-26 17:27:05 +08:00
7 changed files with 56 additions and 21 deletions
@@ -117,6 +117,7 @@ public class AuthService {
.subject(account.getId().toString()) .subject(account.getId().toString())
.claim("user_id", account.getId()) .claim("user_id", account.getId())
.claim("username", account.getUsername()) .claim("username", account.getUsername())
.claim("display_name", account.getDisplayName() == null ? "" : account.getDisplayName())
.claim("tenant_id", account.getDefaultTenantId()) .claim("tenant_id", account.getDefaultTenantId())
.claim("operator_tenant_id", account.getOperatorTenantId()) .claim("operator_tenant_id", account.getOperatorTenantId())
.claim("membership_id", account.getDefaultMembershipId()) .claim("membership_id", account.getDefaultMembershipId())
@@ -187,7 +188,7 @@ public class AuthService {
JwtClaimsSet claims = JwtClaimsSet.builder().issuer(properties.issuer()) JwtClaimsSet claims = JwtClaimsSet.builder().issuer(properties.issuer())
.audience(List.of(properties.audience())).id(jwtId) .audience(List.of(properties.audience())).id(jwtId)
.issuedAt(issuedAt).expiresAt(expiresAt).subject(account.getId().toString()) .issuedAt(issuedAt).expiresAt(expiresAt).subject(account.getId().toString())
.claim("user_id", account.getId()).claim("username", account.getUsername()) .claim("user_id", account.getId()).claim("username", account.getUsername()).claim("display_name", account.getDisplayName() == null ? "" : account.getDisplayName())
.claim("tenant_id", account.getDefaultTenantId()).claim("operator_tenant_id", account.getOperatorTenantId()) .claim("tenant_id", account.getDefaultTenantId()).claim("operator_tenant_id", account.getOperatorTenantId())
.claim("membership_id", account.getDefaultMembershipId()).claim("app_code", "MERCHANT") .claim("membership_id", account.getDefaultMembershipId()).claim("app_code", "MERCHANT")
.claim("client_type", authorization.getClientType()).claim("token_version", account.getTokenVersion()) .claim("client_type", authorization.getClientType()).claim("token_version", account.getTokenVersion())
@@ -7,21 +7,36 @@ import java.util.Set;
* 统一认证用户主体,携带用户、租户、成员身份、应用端、客户端类型及权限信息。 * 统一认证用户主体,携带用户、租户、成员身份、应用端、客户端类型及权限信息。
*/ */
public record AuthenticatedUser(Long userId, Long tenantId, Long operatorTenantId, Long membershipId, public record AuthenticatedUser(Long userId, Long tenantId, Long operatorTenantId, Long membershipId,
String username, String appCode, String clientType, String username, String displayName, String appCode, String clientType,
Set<String> permissions, boolean impersonated, Long actorUserId, Set<String> permissions, boolean impersonated, Long actorUserId,
Long actorMembershipId, boolean passwordChangeRequired, Long actorMembershipId, boolean passwordChangeRequired,
Long overseasWarehouseId) implements Principal { Long overseasWarehouseId) implements Principal {
public AuthenticatedUser(Long userId, Long tenantId, Long operatorTenantId, Long membershipId, public AuthenticatedUser(Long userId, Long tenantId, Long operatorTenantId, Long membershipId,
String username, String appCode, String clientType, Set<String> permissions) { String username, String appCode, String clientType, Set<String> permissions) {
this(userId, tenantId, operatorTenantId, membershipId, username, appCode, clientType, this(userId, tenantId, operatorTenantId, membershipId, username, null, appCode, clientType,
permissions, false, null, null, false, null); permissions, false, null, null, false, null);
} }
public AuthenticatedUser(Long userId, Long tenantId, Long operatorTenantId, Long membershipId, public AuthenticatedUser(Long userId, Long tenantId, Long operatorTenantId, Long membershipId,
String username, String appCode, String clientType, Set<String> permissions, String username, String appCode, String clientType, Set<String> permissions,
boolean impersonated, Long actorUserId, Long actorMembershipId, boolean impersonated, Long actorUserId, Long actorMembershipId,
boolean passwordChangeRequired) { boolean passwordChangeRequired) {
this(userId, tenantId, operatorTenantId, membershipId, username, appCode, clientType, this(userId, tenantId, operatorTenantId, membershipId, username, null, appCode, clientType,
permissions, impersonated, actorUserId, actorMembershipId, passwordChangeRequired, null); permissions, impersonated, actorUserId, actorMembershipId, passwordChangeRequired, null);
}
public AuthenticatedUser(Long userId, Long tenantId, Long operatorTenantId, Long membershipId,
String username, String displayName, String appCode, String clientType,
Set<String> permissions, boolean impersonated, Long actorUserId,
Long actorMembershipId, boolean passwordChangeRequired) {
this(userId, tenantId, operatorTenantId, membershipId, username, displayName, appCode, clientType,
permissions, impersonated, actorUserId, actorMembershipId, passwordChangeRequired, null);
}
public AuthenticatedUser(Long userId, Long tenantId, Long operatorTenantId, Long membershipId,
String username, String appCode, String clientType, Set<String> permissions,
boolean impersonated, Long actorUserId, Long actorMembershipId,
boolean passwordChangeRequired, Long overseasWarehouseId) {
this(userId, tenantId, operatorTenantId, membershipId, username, null, appCode, clientType,
permissions, impersonated, actorUserId, actorMembershipId, passwordChangeRequired,
overseasWarehouseId);
} }
public AuthenticatedUser { public AuthenticatedUser {
permissions = permissions == null ? Set.of() : Set.copyOf(permissions); permissions = permissions == null ? Set.of() : Set.copyOf(permissions);
@@ -37,6 +37,7 @@ public class JwtClaimsMapper {
longClaim(jwt, "operator_tenant_id"), longClaim(jwt, "operator_tenant_id"),
longClaim(jwt, "membership_id"), longClaim(jwt, "membership_id"),
username, username,
firstNonBlankNullable(jwt.getClaimAsString("displayName"), jwt.getClaimAsString("display_name")),
appCode, appCode,
jwt.getClaimAsString("client_type"), jwt.getClaimAsString("client_type"),
permissions, permissions,
@@ -87,4 +88,9 @@ public class JwtClaimsMapper {
for (String value : values) if (value != null && !value.isBlank()) return value; for (String value : values) if (value != null && !value.isBlank()) return value;
return "unknown"; return "unknown";
} }
private String firstNonBlankNullable(String... values) {
for (String value : values) if (value != null && !value.isBlank()) return value.trim();
return null;
}
} }
@@ -13,8 +13,8 @@ final class FinanceOperatorSnapshot {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof AuthenticatedUser user if (authentication != null && authentication.getPrincipal() instanceof AuthenticatedUser user
&& user.userId() != null && user.userId() == operatorId && user.userId() != null && user.userId() == operatorId
&& StringUtils.hasText(user.username())) { && (StringUtils.hasText(user.displayName()) || StringUtils.hasText(user.username()))) {
return user.username().trim(); return StringUtils.hasText(user.displayName()) ? user.displayName().trim() : user.username().trim();
} }
return null; return null;
} }
@@ -41,23 +41,26 @@ public class ReceivableStatisticsService {
LocalDate from = month.atDay(1); LocalDate from = month.atDay(1);
LocalDate to = month.plusMonths(1).atDay(1); LocalDate to = month.plusMonths(1).atDay(1);
StringBuilder sql = new StringBuilder(""" StringBuilder sql = new StringBuilder("""
SELECT tenant_id, customer_name_snapshot, SELECT s.tenant_id,
SUM(total_amount) total_receivable, COALESCE(p.customer_name_snapshot, MAX(s.customer_name_snapshot)) customer_name_snapshot,
SUM(paid_amount) total_received, SUM(s.total_amount) total_receivable,
SUM(outstanding_amount) total_outstanding SUM(s.paid_amount) total_received,
FROM fin_settlement SUM(s.outstanding_amount) total_outstanding
WHERE direction='RECEIVABLE' FROM fin_settlement s
AND status<>'VOIDED' LEFT JOIN fin_customer_settlement_profile p
AND currency_code='CNY' ON p.tenant_id=s.tenant_id AND p.status='ACTIVE'
AND generated_date>=? WHERE s.direction='RECEIVABLE'
AND generated_date<? AND s.status<>'VOIDED'
AND s.currency_code='CNY'
AND s.generated_date>=?
AND s.generated_date<?
"""); """);
List<Object> args = new ArrayList<>(List.of(from, to)); List<Object> args = new ArrayList<>(List.of(from, to));
if (customerId != null) { if (customerId != null) {
sql.append(" AND tenant_id=?"); sql.append(" AND s.tenant_id=?");
args.add(customerId); args.add(customerId);
} }
sql.append(" GROUP BY tenant_id,customer_name_snapshot ORDER BY customer_name_snapshot,tenant_id"); sql.append(" GROUP BY s.tenant_id,p.customer_name_snapshot ORDER BY customer_name_snapshot,s.tenant_id");
return jdbc.query(sql.toString(), (rs, rowNum) -> checked(new ReceivableStatisticsRow( return jdbc.query(sql.toString(), (rs, rowNum) -> checked(new ReceivableStatisticsRow(
rs.getLong("tenant_id"), rs.getString("customer_name_snapshot"), CURRENCY, rs.getLong("tenant_id"), rs.getString("customer_name_snapshot"), CURRENCY,
rs.getBigDecimal("total_receivable"), rs.getBigDecimal("total_received"), rs.getBigDecimal("total_receivable"), rs.getBigDecimal("total_received"),
@@ -40,8 +40,8 @@ class ReceivableStatisticsServiceTest {
assertThat(result.records()).containsExactly(rows.get(0)); assertThat(result.records()).containsExactly(rows.get(0));
var sql = org.mockito.ArgumentCaptor.forClass(String.class); var sql = org.mockito.ArgumentCaptor.forClass(String.class);
verify(jdbc).query(sql.capture(), any(RowMapper.class), any(Object[].class)); verify(jdbc).query(sql.capture(), any(RowMapper.class), any(Object[].class));
assertThat(sql.getValue()).contains("direction='RECEIVABLE'", "status<>'VOIDED'", "currency_code='CNY'", assertThat(sql.getValue()).contains("s.direction='RECEIVABLE'", "s.status<>'VOIDED'", "s.currency_code='CNY'",
"generated_date>=?", "generated_date<?", "GROUP BY tenant_id,customer_name_snapshot"); "s.generated_date>=?", "s.generated_date<?", "fin_customer_settlement_profile", "GROUP BY s.tenant_id,p.customer_name_snapshot");
} }
@Test @Test
@@ -28,6 +28,16 @@ class FinanceOperatorSnapshotTest {
assertThat(FinanceOperatorSnapshot.currentName(72L)).isNull(); assertThat(FinanceOperatorSnapshot.currentName(72L)).isNull();
} }
@Test
void prefersAuthenticatedDisplayNameOverLoginUsername() {
AuthenticatedUser user = new AuthenticatedUser(71L, null, null, null,
"12555214478", "廖万昌", "MERCHANT", "WEB", Set.of(), false, null, null, false);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user, null));
assertThat(FinanceOperatorSnapshot.currentName(71L)).isEqualTo("廖万昌");
}
@Test @Test
void labelsSystemGeneratedEntriesWithoutInventingAnOperator() { void labelsSystemGeneratedEntriesWithoutInventingAnOperator() {
assertThat(FinanceOperatorSnapshot.currentName(0L)).isEqualTo("系统"); assertThat(FinanceOperatorSnapshot.currentName(0L)).isEqualTo("系统");