重构上帝类/God Class
2026-09-07
背景
这是一个Saas系统,其中有一个MerchantService包含了很多业务,使用到了30+Bean。
private final CouponUsageRecordService couponUsageRecordService;
private final AuthenticationComponent authenticationComponent;
private final AmazonS3Service amazonS3Service;
private final MerchantAdminRepository merchantAdminRepository;
private final PasswordEncoder passwordEncoder;
private final CampaignMemberRepository campaignMemberRepository;
private final MerchantCampaignRepository merchantCampaignRepository;
private final MerchantKeyRepository merchantKeyRepository;
private final MemberChopLogRepository memberChopLogRepository;
private final MerchantRoleMenuRepository merchantRoleMenuRepository;
private final MerchantRoleRepository merchantRoleRepository;
private final MerchantRoleActionRepository merchantRoleActionRepository;
private final ApplicationContext applicationContext;
private final AppleIdentifierRepository appleIdentifierRepository;
private final AdminMerchantFeatureMenuRepository adminMerchantFeatureMenuRepository;
private final AdminMerchantFeatureActionRepository adminMerchantFeatureActionRepository;
private final AdminMerchantRepository adminMerchantRepository;
private final ServicePlanRepository servicePlanRepository;
private final SystemCommonProperties systemCommonProperties;
private final MerchantRepository merchantRepository;
private final JwtTokenUtil jwtTokenUtil;
private final FollowUpRepository followUpRepository;
private final CampaignNotificationRepository campaignNotificationRepository;
private final CampaignMemberMessageRepository campaignMemberMessageRepository;
private final VoucherRecordRepository voucherRecordRepository;
private final MerchantVoucherRepository merchantVoucherRepository;
private final CampaignMemberBalanceService campaignMemberBalanceService;
private final MerchantAdminLoginLogRepository merchantAdminLoginLogRepository;
private final CampaignMemberService campaignMemberService;
private final EmailAddressService emailAddressService;
private final MerchantCouponRepository merchantCouponRepository;
// Public方法签名
MerchantFormVo getMerchant(Long merchantId);
void addNewMerchant(NewMerchantParam param);
void updateMerchant(Long id, UpdateMerchantParam param);
MerchantSummaryVo getMerchantSummary();
MerchantDataVo getRedeemInfo(Long targetId);
MerchantDataVo getCameraScanData(Long targetId);
CampaignMemberDataVo getMemberForEarnOrAttend(Long memberId);
void adjustMemberChops(UpdateChopParam updateChopParam, Long memberId);
void adjustMemberChops(UpdateChopParam updateChopParam, Long memberId, MerchantAdmin operator);
void approvePrepaidMember(Long memberId);
Map<String, String> getUploadPresignedUrl(String filename);
void memberChopRedeem(Long memberId);
void memberChopRedeem(Long memberId, MerchantAdmin admin);
void updateMemberRemark(UpdateMemberRemarkParam param);
void updateMemberAdditionalInfo(UpdateMemberAdditionalInfoParam param);
List<MemberNewJoinedVO> getDailyNewMember(SelectDateParam param, Long merchantId);
List<MemberNewJoinedVO> getMonthlyNewMember(Long merchantId);
MerchantKeyVo saveMerchantCidLicense(MerchantKeyParam merchantKeyParam);
MerchantKeyVo getMerchantCidLicense(Long merchantId);
void grantWelcomeGift(UpdateMemberOtherInfo param);
MerchantAdminSummaryVO getMerchantAdminSummary();
InactiveMemberResponse inactiveMember(Long memberId);
AccessToken merchantRegistration(MerchantRegistrationParam merchantRegistrationParam);
BillingServicePlanVo getBillingServicePlan();
List<TopOperatorListVo> getTopOperators(Long merchantId);
List<ExpirationMerchantReminderVo> getExpirationReminderMerchants(LocalDate targetDate);
List<DailyEarnBurnVo> getDailyChopCount(Long merchantId, StartEndDateParam param);
void updateMerchantNameAndPassword(Long merchantAdminId, MerchantEditProfileParam merchantEditProfileParam);
MemberChopLogVo memberRedeemReward(MemberRedeemRewardParam param);
void pushMemberNotification(PushNotificationParam param);
PageInfo<CampaignNotificationListVO> getCampaignNotifications(PageParam pageParam);
void updateMerchantConfiguration(Long merchantId, MerchantConfigurationParam merchantConfigurationParam);
PageInfo<MerchantAdminLoginLogVO> getMerchantAdminLoginHistory(Long merchantId, Long merchantAdminId, PageParam pageParam);
void updateIntegrationChop(IntegrationMemberChopParam param);
void updateIntegrationChops(Long campaignId, List<IntegrationMemberChopParam> param);
CampaignMemberDataVo integrationGetMemberChop(IntegrationMemberChopParam param);分析
拆分
1. 商户基础与账号管理 (Merchant Core & Account)
MerchantService 真正应该保留的核心职责。
getMerchant(Long merchantId)- 获取商户详情addNewMerchant(NewMerchantParam param)- 新增商户updateMerchant(Long id, UpdateMerchantParam param)- 更新商户信息getMerchantSummary()- 获取商户摘要信息merchantRegistration(MerchantRegistrationParam)- 商户注册(返回Token)updateMerchantNameAndPassword(Long, MerchantEditProfileParam)- 更新商户管理员名称和密码saveMerchantCidLicense(MerchantKeyParam)- 保存商户 CID LicensegetMerchantCidLicense(Long merchantId)- 获取商户 CID LicensegetBillingServicePlan()- 获取计费服务套餐updateMerchantConfiguration(Long, MerchantConfigurationParam)- 更新商户配置getExpirationReminderMerchants(LocalDate)- 获取即将过期的商户提醒(偏后台/定时任务)
2. 会员与积分管理 (Member & Chop Management)
MemberManageService包含会员信息的增删改查以及积分(Chop)的调整。
getMemberForEarnOrAttend(Long memberId)- 获取用于赚取或参与活动的会员信息adjustMemberChops(UpdateChopParam, Long)- 调整会员积分(2个重载方法)approvePrepaidMember(Long memberId)- 审批预付会员updateMemberRemark(UpdateMemberRemarkParam)- 更新会员备注updateMemberAdditionalInfo(UpdateMemberAdditionalInfoParam)- 更新会员附加信息getDailyNewMember(SelectDateParam, Long)- 获取每日新增会员getMonthlyNewMember(Long merchantId)- 获取每月新增会员inactiveMember(Long memberId)- 停用会员getTopOperators(Long merchantId)- 获取顶级操作员列表(偏统计)getDailyChopCount(Long, StartEndDateParam)- 获取每日积分增减统计(偏统计)
3. 优惠券与核销业务 (Coupon & Redemption)
CouponRedemptionService包含扫码、核销、奖励发放等交易链路。
getRedeemInfo(Long targetId)- 获取核销信息getCameraScanData(Long targetId)- 获取扫码数据(会员或优惠券)memberChopRedeem(Long memberId)- 会员积分核销(2个重载方法)memberRedeemReward(MemberRedeemRewardParam)- 会员核销奖励updateIntegrationChop(IntegrationMemberChopParam)- 更新集成积分updateIntegrationChops(Long campaignId, List<...>)- 批量更新集成积分integrationGetMemberChop(IntegrationMemberChopParam)- 集成获取会员积分grantWelcomeGift(UpdateMemberOtherInfo)- 发放迎新礼物(偏营销/优惠券)
4. 营销活动与消息通知 (Campaign & Notification)
CampaignNotificationService完全属于独立的营销域。
pushMemberNotification(PushNotificationParam)- 推送会员通知getCampaignNotifications(PageParam)- 获取活动通知列表
5. 权限、安全与基础设施 (Auth, Security & Infra)
MerchantSecurityService属于系统支撑能力,不应与核心业务混杂。
getMerchantAdminSummary()- 获取商户管理员摘要getMerchantAdminLoginHistory(Long, Long, PageParam)- 获取管理员登录日志getUploadPresignedUrl(String filename)- 获取 S3 上传预签名 URL(基础设施)
新问题
在系统中,通常存在两类操作者:
B端商户(Merchant):只能操作自己租户下的数据(如:我的会员、我的优惠券、我的活动)。
SaaS 平台管理员(Platform Admin):拥有上帝视角,可以操作所有商户的数据(如:分配套餐、调整商户权限、查看全局统计)。
目前有些方法(数据)是两边都需要的,会出现权限边界模糊、数据越权风险极高、代码里充斥着大量的 if (isAdmin) 判断的问题。
解决方案
1. 平台管理侧(Platform Admin Services)
职责:专门服务于公司内部的后台管理系统。不受商户数据隔离的限制,负责商户的生命周期、计费和全局管控。
PlatformMerchantManageService(商户生命周期管理)addNewMerchant(平台代商户创建账号)getMerchant(后台查看任意商户详情)updateMerchantConfiguration(后台修改商户配置)saveMerchantCidLicense/getMerchantCidLicense(后台管理 License)
PlatformBillingService(计费与套餐管理)getBillingServicePlan(查看/修改套餐)getExpirationReminderMerchants(后台查看哪些商户快过期了)
PlatformSecurityService(平台级安全审计)getMerchantAdminLoginHistory(后台审计商户管理员的登录日志)
2. 商户端(Merchant Tenant Services)
职责:专门服务于商户后台(App/Web)。所有方法必须强制带上 merchantId 作为数据隔离的边界,确保商户只能看到自己的数据。
MerchantAccountService(商户自身账号管理)merchantRegistration(商户注册账号)updateMerchantNameAndPassword(商户修改自己的密码)getMerchantSummary(商户看自己的摘要)
MerchantCampaignService(商户营销管理)pushMemberNotification(商户给自己的会员发通知)getCampaignNotifications(商户看自己的通知列表)
3. 共享业务域(Shared Domain Services)
3.1 职责
这就是“两边都需要看”的业务。这些 Service 不关心调用者是商户还是平台管理员,它们只关心业务逻辑本身。数据隔离的工作应该交给分开请求接口(即采用 /merchant/xxx 和 /sys/xxx 的独立路径)去做。
MemberManageService(会员管理)getDailyNewMember/getMonthlyNewMember(商户看自己的,平台看全局的)adjustMemberChops/inactiveMember(调整积分、停用)getMemberForEarnOrAttend
CouponRedemptionService(优惠券与核销)getRedeemInfo/getCameraScanData(扫码核销)memberChopRedeem/memberRedeemReward(核销发奖)grantWelcomeGift(发放迎新礼物)
DataStatisticsService(数据统计)getTopOperatorsgetDailyChopCount
3.2 优势
3.2.1 天然的权限与数据隔离边界
商户端接口 (
/merchant/xxx):Controller 层只需配置一个全局拦截器或 AOP,强制校验 JWT 中是否包含merchantId。如果包含,则直接注入到 Service 的参数中。Service 层完全不需要关心权限,只管查数据。平台端接口 (
/sys/xxx):Controller 层校验当前用户是否是PlatformAdmin。如果是,则允许传入merchantId(可选)进行全局或指定查询。
3.2.2 返回数据的差异化(DTO 隔离)
商户和平台管理员对同一个实体的关注点完全不同。
商户看会员:只需要看到自己门店的会员信息、积分。
平台看会员:除了会员信息,可能还需要看到该会员属于哪个商户、商户的套餐类型、甚至是敏感操作日志。
分开接口后,你可以为它们定义完全不同的 VO(如MerchantMemberVO和PlatformMemberVO),互不干扰。
3.2.3网关层(Gateway)的统一管控
在微服务架构下,API 网关可以根据 URL 前缀做不同的路由和鉴权策略。例如,网关可以直接拦截所有 /merchant/ 开头的请求,校验租户 Token;而拦截 /sys/ 开头的请求,校验超级管理员 Token。
未来
以后再有新需求,按照以下流程判断:
它是给谁用的?
如果是平台管理员用来“管商户”的 -> 放入
PlatformMerchantManageService。如果是商户用来“管自己”的 -> 放入
MerchantAccountService。
它是具体的业务动作吗?
如果是会员、优惠券、活动相关的 -> 放入对应的 共享 Domain Service(如
MemberManageService)。
数据隔离怎么做?
在 Controller 层根据登录角色,决定是否把
merchantId传给 Service。