Commit 0ac1d533 by jianan

转介人门户:OIDC 登录与行级隔离,新增仪表盘/保单/在途佣金/月结单/待办接口

parent b81c107f
package com.yd.csf.api.controller;
import com.yd.common.result.Result;
import com.yd.csf.api.security.ReferrerIdentityContext;
import com.yd.csf.service.service.IReferrerPortalService;
import com.yd.csf.service.vo.ReferrerDashboardVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 转介人门户 - 首页指标。
*
* <p>容器内路径 {@code /c/dashboard/**},对外经 context-path 暴露为
* {@code /csf/api/c/dashboard/**},走转介人门户独立安全链。</p>
*/
@RestController
@RequestMapping("/c/dashboard")
public class ReferrerDashboardController {
@Resource
private IReferrerPortalService referrerPortalService;
/**
* 首页指标:本月已发 / 本课税年度累计已发 / 在途佣金 / 生效件数 / 首年 FYP。
*/
@GetMapping("/summary")
public Result<ReferrerDashboardVO> summary() {
String brokerBizId = ReferrerIdentityContext.requireBrokerBizId();
return Result.success(referrerPortalService.getDashboard(brokerBizId));
}
}
package com.yd.csf.api.controller;
import com.yd.common.result.Result;
import com.yd.csf.api.security.ReferrerIdentityContext;
import com.yd.csf.service.service.IReferrerPortalService;
import com.yd.csf.service.vo.ReferrerPipelineVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* 转介人门户 - 在途佣金。
*
* <p>容器内路径 {@code /c/pipeline/**},对外经 context-path 暴露为
* {@code /csf/api/c/pipeline/**},走转介人门户独立安全链。
* 展示待出账的预计发佣及预计到账日;保留/失效翻译为可读文案。</p>
*/
@RestController
@RequestMapping("/c/pipeline")
public class ReferrerPipelineController {
@Resource
private IReferrerPortalService referrerPortalService;
/**
* 在途佣金列表:含预计到账日,按 payout_date 升序。
*/
@GetMapping("/list")
public Result<List<ReferrerPipelineVO>> list() {
String brokerBizId = ReferrerIdentityContext.requireBrokerBizId();
return Result.success(referrerPortalService.listPipeline(brokerBizId));
}
}
package com.yd.csf.api.controller;
import com.yd.common.result.Result;
import com.yd.csf.api.security.ReferrerIdentityContext;
import com.yd.csf.service.service.IReferrerPortalService;
import com.yd.csf.service.vo.ReferrerPolicyCommissionVO;
import com.yd.csf.service.vo.ReferrerPolicyListVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 转介人门户 - 我的保单。
*
* <p>容器内路径 {@code /c/policy/**},对外经 context-path 暴露为
* {@code /csf/api/c/policy/**},走转介人门户独立安全链。
* 生效保单整单展示;退保/失效/拒保分组可见不计件。</p>
*/
@RestController
@RequestMapping("/c/policy")
public class ReferrerPolicyController {
@Resource
private IReferrerPortalService referrerPortalService;
/**
* 我的保单:生效 / 未生效失效两组。
*/
@GetMapping("/list")
public Result<ReferrerPolicyListVO> list() {
String brokerBizId = ReferrerIdentityContext.requireBrokerBizId();
return Result.success(referrerPortalService.listPolicies(brokerBizId));
}
/**
* 保单各年度佣金钻取:首年(FYP/FYC)与续期(RYP/RYC)分列。
*/
@GetMapping("/{policyNo}/commission")
public Result<ReferrerPolicyCommissionVO> commission(@PathVariable String policyNo) {
String brokerBizId = ReferrerIdentityContext.requireBrokerBizId();
return Result.success(referrerPortalService.getPolicyCommission(brokerBizId, policyNo));
}
}
package com.yd.csf.api.controller;
import com.yd.common.result.Result;
import com.yd.csf.api.security.ReferrerIdentityContext;
import com.yd.csf.service.service.IReferrerPortalService;
import com.yd.csf.service.vo.ReferrerStatementDetailVO;
import com.yd.csf.service.vo.ReferrerStatementVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* 转介人门户 - 月结单。
*
* <p>容器内路径 {@code /c/statement/**},对外经 context-path 暴露为
* {@code /csf/api/c/statement/**},走转介人门户独立安全链。
* 仅展示已推送(salary.status=5)的薪资单;头部与逐笔明细各自忠实呈现,不强制二者相等。</p>
*/
@RestController
@RequestMapping("/c/statement")
public class ReferrerStatementController {
@Resource
private IReferrerPortalService referrerPortalService;
/**
* 月结单列表:仅已推送,按月份降序。
*/
@GetMapping("/list")
public Result<List<ReferrerStatementVO>> list() {
String brokerBizId = ReferrerIdentityContext.requireBrokerBizId();
return Result.success(referrerPortalService.listStatements(brokerBizId));
}
/**
* 月结单详情:头部应发/MPF/其他调整/实发/发放日/银行尾号 + 逐笔保单发佣明细。
*/
@GetMapping("/{salaryBizId}")
public Result<ReferrerStatementDetailVO> detail(@PathVariable String salaryBizId) {
String brokerBizId = ReferrerIdentityContext.requireBrokerBizId();
return Result.success(referrerPortalService.getStatementDetail(brokerBizId, salaryBizId));
}
}
package com.yd.csf.api.controller;
import com.yd.common.result.Result;
import com.yd.csf.api.dto.ReferrerTodoNotifyRequest;
import com.yd.csf.api.security.ReferrerIdentityContext;
import com.yd.csf.service.service.IReferrerTodoService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 转介人待办通知留痕接口
*/
@RestController
@RequestMapping("/c/todo")
public class ReferrerTodoController {
@Resource
private IReferrerTodoService referrerTodoService;
/**
* 已通知 / 处理留痕
*/
@PostMapping("/notify")
public Result<Boolean> notify(@Validated @RequestBody ReferrerTodoNotifyRequest request) {
String clientUserBizId = ReferrerIdentityContext.requireClientUserBizId();
referrerTodoService.recordNotify(clientUserBizId, request.getPolicyNo(), request.getTodoType());
return Result.success(Boolean.TRUE);
}
}
\ No newline at end of file
package com.yd.csf.api.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 待办通知留痕请求
*/
@Data
public class ReferrerTodoNotifyRequest {
@NotBlank(message = "保单号不能为空")
private String policyNo;
@NotBlank(message = "待办类型不能为空")
private String todoType;
}
\ No newline at end of file
package com.yd.csf.api.security;
import com.yd.common.exception.BusinessException;
/**
* 当前转介人登录身份上下文。
*
* <p>业务接口只从本类取登录身份,不接收前端身份入参。</p>
*
* <p>身份说明:csf 库的 {@code broker_biz_id}(policy_broker / salary / fortune /
* expected_fortune 等行级归属字段)与 IAM 的 {@code clientUserBizId} 是<b>同一个 ID、
* 同一命名空间</b>,并非两套主键。依据:{@code policy_broker.broker_biz_id} 实际落值为
* {@code client_user_xxx};预计发佣生成逻辑中直接
* {@code resDto.setClientUserBizId(brokerDto.getBrokerBizId())},
* {@code ConditionConfigMapper} 亦以 {@code agent_id AS brokerBizId}。
* 因此行级数据查询直接用登录 {@code clientUserBizId} 作为 {@code brokerBizId},
* 无需任何映射表或手机号反查。</p>
*/
public final class ReferrerIdentityContext {
private static final ThreadLocal<String> CLIENT_USER_BIZ_ID = new ThreadLocal<>();
private ReferrerIdentityContext() {
}
public static void setClientUserBizId(String clientUserBizId) {
CLIENT_USER_BIZ_ID.set(clientUserBizId);
}
public static String getClientUserBizId() {
return CLIENT_USER_BIZ_ID.get();
}
public static String requireClientUserBizId() {
String clientUserBizId = CLIENT_USER_BIZ_ID.get();
if (clientUserBizId == null || clientUserBizId.trim().isEmpty()) {
throw new BusinessException("缺失转介人登录身份");
}
return clientUserBizId;
}
/**
* 当前登录转介人在 csf 行级数据上使用的 {@code brokerBizId}。
*
* <p>当前与 {@link #requireClientUserBizId()} 同值(恒等),语义化方法:
* Controller 取行级归属 ID 应调用本方法,而非直接拿 clientUserBizId。
* 若将来出现独立 broker 命名空间,只需在此桥接一处。</p>
*/
public static String requireBrokerBizId() {
return requireClientUserBizId();
}
public static void clear() {
CLIENT_USER_BIZ_ID.remove();
}
}
\ No newline at end of file
package com.yd.csf.api.security;
import com.yd.common.enums.CommonEnum;
import com.yd.common.result.Result;
import com.yd.user.feign.client.clientuser.ApiClientUserFeignClient;
import com.yd.user.feign.response.clientuser.ApiClientUserIdpLoginResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Objects;
/**
* 转介人门户身份过滤器。
*
* <p>两种身份来源,按优先级:</p>
* <ol>
* <li>请求携带 {@code Authorization: Bearer} 且配置了 JWKS:用 {@link JwtDecoder} 校验
* RS256 + iss + aud,取 {@code sub} 调 yd-user 解析客户端用户,
* 并强制 {@code status=1 && approval_status=CG} 门禁;</li>
* <li>未携带 token 但配置了 {@code referrer.oidc.mock-client-user-biz-id}:注入固定身份,
* 仅供开发期联调(生产不得配置)。</li>
* </ol>
*
* <p>无法建立身份时不写入 SecurityContext,由链内授权规则返回 401。</p>
*/
@Slf4j
public class ReferrerIdentityFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private final ReferrerOidcProperties properties;
private final ApiClientUserFeignClient clientUserFeignClient;
private final JwtDecoder jwtDecoder;
public ReferrerIdentityFilter(ReferrerOidcProperties properties,
ApiClientUserFeignClient clientUserFeignClient,
JwtDecoder jwtDecoder) {
this.properties = properties;
this.clientUserFeignClient = clientUserFeignClient;
this.jwtDecoder = jwtDecoder;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
boolean authenticated = false;
String token = resolveBearerToken(request);
if (StringUtils.hasText(token) && jwtDecoder != null) {
authenticated = authenticateWithJwt(token);
} else if (StringUtils.hasText(token)) {
log.warn("收到 Bearer token 但 JwtDecoder 未初始化:referrer.oidc.jwk-set-uri 是否配置?");
} else if (StringUtils.hasText(properties.getMockClientUserBizId())) {
applyIdentity(properties.getMockClientUserBizId());
authenticated = true;
} else {
log.debug("请求未携带 Bearer token,且未配置 mock 身份");
}
if (!authenticated) {
filterChain.doFilter(request, response);
return;
}
try {
filterChain.doFilter(request, response);
} finally {
ReferrerIdentityContext.clear();
}
}
/**
* 验签并按 sub 解析转介人身份;任何校验失败返回 false(不抛错、不泄露原因)。
*/
private boolean authenticateWithJwt(String token) {
Jwt jwt;
try {
jwt = jwtDecoder.decode(token);
} catch (JwtException | IllegalArgumentException e) {
log.warn("JWT 验签/校验失败:{}", e.getMessage());
return false;
}
String sub = jwt.getSubject();
if (!StringUtils.hasText(sub)) {
log.warn("JWT 缺少 sub claim");
return false;
}
Result<ApiClientUserIdpLoginResponse> result =
clientUserFeignClient.getByIdpSub(properties.getTenantBizId(), sub);
if (Objects.isNull(result)) {
log.warn("调用 yd-user getByIdpSub 失败(Feign 降级返回 null),tenantBizId={} sub={}",
properties.getTenantBizId(), sub);
return false;
}
if (Objects.isNull(result.getData())) {
log.warn("IdP 身份未绑定 client_user 或租户不匹配,tenantBizId={} sub={} code={} msg={}",
properties.getTenantBizId(), sub, result.getCode(), result.getMsg());
return false;
}
ApiClientUserIdpLoginResponse login = result.getData();
if (!Integer.valueOf(1).equals(login.getStatus())) {
log.warn("转介人账号已停用 status={} clientUserBizId={}", login.getStatus(), login.getClientUserBizId());
return false;
}
if (!CommonEnum.APPROVAL_STATUS_CG.getCode().equals(login.getApprovalStatus())) {
log.warn("转介人审批状态不可登录 approvalStatus={} clientUserBizId={}",
login.getApprovalStatus(), login.getClientUserBizId());
return false;
}
applyIdentity(login.getClientUserBizId());
return true;
}
private void applyIdentity(String clientUserBizId) {
ReferrerIdentityContext.setClientUserBizId(clientUserBizId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(clientUserBizId, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private String resolveBearerToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (StringUtils.hasText(header) && header.startsWith(BEARER_PREFIX)) {
return header.substring(BEARER_PREFIX.length()).trim();
}
return null;
}
}
package com.yd.csf.api.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* 转介人门户 OIDC 配置占位。
*
* <p>真实值由 Nacos 提供,开发期可只配置 {@code mockClientUserBizId} 跑通数据接口。</p>
*/
@Configuration
public class ReferrerOidcProperties {
@Value("${referrer.oidc.jwk-set-uri:}")
private String jwkSetUri;
@Value("${referrer.oidc.issuer:}")
private String issuer;
@Value("${referrer.oidc.audience:}")
private String audience;
/**
* 转介人所属租户业务ID(csf 当前为单租户部署,认证时身份解析限定该租户)
*/
@Value("${referrer.oidc.tenant-biz-id:}")
private String tenantBizId;
@Value("${referrer.oidc.mock-client-user-biz-id:}")
private String mockClientUserBizId;
public String getJwkSetUri() {
return jwkSetUri;
}
public String getIssuer() {
return issuer;
}
public String getAudience() {
return audience;
}
public String getTenantBizId() {
return tenantBizId;
}
public String getMockClientUserBizId() {
return mockClientUserBizId;
}
}
\ No newline at end of file
package com.yd.csf.api.security;
import com.yd.user.feign.client.clientuser.ApiClientUserFeignClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 转介人门户独立安全链。
*
* <p>必须沿用 {@link WebSecurityConfigurerAdapter} 老式风格:yd-auth-core 的后台链也是
* Adapter 风格,Spring Security 5.6 不允许 Adapter 与 {@code SecurityFilterChain} Bean 混用,
* 否则启动报 “Found WebSecurityConfigurerAdapter as well as SecurityFilterChain”。</p>
*
* <p>多个 Adapter 凭 {@link Order} 共存:本链 {@code @Order(1)} 优先匹配容器内
* {@code /c/**}(对外 {@code /csf/api/c/**}),其余请求落老链(兜底 {@code /**})。</p>
*/
@Configuration
@Order(1)
public class ReferrerPortalSecurityConfig extends WebSecurityConfigurerAdapter {
private final ReferrerOidcProperties properties;
private final ApiClientUserFeignClient clientUserFeignClient;
public ReferrerPortalSecurityConfig(ReferrerOidcProperties properties,
ApiClientUserFeignClient clientUserFeignClient) {
this.properties = properties;
this.clientUserFeignClient = clientUserFeignClient;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/c/**")
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
// 未携带有效身份时直接 401,避免落到默认 403/登录页
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
// 链内手动 new:不加 @Component,避免被 Boot 注册成容器级 Filter 污染老后台 /** 链
.addFilterBefore(new ReferrerIdentityFilter(properties, clientUserFeignClient, buildJwtDecoder()),
UsernamePasswordAuthenticationFilter.class);
}
/**
* 构建 RS256 JWKS 解码器,校验时间戳 + iss + aud(aud 为数组时要求包含配置 audience)。
* 未配置 jwk-set-uri 时返回 null,过滤器退化为开发期 mock 身份。
*/
private JwtDecoder buildJwtDecoder() {
if (!StringUtils.hasText(properties.getJwkSetUri())) {
return null;
}
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(properties.getJwkSetUri())
.jwsAlgorithm(SignatureAlgorithm.RS256)
.build();
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(new JwtTimestampValidator());
if (StringUtils.hasText(properties.getIssuer())) {
validators.add(new JwtIssuerValidator(properties.getIssuer()));
}
if (StringUtils.hasText(properties.getAudience())) {
String audience = properties.getAudience();
validators.add(new JwtClaimValidator<Collection<?>>("aud",
aud -> aud != null && aud.contains(audience)));
}
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));
return decoder;
}
}
package com.yd.csf.service.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yd.csf.service.model.ReferrerTodo;
/**
* 转介人待办通知留痕表 Mapper
*/
public interface ReferrerTodoMapper extends BaseMapper<ReferrerTodo> {
}
\ No newline at end of file
package com.yd.csf.service.enums;
/**
* 转介人门户佣金四态(design D4.1)。
*
* <p>系统内部出账状态(expected_fortune/fortune 0–7、salary 1–6)不直接对外,
* 统一收敛为转介人语言的四个「钱的位置」。守恒式(互斥且闭合):
* {@code 应得 = 已发 + 在途 + 保留 + 失效}。</p>
*/
public enum ReferrerCommissionStage {
/** 已发:已完成出账、已到手。 */
PAID,
/** 在途:钱会来、尚未到(待出账、部分出账的未付段、分期未到期、未找到来佣 7)。 */
PIPELINE,
/** 保留:暂扣、未说不给。 */
RETAINED,
/** 失效:这笔没了。 */
LAPSED;
/**
* expected_fortune 整行状态 -> 四态(用于只展示剩余待发的在途列表)。
* 不产生 {@link #PAID}:全额出账无独立状态码,已发须按 paid/unpaid 金额判定。
*/
public static ReferrerCommissionStage fromExpectedStatus(String status) {
if ("4".equals(status)) {
return RETAINED;
}
if ("5".equals(status)) {
return LAPSED;
}
// 0/1/2/3/6/7 均按在途;7(未找到来佣)遵 D4.1 归在途,不得判失效。
return PIPELINE;
}
}
package com.yd.csf.service.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 转介人待办通知留痕表
*/
@Getter
@Setter
@TableName("referrer_todo")
public class ReferrerTodo implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("todo_biz_id")
private String todoBizId;
@TableField("broker_biz_id")
private String brokerBizId;
@TableField("policy_no")
private String policyNo;
@TableField("todo_type")
private String todoType;
@TableField("notify_time")
private LocalDateTime notifyTime;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_time")
private LocalDateTime updateTime;
}
\ No newline at end of file
package com.yd.csf.service.service;
import com.yd.csf.service.vo.ReferrerDashboardVO;
import com.yd.csf.service.vo.ReferrerPipelineVO;
import com.yd.csf.service.vo.ReferrerPolicyCommissionVO;
import com.yd.csf.service.vo.ReferrerPolicyListVO;
import com.yd.csf.service.vo.ReferrerStatementDetailVO;
import com.yd.csf.service.vo.ReferrerStatementVO;
import java.util.List;
/**
* 转介人门户只读数据服务。
*
* <p>所有方法均要求调用方传入已解析的转介人 {@code brokerBizId},
* 服务层不接收任何前端身份入参。</p>
*/
public interface IReferrerPortalService {
/**
* 首页指标。
*/
ReferrerDashboardVO getDashboard(String brokerBizId);
/**
* 月结单列表(仅已推送,按月份降序)。
*/
List<ReferrerStatementVO> listStatements(String brokerBizId);
/**
* 月结单详情。
*/
ReferrerStatementDetailVO getStatementDetail(String brokerBizId, String salaryBizId);
/**
* 我的保单(生效 / 未生效失效分组)。
*/
ReferrerPolicyListVO listPolicies(String brokerBizId);
/**
* 保单各年度佣金钻取。
*/
ReferrerPolicyCommissionVO getPolicyCommission(String brokerBizId, String policyNo);
/**
* 在途佣金列表。
*/
List<ReferrerPipelineVO> listPipeline(String brokerBizId);
}
\ No newline at end of file
package com.yd.csf.service.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yd.csf.service.model.ReferrerTodo;
/**
* 转介人待办通知留痕服务
*/
public interface IReferrerTodoService extends IService<ReferrerTodo> {
/**
* 记录一次「已通知」留痕。
*
* @param brokerBizId 登录态转介人业务 ID
* @param policyNo 保单号
* @param todoType 待办类型
*/
void recordNotify(String brokerBizId, String policyNo, String todoType);
}
\ No newline at end of file
package com.yd.csf.service.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yd.csf.service.dao.ReferrerTodoMapper;
import com.yd.csf.service.model.ReferrerTodo;
import com.yd.csf.service.service.IReferrerTodoService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 转介人待办通知留痕服务实现
*/
@Service
public class ReferrerTodoServiceImpl extends ServiceImpl<ReferrerTodoMapper, ReferrerTodo>
implements IReferrerTodoService {
@Override
@Transactional(rollbackFor = Exception.class)
public void recordNotify(String brokerBizId, String policyNo, String todoType) {
LocalDateTime now = LocalDateTime.now();
ReferrerTodo todo = new ReferrerTodo();
todo.setTodoBizId(UUID.randomUUID().toString().replace("-", ""));
todo.setBrokerBizId(brokerBizId);
todo.setPolicyNo(policyNo);
todo.setTodoType(todoType);
todo.setNotifyTime(now);
todo.setCreateTime(now);
todo.setUpdateTime(now);
this.save(todo);
}
}
\ No newline at end of file
package com.yd.csf.service.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 转介人首页指标。
*/
@Data
public class ReferrerDashboardVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 本月已发佣金(HKD)
*/
private BigDecimal monthPaidCommission;
/**
* 本课税年度累计已发佣金(HKD)
*/
private BigDecimal taxYearPaidCommission;
/**
* 在途佣金(HKD,不含失效/未找到来佣)
*/
private BigDecimal pipelineCommission;
/**
* 生效件数
*/
private Long effectivePolicyCount;
/**
* 首年业绩 FYP(HKD,按本人占比折算)
*/
private BigDecimal firstYearFyp;
}
\ No newline at end of file
package com.yd.csf.service.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 转介人在途佣金项。
*/
@Data
public class ReferrerPipelineVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 预计发佣业务ID
*/
private String expectedFortuneBizId;
/**
* 保单号
*/
private String policyNo;
/**
* 产品名称
*/
private String productName;
/**
* 发佣期数(1=第一年...)
*/
private Integer fortunePeriod;
/**
* 发佣名称
*/
private String fortuneName;
/**
* 预计出账日期
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate payoutDate;
/**
* 语义状态码:PAID / PIPELINE / RETAINED / LAPSED(D4.1)
*/
private String stage;
/**
* 在途金额(剩余发放折合港币)
*/
private BigDecimal hkdAmount;
}
\ No newline at end of file
package com.yd.csf.service.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 转介人保单佣金段:一个保单年度的钱按「钱的位置」(D4.1) 拆分后的一段。
*
* <p>部分出账的年度会产出两段:{@code PAID(已出账金额)} + {@code PIPELINE(待出账金额)},
* 保证各段金额相加恒等于该年度应得。</p>
*/
@Data
public class ReferrerPolicyCommissionItemVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 语义状态码:PAID / PIPELINE / RETAINED / LAPSED
*/
private String stage;
/**
* 发佣期数(1=第一年...)
*/
private Integer fortunePeriod;
/**
* 年度文案:首年 / 第2年 / 第3年...
*/
private String periodLabel;
/**
* 发佣名称
*/
private String fortuneName;
/**
* 保留/失效原因(财务登记,未登记时为空,前端不臆造)
*/
private String statusDesc;
/**
* 该段金额(HKD)
*/
private BigDecimal amount;
/**
* 预计出账日
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate payoutDate;
/**
* 实际出账日
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate actualPayoutDate;
}
package com.yd.csf.service.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 转介人保单佣金钻取结果。
*/
@Data
public class ReferrerPolicyCommissionVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 保单号
*/
private String policyNo;
/**
* 首年佣金(FYC,HKD)
*/
private java.math.BigDecimal firstYearCommission;
/**
* 续期佣金(RYC,HKD)
*/
private java.math.BigDecimal renewalCommission;
/**
* 各年度佣金明细
*/
private List<ReferrerPolicyCommissionItemVO> items;
}
\ No newline at end of file
package com.yd.csf.service.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 转介人名下保单分组。
*/
@Data
public class ReferrerPolicyListVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 生效保单(计件计业绩)
*/
private List<ReferrerPolicyVO> effectivePolicies;
/**
* 未生效/已失效保单(可见不计件计业绩)
*/
private List<ReferrerPolicyVO> inactivePolicies;
}
\ No newline at end of file
package com.yd.csf.service.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 转介人名下保单。
*/
@Data
public class ReferrerPolicyVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 保单号
*/
private String policyNo;
/**
* 保單持有人
*/
private String policyHolder;
/**
* 受保人
*/
private String insured;
/**
* 产品名称
*/
private String productName;
/**
* 保险公司
*/
private String insuranceCompany;
/**
* 保单币种
*/
private String currency;
/**
* 全额保费(承保规模,原币)
*/
private BigDecimal fullPremium;
/**
* 全额保费(HKD)
*/
private BigDecimal fullPremiumHkd;
/**
* 我的介绍费占比
*/
private String myRatio;
/**
* 保单状态
*/
private String status;
}
\ No newline at end of file
package com.yd.csf.service.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
* 转介人月结单详情。
*/
@Data
public class ReferrerStatementDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 薪资单业务ID
*/
private String salaryBizId;
/**
* 薪资月份 yyyyMM
*/
private String month;
/**
* 发放编号
*/
private String salaryNo;
/**
* 应发总金额(HKD)
*/
private BigDecimal grossAmount;
/**
* MPF事项总金额(HKD)
*/
private BigDecimal mpfAmount;
/**
* 其他调整总金额(HKD)
*/
private BigDecimal otherAmount;
/**
* 实发总金额(HKD)
*/
private BigDecimal paidAmount;
/**
* 发放日
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate payoutDate;
/**
* 收款银行名称
*/
private String bankName;
/**
* 账户尾号
*/
private String accountEndNo;
/**
* 逐笔保单明细
*/
private List<ReferrerStatementItemVO> items;
}
\ No newline at end of file
package com.yd.csf.service.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 转介人月结单逐笔明细项。
*/
@Data
public class ReferrerStatementItemVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 保单号
*/
private String policyNo;
/**
* 保單持有人
*/
private String policyHolder;
/**
* 保险公司
*/
private String insuranceCompany;
/**
* 产品名称
*/
private String productName;
/**
* 发佣期数(1=第一年...)
*/
private Integer fortunePeriod;
/**
* 发佣名称
*/
private String fortuneName;
/**
* 转介人介绍费占比
*/
private String brokerRatio;
/**
* 原币种
*/
private String originalCurrency;
/**
* 原币种金额
*/
private BigDecimal originalAmount;
/**
* 原币种→港币汇率
*/
private BigDecimal originalToHkdRate;
/**
* 折算港币金额
*/
private BigDecimal hkdAmount;
}
\ No newline at end of file
package com.yd.csf.service.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 转介人月结单列表项。
*/
@Data
public class ReferrerStatementVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 薪资单业务ID
*/
private String salaryBizId;
/**
* 薪资月份 yyyyMM
*/
private String month;
/**
* 发放编号
*/
private String salaryNo;
/**
* 应发总金额(HKD)
*/
private BigDecimal grossAmount;
/**
* MPF事项总金额(HKD)
*/
private BigDecimal mpfAmount;
/**
* 其他调整总金额(HKD)
*/
private BigDecimal otherAmount;
/**
* 实发总金额(HKD)
*/
private BigDecimal paidAmount;
/**
* 发放日(取当月已出账头日期)
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate payoutDate;
}
\ No newline at end of file
-- 转介人待办通知留痕表 DDL
-- 执行前确认数据库名称
CREATE TABLE IF NOT EXISTS `referrer_todo` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`todo_biz_id` VARCHAR(64) NOT NULL COMMENT '待办留痕业务ID',
`broker_biz_id` VARCHAR(64) NOT NULL COMMENT '转介人业务ID(登录态注入)',
`policy_no` VARCHAR(64) NOT NULL COMMENT '保单号快照',
`todo_type` VARCHAR(32) NOT NULL COMMENT '待办类型:续期/失效/生日',
`notify_time` DATETIME COMMENT '通知时间',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_todo_biz_id` (`todo_biz_id`),
INDEX `idx_broker_biz_id` (`broker_biz_id`),
INDEX `idx_policy_no` (`policy_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='转介人待办通知留痕表';
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment