
导读零工平台做会员制最怕的就是规则写死在代码里——企业发岗必须会员这个政策产品经理今天说要、明天说不要拨号免费要不要限次数运营天天想调。我把所有权益开关收进 base_config 配置表代码只读配置不写死运营改一条配置就生效。这篇讲 VipPrivilegeServiceImpl 怎么实现的以及那个让我改了两次的循环依赖。权益规则长什么样平台现在的会员玩法不算复杂但每一档都牵扯到具体动作企业发岗开了开关后非会员直接拦截拨号联系VIP 免费打普通用户 30 分钟内对同一岗位拨过号就不重复收费防止骚扰有效期按vip_end_time判断过期自动失效。规则不多但变动的频率很高。最早这些判断散在 Controller 和 Service 里一个开关改动要动两三个文件还容易漏。后来统一收敛到IVipPrivilegeService业务方只调一个方法。到期判断别用时间字符串比大小VIP 是否有效核心就一个方法OverridepublicbooleanisVipActive(StringuserId,StringroleCode){if(oConvertUtils.isEmpty(userId)||oConvertUtils.isEmpty(roleCode)){returnfalse;}UmsUserVipvipuserVipService.getUserVip(userId,roleCode);if(vipnull||vip.getVipEndTime()null){returnfalse;}returnvip.getVipEndTime().after(newDate());}看起来平平无奇但有个细节UmsUserVip按(userId, roleCode)存因为同一个用户可能是求职者也是企业主两个身份各自的会员状态独立。角色维度不隔离的话会出现企业会员到期了求职者身份也被连带拦截这种玄学问题。拨号免费VIP 直接放行普通用户 30 分钟内不重复扣isContactFree是权益里最容易出 bug 的地方逻辑是二选一OverridepublicbooleanisContactFree(StringuserId,StringroleCode,StringpostId){if(oConvertUtils.isEmpty(userId)||oConvertUtils.isEmpty(roleCode)){returnfalse;}if(isVipActive(userId,roleCode)isFlagOn(PrivilegeCodes.VIP_CONTACT_FREE,true)){returntrue;}returnhasRecentContact(userId,roleCode,postId);}hasRecentContact查的是这个用户对同一岗位最近 30 分钟内有没有拨号记录有就免费privatebooleanhasRecentContact(StringuserId,StringroleCode,StringpostId){if(oConvertUtils.isEmpty(postId)){returnfalse;}intminutes30;try{BaseConfigconfigconfigService.getConfigByCode(BizConstants.CALL_ENSURE_TIME);if(config!nulloConvertUtils.isNotEmpty(config.getConfigValue())){minutesInteger.parseInt(config.getConfigValue().trim());}}catch(Exceptione){log.warn(读取拨号确认时长失败使用默认 {} 分钟,minutes);}DatesinceDateUtil.offsetMinute(newDate(),-minutes);QueryWrapperqwnewQueryWrapper();qw.eq(post_id,postId).eq(role_code,roleCode).ge(create_time,since);if(BizConstants.ROLE_CODE_COMPANY.equals(roleCode)){qw.eq(post_user_id,userId);}else{qw.eq(user_id,userId);}qw.orderByDesc(create_time).last(LIMIT 1);ListlistcontactMapper.selectList(qw);returnlist!null!list.isEmpty();}注意这里CALL_ENSURE_TIME是从配置表读的——运营把 30 分钟改成 60不用发版。时长解析失败还有兜底默认 30 分钟配置格式写错不会把接口搞挂。配置开关解析1 / true / Y 都认权益开关统一走isFlagOn配置值兼容三种写法运营填什么都行privatebooleanisFlagOn(StringconfigCode,booleandefaultOn){try{BaseConfigcfgconfigService.getConfigByCode(configCode);if(cfgnull||oConvertUtils.isEmpty(cfg.getConfigValue())){returndefaultOn;}Stringvcfg.getConfigValue().trim();return1.equals(v)||true.equalsIgnoreCase(v)||Y.equalsIgnoreCase(v);}catch(Exceptione){log.warn(读取权益配置失败 code{}使用默认值 {},configCode,defaultOn);returndefaultOn;}}defaultOn这个默认值参数很关键像拨号免费这种偏保守的开关默认开像发岗必须会员这种影响全站的开关默认关。默认值的方向代表了业务安全取向别全写 true。踩坑循环依赖Spring 启动直接报错问题现象权益服务一接进来项目启动就失败控制台报BeanCurrentlyInCreationException: The dependencies of some of the beans in the application context form a cycle。排查过程看堆栈VipPrivilegeServiceImpl依赖ContactService拨号服务而ContactService里又要调权益判断来确认免费不免费——两个 Service 互相注入形成环。日志里能明显看到-指向来回跳。定位思路权益判断本质上只需要查拨号记录表这一个数据能力没必要依赖整个 ContactService。把依赖从服务降级为Mapper。最终解决VipPrivilegeServiceImpl里只注入JobPostContactMapper直接查表不依赖 ContactService 的完整逻辑。注释里也写了不依赖 ContactService避免与拨号服务循环依赖。启动恢复正常职责也更干净——权益服务只判断不执行业务动作。可直接复用的清单VIP 状态按(userId, roleCode)双维度存身份隔离到期判断用Date.after(new Date())别用字符串比拨号免费 VIP 放行 OR 近期有拨打记录两个条件独立所有开关读配置表格式兼容1/true/Y带默认值兜底权益服务只注入 Mapper 不注入业务 Service从根上避免循环依赖。这套会员权益校验在 xllg 日结零工系统里。项目源码https://gitee.com/gzqkl/xllg