@ -0,0 +1,374 @@
package com.company.agentfoundation.crm.report ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import org.springframework.dao.EmptyResultDataAccessException ;
import org.springframework.http.HttpStatus ;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource ;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate ;
import org.springframework.stereotype.Service ;
import org.springframework.web.server.ResponseStatusException ;
import java.math.BigDecimal ;
import java.math.RoundingMode ;
import java.time.LocalDate ;
import java.time.LocalDateTime ;
import java.time.ZoneId ;
import java.time.format.DateTimeFormatter ;
import java.util.ArrayList ;
import java.util.LinkedHashMap ;
import java.util.LinkedHashSet ;
import java.util.List ;
import java.util.Locale ;
import java.util.Map ;
import java.util.Set ;
@Service
public class CustomerDailyReportService {
private static final ZoneId BUSINESS_ZONE = ZoneId . of ( " Asia/Shanghai " ) ;
private static final DateTimeFormatter SQL_TIME = DateTimeFormatter . ofPattern ( " yyyy-MM-dd HH:mm:ss " ) ;
private static final String CUSTOMER = " xuebang_db.xuebang_api_customer_findcustomerlist " ;
private static final String FOLLOWUP = " xuebang_db.xuebang_api_customer_customerfollowup_findbyinstitution " ;
private static final String APPOINTMENT = " xuebang_db.xuebang_api_customer_appiontmentmanage_findbyinstitution " ;
private static final String CONTRACT = " xuebang_db.xuebang_api_contract_findbyinstitution " ;
private static final String USER = " xuebang_db.xuebang_api_user_listbyinstitution " ;
private static final String USER_COMPOSITE = " xuebang_db.xuebang_api_user_composite_search " ;
private static final int WARNING_HOURS = 24 ;
private static final int CRITICAL_HOURS = 72 ;
private static final double LOW_CONVERSION = 0 . 35D ;
private static final double COMPLETENESS_TARGET = 0 . 98D ;
private final NamedParameterJdbcTemplate jdbc ;
private final ObjectMapper objectMapper ;
public CustomerDailyReportService ( NamedParameterJdbcTemplate jdbc , ObjectMapper objectMapper ) {
this . jdbc = jdbc ;
this . objectMapper = objectMapper ;
}
public Map < String , Object > generate ( CustomerDailyReportRequest request ) {
if ( request = = null | | clean ( request . wecomUserId ( ) ) . isEmpty ( ) ) {
throw new IllegalArgumentException ( " wecomUserId is required " ) ;
}
LocalDate today = LocalDate . now ( BUSINESS_ZONE ) ;
LocalDate startDate = request . startDate ( ) = = null ? today : request . startDate ( ) ;
LocalDate endDate = request . endDate ( ) = = null ? startDate : request . endDate ( ) ;
if ( endDate . isBefore ( startDate ) | | startDate . plusYears ( 1 ) . isBefore ( endDate ) ) {
throw new IllegalArgumentException ( " invalid report date range " ) ;
}
Caller caller = resolveCaller ( request . wecomUserId ( ) ) ;
if ( ! caller . resolved ( ) ) {
throw new ResponseStatusException ( HttpStatus . FORBIDDEN , " 无权限查看:企业微信账号未完成人员映射 " ) ;
}
Scope scope = deriveScope ( caller ) ;
MapSqlParameterSource params = parameters ( scope , startDate . atStartOfDay ( ) , endDate . plusDays ( 1 ) . atStartOfDay ( ) ) ;
Map < String , Object > counts = jdbc . queryForMap ( funnelSql ( ) , params ) ;
long added = number ( counts , " new_count " ) ;
long assigned = number ( counts , " assigned_count " ) ;
long firstCall = number ( counts , " first_call_count " ) ;
long appointment = number ( counts , " appointment_count " ) ;
long visit = number ( counts , " visit_count " ) ;
long deal = number ( counts , " deal_count " ) ;
long converted = number ( counts , " converted_count " ) ;
long invalid = number ( counts , " invalid_count " ) ;
List < Map < String , Object > > funnel = new ArrayList < > ( ) ;
stage ( funnel , " NEW " , " 新增客资 " , added , null ) ;
stage ( funnel , " ASSIGNED " , " 分配 " , assigned , added ) ;
stage ( funnel , " FIRST_CALL " , " 首电 " , firstCall , assigned ) ;
stage ( funnel , " APPOINTMENT " , " 邀约 " , appointment , firstCall ) ;
stage ( funnel , " VISIT " , " 到店 " , visit , appointment ) ;
stage ( funnel , " DEAL " , " 成交 " , deal , visit ) ;
List < Map < String , Object > > alerts = loadAlerts ( params ) ;
long noFollowup = alerts . stream ( ) . mapToLong ( row - > ( ( Number ) row . get ( " customerCount " ) ) . longValue ( ) ) . sum ( ) ;
long over3Days = alerts . stream ( ) . mapToLong ( row - > ( ( Number ) row . get ( " over3DaysCount " ) ) . longValue ( ) ) . sum ( ) ;
Map < String , Object > completeness = jdbc . queryForMap ( completenessSql ( ) , params ) ;
long total = number ( completeness , " total_count " ) ;
long missingName = number ( completeness , " missing_name " ) ;
long missingContact = number ( completeness , " missing_contact " ) ;
long missingSource = number ( completeness , " missing_source " ) ;
long missingCampus = number ( completeness , " missing_campus " ) ;
long missingIntention = number ( completeness , " missing_intention " ) ;
double completenessRate = total = = 0 ? 1D : round (
( total * 5D - missingName - missingContact - missingSource - missingCampus - missingIntention ) / ( total * 5D ) ) ;
Map < String , Object > result = new LinkedHashMap < > ( ) ;
result . put ( " reportWindow " , Map . of ( " startDate " , startDate , " endDate " , endDate , " zoneId " , BUSINESS_ZONE . getId ( ) ) ) ;
result . put ( " scope " , scopeMap ( scope ) ) ;
result . put ( " caller " , callerMap ( caller ) ) ;
result . put ( " coreMetrics " , Map . of (
" added " , added , " assigned " , assigned , " followed " , firstCall , " firstCall " , firstCall ,
" converted " , converted , " invalid " , invalid ) ) ;
result . put ( " funnel " , funnel ) ;
result . put ( " exceptionMonitor " , Map . of (
" noFollowupCount " , noFollowup , " over24HoursCount " , noFollowup ,
" over3DaysCount " , over3Days , " alerts " , alerts ) ) ;
result . put ( " specificationCheck " , Map . of (
" completenessRate " , completenessRate , " targetRate " , COMPLETENESS_TARGET ,
" passed " , completenessRate > = COMPLETENESS_TARGET ,
" missingFields " , Map . of ( " customerName " , missingName , " contact " , missingContact ,
" source " , missingSource , " campus " , missingCampus , " intention " , missingIntention ) ) ) ;
return result ;
}
private Caller resolveCaller ( String wecomUserId ) {
String safeUserId = clean ( wecomUserId ) ;
try {
return jdbc . queryForObject ( " " "
SELECT COALESCE ( NULLIF ( u . user_id , ' ' ) , c . boss_user_id , ' ' ) user_id ,
COALESCE ( NULLIF ( u . name , ' ' ) , c . name , ' ' ) name ,
COALESCE ( u . organization_id , ' ' ) organization_id ,
COALESCE ( u . organization_name , ' ' ) organization_name ,
COALESCE ( u . role_names , ' ' ) role_names ,
COALESCE ( u . user_manage_organizations , ' ' ) manage_organizations
FROM % s c LEFT JOIN % s u ON u . user_id = c . boss_user_id
WHERE c . wxqy_user_id = : wecomUserId LIMIT 1
" " " .formatted(USER_COMPOSITE, USER), Map.of( " wecomUserId " , safeUserId),
( rs , row ) - > caller ( safeUserId , rs . getString ( " user_id " ) , rs . getString ( " name " ) ,
rs . getString ( " organization_id " ) , rs . getString ( " organization_name " ) ,
rs . getString ( " role_names " ) , rs . getString ( " manage_organizations " ) , " WECOM_USER_ID " , " " ) ) ;
} catch ( EmptyResultDataAccessException ignored ) {
return resolveLegacy ( safeUserId ) ;
}
}
private Caller resolveLegacy ( String wecomUserId ) {
Map < String , Object > legacy ;
try {
legacy = jdbc . queryForMap ( " " "
SELECT employee_name , COALESCE ( xuebang_account , ' ' ) xuebang_account ,
COALESCE ( data_scope_type , ' ' ) data_scope_type
FROM employee_directory WHERE wecom_user_id = : wecomUserId LIMIT 1
" " " , Map.of( " wecomUserId " , wecomUserId));
} catch ( EmptyResultDataAccessException ignored ) {
return Caller . unresolved ( wecomUserId ) ;
}
String account = text ( legacy , " xuebang_account " ) ;
if ( ! account . isEmpty ( ) ) {
try {
Map < String , Object > row = jdbc . queryForMap ( " SELECT * FROM " + USER + " WHERE account=:account LIMIT 1 " ,
Map . of ( " account " , account ) ) ;
return callerFromRow ( wecomUserId , row , " LEGACY_ACCOUNT " , text ( legacy , " data_scope_type " ) ) ;
} catch ( EmptyResultDataAccessException ignored ) {
/ / Continue with the unique - name fallback .
}
}
String normalized = normalizeName ( text ( legacy , " employee_name " ) ) ;
List < Map < String , Object > > matches = jdbc . queryForList ( " " "
SELECT user_id , name , organization_id , organization_name , role_names , user_manage_organizations
FROM % s WHERE COALESCE ( has_dimission , '0' ) = '0'
" " " .formatted(USER), Map.of()).stream()
. filter ( row - > normalized . equals ( normalizeName ( text ( row , " name " ) ) ) )
. toList ( ) ;
return matches . size ( ) = = 1
? callerFromRow ( wecomUserId , matches . get ( 0 ) , " NORMALIZED_UNIQUE_NAME " , text ( legacy , " data_scope_type " ) )
: Caller . unresolved ( wecomUserId ) ;
}
private Caller callerFromRow ( String wecomUserId , Map < String , Object > row , String method , String legacyScope ) {
return caller ( wecomUserId , text ( row , " user_id " ) , text ( row , " name " ) , text ( row , " organization_id " ) ,
text ( row , " organization_name " ) , text ( row , " role_names " ) , text ( row , " user_manage_organizations " ) ,
method , legacyScope ) ;
}
private Caller caller ( String wecomUserId , String userId , String name , String organizationId ,
String organizationName , String roles , String managed , String method , String legacyScope ) {
return new Caller ( wecomUserId , ! clean ( userId ) . isEmpty ( ) , clean ( userId ) , clean ( name ) , clean ( organizationId ) ,
clean ( organizationName ) , clean ( roles ) , clean ( managed ) , method , clean ( legacyScope ) ) ;
}
private Scope deriveScope ( Caller caller ) {
if ( caller . roles ( ) . contains ( " 集团管理员 " ) | | " ALL_ORGANIZATION " . equalsIgnoreCase ( caller . legacyScope ( ) ) ) {
return new Scope ( " ALL_ORGANIZATION " , true , false , List . of ( ) , List . of ( ) , caller . userId ( ) , caller . name ( ) ) ;
}
Organizations managed = parseOrganizations ( caller . managedOrganizations ( ) ) ;
boolean organizationRole = caller . roles ( ) . contains ( " 城市总 " ) | | caller . roles ( ) . contains ( " 馆长 " )
| | caller . roles ( ) . contains ( " 教学主管 " ) | | " MULTI_VENUE " . equalsIgnoreCase ( caller . legacyScope ( ) )
| | " VENUE " . equalsIgnoreCase ( caller . legacyScope ( ) ) ;
if ( organizationRole | | ! managed . ids ( ) . isEmpty ( ) | | ! managed . names ( ) . isEmpty ( ) ) {
Set < String > ids = new LinkedHashSet < > ( managed . ids ( ) ) ;
Set < String > names = new LinkedHashSet < > ( managed . names ( ) ) ;
if ( ! caller . organizationId ( ) . isEmpty ( ) ) ids . add ( caller . organizationId ( ) ) ;
if ( ! caller . organizationName ( ) . isEmpty ( ) ) names . add ( caller . organizationName ( ) ) ;
if ( ids . isEmpty ( ) & & names . isEmpty ( ) ) throw forbidden ( ) ;
return new Scope ( ids . size ( ) + names . size ( ) > 2 ? " MULTI_ORGANIZATION " : " ORGANIZATION " , false , false ,
List . copyOf ( ids ) , List . copyOf ( names ) , caller . userId ( ) , caller . name ( ) ) ;
}
if ( caller . organizationId ( ) . isEmpty ( ) & & caller . organizationName ( ) . isEmpty ( ) ) throw forbidden ( ) ;
return new Scope ( " SELF_OR_ASSIGNED " , false , true , List . of ( ) , List . of ( ) , caller . userId ( ) , caller . name ( ) ) ;
}
private ResponseStatusException forbidden ( ) {
return new ResponseStatusException ( HttpStatus . FORBIDDEN , " 无权限查看:账号不在可查询的组织范围内 " ) ;
}
private Organizations parseOrganizations ( String raw ) {
if ( raw = = null | | raw . isBlank ( ) ) return new Organizations ( List . of ( ) , List . of ( ) ) ;
try {
JsonNode root = objectMapper . readTree ( raw ) ;
Set < String > ids = new LinkedHashSet < > ( ) ;
Set < String > names = new LinkedHashSet < > ( ) ;
if ( root . isArray ( ) ) root . forEach ( node - > addOrganization ( node , ids , names ) ) ;
else addOrganization ( root , ids , names ) ;
return new Organizations ( List . copyOf ( ids ) , List . copyOf ( names ) ) ;
} catch ( Exception ignored ) {
return new Organizations ( List . of ( ) , List . of ( ) ) ;
}
}
private void addOrganization ( JsonNode node , Set < String > ids , Set < String > names ) {
if ( node = = null | | ! node . isObject ( ) ) return ;
addFirst ( node , ids , " organizationId " , " organization_id " , " id " ) ;
addFirst ( node , names , " organizationName " , " organization_name " , " name " ) ;
}
private void addFirst ( JsonNode node , Set < String > target , String . . . fields ) {
for ( String field : fields ) {
JsonNode value = node . get ( field ) ;
if ( value ! = null & & ! value . asText ( ) . isBlank ( ) ) {
target . add ( value . asText ( ) . trim ( ) ) ;
return ;
}
}
}
private List < Map < String , Object > > loadAlerts ( MapSqlParameterSource params ) {
return jdbc . query ( alertSql ( ) , params , ( rs , row ) - > {
long count = rs . getLong ( " customer_count " ) ;
long critical = rs . getLong ( " critical_count " ) ;
String advisorName = rs . getString ( " advisor_name " ) ;
Map < String , Object > alert = new LinkedHashMap < > ( ) ;
alert . put ( " advisorId " , rs . getString ( " advisor_id " ) ) ;
alert . put ( " advisorName " , advisorName ) ;
alert . put ( " customerCount " , count ) ;
alert . put ( " over3DaysCount " , critical ) ;
alert . put ( " maxStaleHours " , rs . getLong ( " max_stale_hours " ) ) ;
alert . put ( " level " , critical > 0 ? " CRITICAL " : " WARNING " ) ;
alert . put ( " message " , " 警告:顾问%s 名下 %d 个客资超过24小时未跟进, 其中 %d 个已超过3天 "
. formatted ( advisorName , count , critical ) ) ;
return alert ;
} ) ;
}
private MapSqlParameterSource parameters ( Scope scope , LocalDateTime start , LocalDateTime end ) {
return new MapSqlParameterSource ( )
. addValue ( " startTime " , SQL_TIME . format ( start ) ) . addValue ( " endTime " , SQL_TIME . format ( end ) )
. addValue ( " scopeAll " , scope . all ( ) ? 1 : 0 ) . addValue ( " scopeAdvisor " , scope . advisorOnly ( ) ? 1 : 0 )
. addValue ( " organizationIds " , scope . organizationIds ( ) . isEmpty ( ) ? List . of ( " __NONE__ " ) : scope . organizationIds ( ) )
. addValue ( " organizationNames " , scope . organizationNames ( ) . isEmpty ( ) ? List . of ( " __NONE__ " ) : scope . organizationNames ( ) )
. addValue ( " callerUserId " , scope . advisorId ( ) ) . addValue ( " callerUserName " , scope . advisorName ( ) )
. addValue ( " warningHours " , WARNING_HOURS ) . addValue ( " criticalHours " , CRITICAL_HOURS ) . addValue ( " maxAlerts " , 100 ) ;
}
private String scopeSql ( ) {
return " " "
( : scopeAll = 1 OR ( : scopeAdvisor = 1 AND ( c . deliver_user_id = : callerUserId OR c . deliver_user_name = : callerUserName
OR c . deliver_user_ids REGEXP CONCAT ( ' ( ^ | [ ^ 0 - 9A - Za - z ] ) ' , : callerUserId , ' ( [ ^ 0 - 9A - Za - z ] | $ ) ' )
OR c . deliver_user_names LIKE CONCAT ( ' % % ' , : callerUserName , ' % % ' ) ) )
OR ( : scopeAdvisor = 0 AND ( c . bl_school_id IN ( : organizationIds ) OR c . bl_school_name IN ( : organizationNames ) ) ) )
" " " ;
}
private String funnelSql ( ) {
return " " "
WITH base AS ( SELECT COALESCE ( NULLIF ( c . customer_id , ' ' ) , c . data_key ) customer_key , c . status , c . deal_status , c . deliver_time
FROM % s c WHERE c . create_time > = : startTime AND c . create_time < : endTime AND % s ) ,
followed AS ( SELECT DISTINCT customer_id FROM % s WHERE create_time < : endTime AND customer_id IS NOT NULL AND customer_id < > ' ' ) ,
appointed AS ( SELECT DISTINCT customer_id FROM % s WHERE record_time < : endTime AND customer_id IS NOT NULL AND customer_id < > ' ' ) ,
visited AS ( SELECT DISTINCT customer_id FROM % s WHERE appointment_confirm_time < : endTime
AND appointment_confirm IN ( ' PARENT_COME ',' STUDENT_COME ',' PARENT_STUDENT_COME ' ) AND customer_id IS NOT NULL AND customer_id < > ' ' ) ,
contracted AS ( SELECT DISTINCT customer_id FROM % s WHERE create_time < : endTime AND customer_id IS NOT NULL AND customer_id < > ' '
AND COALESCE ( contract_refund_status , ' NORMAL ' ) NOT IN ( ' REFUNDED ',' TRANSFER_LESSON ' ) )
SELECT COUNT ( DISTINCT b . customer_key ) new_count ,
COUNT ( DISTINCT CASE WHEN b . deliver_time IS NOT NULL AND b . deliver_time < > ' ' THEN b . customer_key END ) assigned_count ,
COUNT ( DISTINCT CASE WHEN b . deliver_time IS NOT NULL AND b . deliver_time < > ' ' AND f . customer_id IS NOT NULL THEN b . customer_key END ) first_call_count ,
COUNT ( DISTINCT CASE WHEN f . customer_id IS NOT NULL AND a . customer_id IS NOT NULL THEN b . customer_key END ) appointment_count ,
COUNT ( DISTINCT CASE WHEN a . customer_id IS NOT NULL AND v . customer_id IS NOT NULL THEN b . customer_key END ) visit_count ,
COUNT ( DISTINCT CASE WHEN v . customer_id IS NOT NULL AND ( b . deal_status = ' SIGNEUP ' OR ct . customer_id IS NOT NULL ) THEN b . customer_key END ) deal_count ,
COUNT ( DISTINCT CASE WHEN b . deal_status = ' SIGNEUP ' OR ct . customer_id IS NOT NULL THEN b . customer_key END ) converted_count ,
COUNT ( DISTINCT CASE WHEN b . status = '0' THEN b . customer_key END ) invalid_count
FROM base b LEFT JOIN followed f ON f . customer_id = b . customer_key LEFT JOIN appointed a ON a . customer_id = b . customer_key
LEFT JOIN visited v ON v . customer_id = b . customer_key LEFT JOIN contracted ct ON ct . customer_id = b . customer_key
" " " .formatted(CUSTOMER, scopeSql(), FOLLOWUP, APPOINTMENT, APPOINTMENT, CONTRACT);
}
private String alertSql ( ) {
return " " "
SELECT COALESCE ( NULLIF ( c . deliver_user_id , ' ' ) , ' UNASSIGNED ' ) advisor_id ,
COALESCE ( NULLIF ( c . deliver_user_name , ' ' ) , NULLIF ( c . deliver_user_names , ' ' ) , ' 未指定顾问 ' ) advisor_name ,
COUNT ( DISTINCT COALESCE ( NULLIF ( c . customer_id , ' ' ) , c . data_key ) ) customer_count ,
SUM ( CASE WHEN TIMESTAMPDIFF ( HOUR , STR_TO_DATE ( COALESCE ( NULLIF ( c . deliver_time , ' ' ) , c . create_time ) , ' % % Y - % % m - % % d % % H : % % i : % % s ' ) ,
STR_TO_DATE ( : endTime , ' % % Y - % % m - % % d % % H : % % i : % % s ' ) ) > = : criticalHours THEN 1 ELSE 0 END ) critical_count ,
MAX ( TIMESTAMPDIFF ( HOUR , STR_TO_DATE ( COALESCE ( NULLIF ( c . deliver_time , ' ' ) , c . create_time ) , ' % % Y - % % m - % % d % % H : % % i : % % s ' ) ,
STR_TO_DATE ( : endTime , ' % % Y - % % m - % % d % % H : % % i : % % s ' ) ) ) max_stale_hours
FROM % s c LEFT JOIN % s f ON f . customer_id = c . customer_id AND f . create_time < : endTime
WHERE c . create_time < : endTime AND c . status = '1' AND c . deliver_time IS NOT NULL AND c . deliver_time < > ' ' AND f . customer_id IS NULL
AND TIMESTAMPDIFF ( HOUR , STR_TO_DATE ( COALESCE ( NULLIF ( c . deliver_time , ' ' ) , c . create_time ) , ' % % Y - % % m - % % d % % H : % % i : % % s ' ) ,
STR_TO_DATE ( : endTime , ' % % Y - % % m - % % d % % H : % % i : % % s ' ) ) > = : warningHours AND % s
GROUP BY advisor_id , advisor_name ORDER BY critical_count DESC , customer_count DESC , max_stale_hours DESC LIMIT : maxAlerts
" " " .formatted(CUSTOMER, FOLLOWUP, scopeSql());
}
private String completenessSql ( ) {
return " " "
SELECT COUNT ( * ) total_count ,
SUM ( CASE WHEN customer_name IS NULL OR TRIM ( customer_name ) = ' ' THEN 1 ELSE 0 END ) missing_name ,
SUM ( CASE WHEN contact IS NULL OR TRIM ( contact ) = ' ' THEN 1 ELSE 0 END ) missing_contact ,
SUM ( CASE WHEN ( cus_org_name IS NULL OR TRIM ( cus_org_name ) = ' ' ) AND ( cus_type_name IS NULL OR TRIM ( cus_type_name ) = ' ' ) THEN 1 ELSE 0 END ) missing_source ,
SUM ( CASE WHEN bl_school_name IS NULL OR TRIM ( bl_school_name ) = ' ' THEN 1 ELSE 0 END ) missing_campus ,
SUM ( CASE WHEN intention_name IS NULL OR TRIM ( intention_name ) = ' ' THEN 1 ELSE 0 END ) missing_intention
FROM % s c WHERE c . create_time > = : startTime AND c . create_time < : endTime AND % s
" " " .formatted(CUSTOMER, scopeSql());
}
private void stage ( List < Map < String , Object > > stages , String key , String label , long count , Long previous ) {
Double rate = previous = = null ? null : previous = = 0 ? 0D : round ( ( double ) count / previous ) ;
Map < String , Object > stage = new LinkedHashMap < > ( ) ;
stage . put ( " key " , key ) ; stage . put ( " label " , label ) ; stage . put ( " count " , count ) ;
stage . put ( " conversionRate " , rate ) ; stage . put ( " lowConversion " , rate ! = null & & previous > 0 & & rate < LOW_CONVERSION ) ;
stages . add ( stage ) ;
}
private Map < String , Object > scopeMap ( Scope scope ) {
Map < String , Object > map = new LinkedHashMap < > ( ) ;
map . put ( " type " , scope . type ( ) ) ; map . put ( " organizationIds " , scope . organizationIds ( ) ) ;
map . put ( " organizationNames " , scope . organizationNames ( ) ) ; map . put ( " advisorId " , scope . advisorId ( ) ) ;
map . put ( " advisorName " , scope . advisorName ( ) ) ; return map ;
}
private Map < String , Object > callerMap ( Caller caller ) {
Map < String , Object > map = new LinkedHashMap < > ( ) ;
map . put ( " wecomUserId " , caller . wecomUserId ( ) ) ; map . put ( " mappingResolved " , caller . resolved ( ) ) ;
map . put ( " xuebangUserId " , caller . userId ( ) ) ; map . put ( " xuebangUserName " , caller . name ( ) ) ;
map . put ( " xuebangOrganizationId " , caller . organizationId ( ) ) ; map . put ( " xuebangOrganizationName " , caller . organizationName ( ) ) ;
map . put ( " xuebangRoleNames " , caller . roles ( ) ) ; map . put ( " matchMethod " , caller . matchMethod ( ) ) ; return map ;
}
private static long number ( Map < String , Object > row , String key ) {
Object value = row . get ( key ) ; return value instanceof Number number ? number . longValue ( ) : 0L ;
}
private static String text ( Map < String , Object > row , String key ) {
Object value = row . get ( key ) ; return value = = null ? " " : value . toString ( ) . trim ( ) ;
}
private static String clean ( String value ) { return value = = null ? " " : value . trim ( ) ; }
private static String normalizeName ( String value ) {
return clean ( value ) . replaceFirst ( " (?i)^(SH|BJ|HF|GZ|CD|XZ)[ \\ s-]* " , " " )
. replace ( " 跃动体育 " , " " ) . replace ( " 跃动运动馆 " , " " )
. replaceAll ( " [ \\ s_, ·.( ) () \\ -] " , " " ) . toLowerCase ( Locale . ROOT ) ;
}
private static double round ( double value ) {
return BigDecimal . valueOf ( value ) . setScale ( 4 , RoundingMode . HALF_UP ) . doubleValue ( ) ;
}
private record Caller ( String wecomUserId , boolean resolved , String userId , String name , String organizationId ,
String organizationName , String roles , String managedOrganizations , String matchMethod ,
String legacyScope ) {
static Caller unresolved ( String userId ) { return new Caller ( userId , false , " " , " " , " " , " " , " " , " " , " UNMATCHED " , " " ) ; }
}
private record Scope ( String type , boolean all , boolean advisorOnly , List < String > organizationIds ,
List < String > organizationNames , String advisorId , String advisorName ) { }
private record Organizations ( List < String > ids , List < String > names ) { }
}