写点什么

如何设计一套基于 API 的会员系统

作者:Kevin_913
  • 2023-10-19
    广东
  • 本文字数:2568 字

    阅读完需:约 8 分钟

这一篇文章将完整的描述如何设计一个基于 api 的会员系统,从数据库到代码的实现。

需求

允许用户购买单次,也可以允许用户购买会员,这里以 AI 模型为例子,大致效果图如下。



1、用户可以单次购买其中某一个模型。


2、用户可以购买某一个模型的会员。

模型设计

  • 模型:模型相关信息,例如上图所示的Chatgpt,基础表。

  • 会员表:会员相关信息,例如上图所示的Chatgpt会员,基础表。

  • 模型定价表:模型单价,例如上图所示的20次/元,基础表。

  • 会员定价表:会员权益相关信息,例如上图所示1000次/188元,基础表。

  • 这里可以衍生出会员可以按照每个月来算 quota。

  • 这里可以设置不同会员,例如年卡,季卡,月卡等。

  • 用户:用户相关信息,业务表。

  • 订单表:订单相关信息,业务表。

  • 用户按次购买的订单

  • 用户按会员购买的订单

  • 用户单次购买表:由订单生成,结合定价表计算出此次订单用户可以使用的额度,业务表。

  • 用户会员表:由订单生成,结合模型定价表计算出此次订单用户可以使用的额度,以及到期时间,业务表。

  • APIClient 表:保存用户用来访问 api 的 apikey 和 apisecret,业务表。

  • Usage 表:用户使用记录,业务表。

数据库设计


代码实现

创建订单

创建一个订单的时候,需要将下游表放到一个事务同时创建,可以通过工厂模式来设计下游表的创建。


  • 订单创建示例代码


@Transactionalpublic long saveApiOrder(ApiOrder domain) {    long orderId = idGenerator.nextId();    domain.setCreatedAt(new Date());    domain.setId(orderId);    compositiveCalService.saveUsagePlan(domain);// save related domain.    return apiOrderMapper.saveApiOrder(domain);}
复制代码


  • CompositiveCalculationService 示例代码


public class CompositiveCalculationService implements CalculationService {    private final CalculationService clientCalculationService;    private final CalculationService memberCalculationService;
public int saveUsagePlan(ApiOrder order) { return getCalculationService(order).saveUsagePlan(order); } private CalculationService getCalculationService(ApiOrder order) { log.info("order type is {}", order.getType()); if (order.getType().equalsIgnoreCase("client")) { return clientCalculationService; } return memberCalculationService;
}}
复制代码


  • ClientCalculationServiceImpl 示例代码


public class ClientCalculationServiceImpl implements CalculationService {    private final ClientMapper clientMapper;    private final ModelMapper modelMapper;    private final UserClientMapper userClientMapper;
public int saveUsagePlan(ApiOrder order) { Model model = modelMapper.getModelById(order.getModelId()); int totalCount = model.getQuota() * order.getPayment(); int clientId = clientMapper.saveClient(new Client().withDefault().withOrder(order)); UserClient uc = new UserClient() .withDefault() .withOrder(order) .withClientId(clientId) .withQuota(totalCount);
return userClientMapper.saveUserClient(uc); }
}
复制代码


  • MemberCalculationServiceImpl 示例代码


public class MemberCalculationServiceImpl implements CalculationService {    private final UserMemberMapper userMemberMapper;    private final MemberRuleMapper memberRuleMapper;
public int saveUsagePlan(ApiOrder order) { MemberRule mr = memberRuleMapper.getRuleById(order.getMemberId()); return userMemberMapper.saveUserMember(new UserMember().withDefault().withOrder(order).withQuota(mr.getQuota())); }}
复制代码

计算用户可使用次数

  • 查询会员可使用次数


select sum(quota) as c from user_member where user_id=#{userId} and model_id=#{modelId} and expired_at>now()
复制代码


  • 查询单次购买可使用次数


select sum(quota) as c from user_client where user_id=#{userId} and model_id=#{modelId}
复制代码


  • 查询用户使用次数


select d.model_id, count(1) as sum from usage a " +            "inner join model d on a.model_id=d.id " +            "where a.user_id=#{userId} and d.id=#{modelId} " +            "group by d.model_id
复制代码


  • 查询用户最终可用次数


public int getModelQuota(long userId, int modelId) {    int quota = memberCalculationService.getModelQuota(userId, modelId) + clientCalculationService.getModelQuota(userId, modelId);    if (quota > 0) {        GroupStatics gs = usageMapper.clientGroupStatics(userId, modelId);        if (gs != null) {            quota = quota - gs.getSum();        }    }    return quota;}
复制代码

记录用户使用

可以通过注解的方式来实现,同样对于数据库 quota 的查询可以重构到 Redis 当中,避免影响查询性能,示例代码:


@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface AccessLimit {    int model() default 1;
int idPosition() default 0;}
@Around("@annotation(accessLimit)")public Object checkLimit(ProceedingJoinPoint joinPoint, AccessLimit accessLimit) throws Throwable { Object[] args = joinPoint.getArgs(); long userId = Long.parseLong(args[accessLimit.idPosition()].toString()); int quota = compositiveCalService.getModelQuota(userId, accessLimit.model()); int model = accessLimit.model(); log.info("{} has {} of {} access", userId, quota, model); if (quota <= 0) { throw new AccessLimitException("exceed limit."); }
Usage usage = new Usage.Builder() .withUserId(userId) .withModelId(model) .build(); executorService.execute(() -> usageService.saveUsage(usage)); return joinPoint.proceed();}
复制代码

用户界面查询效果


发布于: 刚刚阅读数: 4
用户头像

Kevin_913

关注

纸上得来终觉浅,绝知此事要躬行。 2019-02-25 加入

专注于代码和设计15+年。 主要涉及Java,Golang,云平台。

评论

发布
暂无评论
如何设计一套基于API的会员系统_架构设计实战_Kevin_913_InfoQ写作社区