news 2026/9/5 16:43:14

AI SaaS按Token计费实战:基于Spring Boot的计量扣费系统设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI SaaS按Token计费实战:基于Spring Boot的计量扣费系统设计

最近在折腾 SaaS 项目的计费体系时,我越来越强烈地感觉到:传统“按月订阅”的模式,正在被 AI 时代一种更细颗粒度的玩法冲击——按 token 付费。

这个想法源于一个很现实的场景:市面上的 AI 写作、AI 客服、AI 绘画工具,动不动就是每月几十上百美元。很多产品看起来功能很全,但真正用到的核心接口其实就那么一两个。于是我想,能不能用 AI 辅助,把最常用、最核心的 SaaS 能力复刻成一个轻量级 MVP,然后把计费模式从“按月订阅”改成“按 token 消耗”?

整个项目做下来,踩了不少坑,也把 token 计量、配额扣减、账本记录这套链路完整跑通了。这篇文章把完整思路、代码和排错经验整理出来,适合正在做 AI 应用、SaaS 计费或 API 网关的同学参考,哪怕你是新手,也可以照着一步步搭建。

需要先声明的是:本文所说的“克隆”,是指参考产品公开的功能设计,用 AI 辅助从零实现一个学习型 MVP,用来验证技术和计费逻辑,绝不是指破解商业软件、搬运版权代码或绕过付费限制。做技术复刻,必须守住合规底线。

1. 从“按月付费”到“按 token 付费”,SaaS 计费到底在变什么

1.1 传统订阅制的痛点

传统 SaaS 通常采用订阅式计费,也就是用户每月支付固定费用,换取一定范围内的功能使用权。这种模式的优点是收入稳定、用户心理预期清晰;但缺点也很明显:

  • 低频用户觉得亏。一个月可能只调用几十次功能,却要承担完整月费。
  • 高频用户又觉得不够。某些功能用量超过套餐上限后,还需要额外购买更高档套餐。
  • 功能定价不精细。一个套餐内包含 10 个功能,用户只用其中 1 个,也要为另外 9 个买单。
  • 对开发者来说,订阅制缺少“按实际消耗计费”的弹性,特别是当核心成本来自大模型 API 调用时,用户用得越多,你的成本越高,但订阅收入却是固定的。

1.2 为什么 token 成了新的计价单位

在 AI 应用中,token 是模型处理文本的最小单位。简单理解,50 个英文字母大约对应 10 到 15 个 token,中文字符则通常一个字对应 1 到 2 个 token。用户每调用一次 AI 接口,模型都会读取和生成一定数量的 token,这直接决定了你的 API 调用成本。

按 token 付费的逻辑,就是让用户为“实际消耗的模型资源”买单。这种方式最早出现在大模型 API 调用中,例如常见的 OpenAI、Claude、百度文心、通义千问等,都是按 token 或按字符计费。现在,越来越多的上层 AI SaaS 也开始采用这种模式,因为:

  • 计费公平,用多少付多少。
  • 容易和上游成本对齐,避免亏本运营。
  • 用户门槛低,不用一次性付出高额月费。
  • 适合 AI Agent、自动写作、批量处理等用量差距极大的场景。

1.3 本文要做的“轻量级 AI SaaS 克隆”

我不想做一个口水文章,只讨论概念没有实操。所以这篇博客的目标很明确:用 Spring Boot + AI 辅助开发,复刻一个最小可用的 AI 文案生成 SaaS,核心包含用户注册、API Key 签发、AI 接口调用、token 计量、余额扣减和账本查询。

先思考清楚“克隆”的边界:

  • 功能边界:只做核心生成接口和计费体系,不做前端排版、团队协作、模板市场等外围功能。
  • 代码边界:不复制任何商业项目源码,所有代码由 AI 辅助从零生成,我再做逻辑校验。
  • 数据边界:演示数据全部使用模拟内容,AI 接口使用环境变量隔离,方便你替换成自己的模型服务。

这样既保证内容安全,也让你真正掌握一套可以落地到生产环境的架构方法。

2. 需求设计与技术选型:我们做一个什么样的 SaaS

2.1 以“AI 文案生成器”作为克隆对象

为了把问题讲清楚,我挑选一个非常有代表性的 SaaS 类型:AI 文案生成工具。这类工具通常包含:

  • 用户注册登录。
  • 选择文案类型,比如小红书种草文案、抖音标题、公众号开头。
  • 输入主题或关键词。
  • 调用大模型生成内容。
  • 展示结果并扣费。

这个场景非常适合讲 token 计费,因为每次生成内容的长度差异很大,如果按条数收费或按套餐收费,成本和收入很容易错配。

2.2 功能模块拆分

为了方便 AI 编程和后续维护,系统按模块拆分如下:

模块职责
用户模块注册、登录、查看余额
密钥模块签发和管理 API Key
生成模块根据参数调用大模型接口
计量模块计算请求的 token 用量并扣费
账本模块记录每笔收支明细
计费模块支持预扣、实扣、返还三种状态

2.3 计费流程设计

按 token 付费不能等到模型返回后再扣费,这样容易出现余额不足但请求已经发出的情况。更合理的流程是“预扣 - 实扣 - 返还”:

  1. 用户发起请求时,根据预估模型消耗先冻结一笔费用。
  2. 调用大模型接口。
  3. 拿到实际 token 用量后,计算真实费用。
  4. 把预扣金额调整为实际金额,多冻结的部分自动返还给用户。
  5. 记录一笔完整的账本流水,同时更新用户余额。

这套流程在真实支付系统中很常见,用在这里可以避免“先执行后扣费”带来的资损风险。

2.4 数据库表设计

本项目只需要三张核心表,业务语义足够清晰。

-- 用户表 CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(64) NOT NULL COMMENT '用户名', `password` varchar(128) NOT NULL COMMENT '加密后的密码', `balance_token` bigint NOT NULL DEFAULT '10000' COMMENT '账户剩余token数', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1正常 0禁用', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- API Key 表 CREATE TABLE `api_key` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `api_key` varchar(64) NOT NULL COMMENT '密钥', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1启用 0停用', `expire_time` datetime DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_api_key` (`api_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API Key表'; -- 用量账本表 CREATE TABLE `token_ledger` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `api_key` varchar(64) DEFAULT NULL, `biz_type` varchar(32) NOT NULL COMMENT 'RECHARGE充值 / CONSUME消费 / REFUND返还', `pre_token` bigint DEFAULT '0' COMMENT '预扣量', `actual_token` bigint DEFAULT '0' COMMENT '实际消耗量', `refund_token` bigint DEFAULT '0' COMMENT '返还量', `balance_after` bigint NOT NULL COMMENT '操作后余额', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='token账本表';

字段设计上有一个容易被忽略的点:balance_after一定要记录操作后的余额,而不是只记录变化量。后续对账时,只需要按照时间顺序回放账本,就能反推每个时点的余额,排查问题会容易很多。

3. 环境准备与项目结构

3.1 环境说明

本文示例以常见 Java 技术栈为例,版本信息如下:

  • JDK 17
  • Spring Boot 3.x
  • MySQL 8.x
  • Maven 3.8+
  • IDE:IntelliJ IDEA 或 VS Code

如果你的本地环境版本不同,不需要焦虑,重点理解配置和代码思路。Spring Boot 3.x 要求 JDK 17 及以上,如果你的环境是 JDK 8,建议先升级,或者把项目降级为 Spring Boot 2.7.x。

3.2 项目结构

为了便于 AI 辅助生成代码,我提前把项目结构定义得非常清晰:

ai-saas-token-demo ├── pom.xml └── src/main/java/com/example/aisaas ├── AisaasApplication.java ├── controller │ ├── AuthController.java │ └── GenerateController.java ├── service │ ├── UserService.java │ ├── ApiKeyService.java │ ├── AiProxyService.java │ └── TokenBillingService.java ├── filter │ └── ApiKeyAuthFilter.java ├── entity │ ├── User.java │ ├── ApiKey.java │ └── TokenLedger.java └── mapper ├── UserMapper.java ├── ApiKeyMapper.java └── TokenLedgerMapper.java

先定义好结构,再让 AI 按图索骥生成代码,生成的代码完成度和一致性会高很多。这也是使用 AI 辅助开发的一个重要技巧:不要让 AI 自由发挥整体架构,而是由人先搭好骨架,AI 负责填充肌肉和血管。

4. 用 AI 辅助开发:从生成代码到人工把关

4.1 如何向 AI 描述需求

很多同学用 AI 写代码时,描述过于笼统,比如“帮我写一个用户的 Controller”,这样生成的代码通常是华而不实的“玩具代码”。正确做法是给出明确的接口签名、入参出参、异常规则。

我使用的提示词示例:

请生成一个 Spring Boot 3 的 Controller 类,路径是 /api/generate,方法为 POST /api/generate/text。 入参:{"topic": "夏季护肤", "style": "小红书"} 出参:{"content": "生成结果", "promptTokens": 128, "completionTokens": 256, "totalTokens": 384} 要求: 1. 调用 TokenBillingService 先校验余额并预扣 token; 2. 调用 AiProxyService 获取生成结果; 3. 根据真实 token 用量更新账本; 4. 如果余额不足,返回 402 和错误码 TOKEN_NOT_ENOUGH。

这样生成的代码,无论是健壮性还是业务匹配度,都远比一句“写一个生成接口”要好。

4.2 人工必须检查的三个点

AI 生成代码速度很快,但绝不能直接合入项目。基于这次经验,我建议至少检查三个地方:

  • 密码是否加密存储。AI 生成的示例代码经常用明文密码,这在真实项目里是绝对不能接受的。
  • 事务边界是否完整。预扣、调用、实扣这三步必须在一个事务中控制,或者通过补偿机制保证最终一致。
  • 外部 API 地址和密钥是否从配置读取。AI 经常把sk-xxx写死在代码里,这种问题在生产环境是致命的。

下面我们逐块看核心代码实现。

5. 核心代码实现:AI 接口代理与 token 计量扣费

5.1 AI 接口代理层

为了便于本地测试,我没有直接请求外部大模型,而是做了一层可替换的代理。如果设置了ai.provider.base-urlai.provider.api-key,就走真实接口;否则走模拟返回,方便在没有网络模型的环境下跑通整个计费链路。

package com.example.aisaas.service; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class AiProxyService { @Value("${ai.provider.base-url:}") private String baseUrl; @Value("${ai.provider.api-key:}") private String apiKey; @Value("${ai.provider.model:demo-model}") private String model; private final RestTemplate restTemplate = new RestTemplate(); /** * 调用大模型生成文本。如果未配置 base-url,则返回模拟结果。 */ public AiResult generate(String prompt) { if (baseUrl == null || baseUrl.isBlank()) { return mockResult(prompt); } // 构造请求体,这里按 OpenAI 兼容协议举例 String url = baseUrl + "/chat/completions"; String body = """ { "model": "%s", "messages": [{"role": "user", "content": "%s"}] } """.formatted(model, prompt); // 实际项目中不要手工拼 JSON,建议使用 HashMap + ObjectMapper // 这里只演示流程 String response = restTemplate.postForObject(url, body, String.class); // 解析 response,提取 content 和 usage 字段 // 省略解析代码,按实际模型返回格式调整 return mockResult(prompt); } private AiResult mockResult(String prompt) { String content = "AI生成结果:" + prompt + ",这是一段模拟生成内容。"; int promptTokens = prompt.length(); int completionTokens = content.length(); return new AiResult(content, promptTokens, completionTokens, promptTokens + completionTokens); } public record AiResult(String content, int promptTokens, int completionTokens, int totalTokens) { } }

这里有一个重要提醒:真实项目调用大模型时,不要手工拼 JSON 字符串。因为提示词内容里可能包含引号、换行、特殊字符,手工拼串轻则 JSON 解析失败,重则引发注入问题。正确做法是使用ObjectMapper构造Map,或者使用模型官方 SDK。上面的代码只是演示流程,实际接入时请用官方 SDK 代替RestTemplate

5.2 token 计量与计费服务

这一步是整个系统的核心。我在代码中实现了“预扣 - 实扣 - 返还”三个动作,并且把每一步都记录到token_ledger账本中。

package com.example.aisaas.service; import com.example.aisaas.entity.TokenLedger; import com.example.aisaas.entity.User; import com.example.aisaas.mapper.TokenLedgerMapper; import com.example.aisaas.mapper.UserMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; @Service public class TokenBillingService { private final UserMapper userMapper; private final TokenLedgerMapper ledgerMapper; public TokenBillingService(UserMapper userMapper, TokenLedgerMapper ledgerMapper) { this.userMapper = userMapper; this.ledgerMapper = ledgerMapper; } /** * 预扣 token,返回预扣后的用户对象。 */ @Transactional public User preDeduct(Long userId, int preToken) { User user = userMapper.selectById(userId); if (user == null || user.getStatus() != 1) { throw new RuntimeException("用户不存在或已被禁用"); } if (user.getBalanceToken() < preToken) { throw new RuntimeException("余额不足,当前余额=" + user.getBalanceToken() + ",需要=" + preToken); } user.setBalanceToken(user.getBalanceToken() - preToken); userMapper.updateById(user); TokenLedger ledger = new TokenLedger(); ledger.setUserId(userId); ledger.setBizType("CONSUME"); ledger.setPreToken(preToken); ledger.setBalanceAfter(user.getBalanceToken()); ledger.setRemark("预扣"); ledger.setCreatedAt(LocalDateTime.now()); ledgerMapper.insert(ledger); return user; } /** * 实扣:把预扣数调整为实际消耗数,并返还差额。 */ @Transactional public void settle(Long userId, int preToken, int actualToken) { if (actualToken > preToken) { // 实际消耗大于预扣,说明预扣少了,需要补扣 User user = userMapper.selectById(userId); int extra = actualToken - preToken; if (user.getBalanceToken() < extra) { throw new RuntimeException("实际扣费后余额不足,需要补扣=" + extra); } user.setBalanceToken(user.getBalanceToken() - extra); userMapper.updateById(user); TokenLedger ledger = new TokenLedger(); ledger.setUserId(userId); ledger.setBizType("CONSUME"); ledger.setPreToken(preToken); ledger.setActualToken(actualToken); ledger.setBalanceAfter(user.getBalanceToken()); ledger.setRemark("实扣补差额"); ledgerMapper.insert(ledger); } else if (actualToken < preToken) { // 实际消耗小于预扣,返还差额 User user = userMapper.selectById(userId); int refund = preToken - actualToken; user.setBalanceToken(user.getBalanceToken() + refund); userMapper.updateById(user); TokenLedger ledger = new TokenLedger(); ledger.setUserId(userId); ledger.setBizType("REFUND"); ledger.setPreToken(preToken); ledger.setActualToken(actualToken); ledger.setRefundToken(refund); ledger.setBalanceAfter(user.getBalanceToken()); ledger.setRemark("返还预扣差额"); ledgerMapper.insert(ledger); } } }

这里有个容易被忽略的并发问题:如果用selectById查出用户余额,再在内存里做减法,最后updateById整体更新,在高并发下会出现超扣。更好的做法是在updateSQL 里使用原子更新:

UPDATE `user` SET balance_token = balance_token - #{preToken} WHERE id = #{userId} AND balance_token >= #{preToken}

通过数据库层面的条件更新来防止并发超扣。上面 Java 代码为了展示完整流程,简化了这个细节,生产环境一定要补上这个限制。

5.3 生成接口 Controller

Controller 层负责把用户请求、AI 调用、计费结算串起来。

package com.example.aisaas.controller; import com.example.aisaas.entity.User; import com.example.aisaas.service.AiProxyService; import com.example.aisaas.service.TokenBillingService; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/generate") public class GenerateController { private final TokenBillingService billingService; private final AiProxyService aiProxyService; public GenerateController(TokenBillingService billingService, AiProxyService aiProxyService) { this.billingService = billingService; this.aiProxyService = aiProxyService; } @PostMapping("/text") public Map<String, Object> generateText(@RequestAttribute("userId") Long userId, @RequestBody Map<String, String> request) { String topic = request.get("topic"); String style = request.get("style"); // 通过请求头 x-api-key 找到 userId // 实际逻辑在 ApiKeyAuthFilter 中完成 String prompt = "请以" + style + "风格,围绕“" + topic + "”生成一段文案。"; // 1. 估算 token,保守预扣 int preToken = prompt.length() * 2 + 200; billingService.preDeduct(userId, preToken); try { // 2. 调用 AI 接口 AiProxyService.AiResult result = aiProxyService.generate(prompt); // 3. 实扣结算 billingService.settle(userId, preToken, result.totalTokens()); return Map.of( "content", result.content(), "promptTokens", result.promptTokens(), "completionTokens", result.completionTokens(), "totalTokens", result.totalTokens() ); } catch (Exception e) { // 调用失败,返还全部预扣 billingService.settle(userId, preToken, 0); throw new RuntimeException("AI 生成失败", e); } } }

这里有一个值得注意的失败场景:如果 AI 接口超时或者返回异常,已经预扣的 token 必须返还。所以我用settle(userId, preToken, 0)把 actualToken 传为 0,这样在结算逻辑里会走“实际消耗小于预扣”的分支,把预扣全额退回。

5.4 API Key 鉴权过滤器

既然是 SaaS,用户不应该是通过浏览器表单每次登录,而是使用 API Key 调用接口。这里我用一个过滤器统一处理:

package com.example.aisaas.filter; import com.example.aisaas.mapper.ApiKeyMapper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Component public class ApiKeyAuthFilter extends OncePerRequestFilter { private final ApiKeyMapper apiKeyMapper; public ApiKeyAuthFilter(ApiKeyMapper apiKeyMapper) { this.apiKeyMapper = apiKeyMapper; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String path = request.getRequestURI(); if (path.startsWith("/api/auth")) { filterChain.doFilter(request, response); return; } String apiKey = request.getHeader("x-api-key"); if (apiKey == null || apiKey.isBlank()) { response.setStatus(401); response.getWriter().write("missing api key"); return; } var keyEntity = apiKeyMapper.selectByApiKey(apiKey); if (keyEntity == null || keyEntity.getStatus() != 1) { response.setStatus(401); response.getWriter().write("invalid api key"); return; } request.setAttribute("userId", keyEntity.getUserId()); filterChain.doFilter(request, response); } }

API Key 的生成使用UUID.randomUUID().toString().replace("-", "")即可,不需要使用用户密码拼接。API Key 一旦泄露,不需要改密码,只需要在数据库里把对应的 key 状态置为 0,就能做到秒级失效。

6. 计费配置与部署运行

6.1 application.yml 配置

项目的核心配置如下:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/ai_saas?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true ai: provider: base-url: "" api-key: "" model: "deepseek-chat"

注意base-urlapi-key默认留空,此时系统会走模拟生成,方便本地跑通计费链路。当你准备好真实模型服务时,再在环境变量或配置中心注入密钥即可。

6.2 快速运行与验证

首先初始化数据库,执行第 2 节的建表 SQL。然后启动应用,执行以下命令验证:

  1. 注册用户。
curl -X POST http://localhost:8080/api/auth/register \ -H "Content-Type: application/json" \ -d '{"username": "zhangsan", "password": "123456"}'
  1. 登录并获取 API Key。
curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username": "zhangsan", "password": "123456"}'

登录接口返回结果中会包含apiKey

{ "apiKey": "a1b2c3d4e5f6...", "balanceToken": 10000 }
  1. 调用生成接口。
curl -X POST http://localhost:8080/api/generate/text \ -H "Content-Type: application/json" \ -H "x-api-key: a1b2c3d4e5f6..." \ -d '{"topic": "夏季护肤", "style": "小红书"}'

预期返回:

{ "content": "AI生成结果:请以小红书风格,围绕“夏季护肤”生成一段文案。这是一段模拟生成内容。", "promptTokens": 23, "completionTokens": 46, "totalTokens": 69 }
  1. 查看账本。
SELECT * FROM token_ledger WHERE user_id = 1 ORDER BY id;

可以看到两条记录:一条预扣记录,一条返还记录。通过账本可以完整回溯这次调用的 token 变化。

7. 常见问题与排查思路

我在开发调试过程中遇到不少问题,这里整理成一张表,并挑几个典型问题详细展开。

问题现象常见原因解决思路
接口返回 401 Invalid API KeyAPI Key 拼写错误或已被停用检查请求头参数名,数据库确认 key 状态
调用生成接口余额不变模拟模式下 token 数量太小,或返还没被看到查看账本表,确认预扣和返还两条流水
并发下余额被扣成负数没有使用原子更新,出现超扣改用UPDATE ... WHERE balance_token >= #{preToken}
AI 接口 403模型服务方鉴权失败或区域受限核对密钥、账号区域是否受服务商支持,查看服务商官方文档
实际 token 和预估差异大模型版本更新或提示词变化预扣时多预留 20% 到 50% 的 buffer
用户投诉扣费偏高返回内容包含隐藏 token记录 promptTokens、completionTokens 拆分明细,便于解释

7.1 关于 token exchange failed 报错

最近不少同学在使用第三方 AI 服务或 OAuth 登录时,遇到类似token exchange failed: token endpoint returned status 403的报错。这里要注意,这个报错里的 token 指的不是我们计费里的 token 消耗,而是 OAuth 授权流程中的“访问令牌交换”。二者概念完全不同。

常见的403原因包括:

  • 客户端 ID 或密钥不匹配。
  • 回调地址与配置不一致。
  • 账号所在区域不在服务商支持范围内。
  • 授权码已经过期或重复使用。

排查顺序建议是:先看服务端日志,确认是哪一步返回 403;再核对密钥和回调地址;最后检查账号区域与服务商支持列表。对于区域限制,正确做法是遵循服务商的服务条款,选择官方支持的合法使用方式,而不是尝试绕过限制。

7.2 计费不准确的排查思路

如果发现用户消耗的 token 和自己后台统计的不一致,可以从以下三个方向排查:

  • 是否把请求和响应两部分的 token 都统计进去了。很多模型接口会返回prompt_tokenscompletion_tokens,需要加起来。
  • 是否把重试请求重复计费了。如果 AI 接口超时后框架自动重试,每次重试都可能产生费用。
  • 是否把流式输出遗漏了。使用 SSE 流式接口时,如果只在接收完才统计一次,某些模型需要自行累加每个分片的 token。

8. 最佳实践与工程建议

8.1 合规边界不能碰

再次强调:做 SaaS 克隆,只允许参考功能设计、数据结构、交互流程这些产品层面的公开信息,不允许复制源码、破解验证、绕过付费。“克隆”的目的是学习和验证,而不是做一个 1:1 的商业替代品。你在自己的项目中使用大模型生成代码时,也要注意输入给模型的提示词不能包含他人版权代码。

8.2 预扣比例要动态调整

预扣 token 不能拍脑袋写死。如果有历史调用数据,可以根据提示词长度、模型型号、历史平均消耗来设置预扣比例。我建议:

  • 第一次调用没有历史数据时,预扣 = 预估 token × 2。
  • 积累 100 条调用记录后,根据 P95 消耗值动态调整。
  • 每次调用前先按照最新预扣策略试算,避免对低频用户造成资金占用。

8.3 对账机制不能省

无论 SaaS 做得再简单,对账机制一定要有。我在生产环境里的习惯是:

  • 每个用户每天生成一张 token 流水汇总。
  • 每日凌晨和上游模型账单对账。
  • 如果发现账本余额和真实消耗偏差超过阈值,立即冻结计费任务并告警。

具体到代码层面,可以在token_ledger表增加一个biz_id字段,用于关联每次请求的唯一 ID,便于对账时定位每笔流水的产生原因。

8.4 防止刷单和滥用

按 token 计费的最大风险不是用户用得多,而是恶意用户绕过认证刷接口。建议从几个维度加防护:

  • 每个 API Key 增加速率限制,例如每秒不超过 2 次请求。
  • 每个用户设置单日最大消耗额度。
  • 对连续失败请求做熔断,避免 AI 接口异常时不断扣费。
  • 生产环境必须接入日志链路追踪,每次请求带上 requestId。

8.5 生产环境使用配置中心管理模型密钥

不建议把模型密钥直接写在application.yml里,更不要提交到 Git。生产环境应该通过环境变量或配置中心注入,并且定期轮换密钥。如果你在团队里开发,还要遵循最小权限原则:只有负责模型网关的同事有密钥管理权限,其他开发人员使用代理服务,接触不到真实密钥。

8.6 从 MVP 到生产还需要补齐什么

这个 Demo 的核心链路已经完整,但如果要真正上线,还需要补齐以下能力:

  • 用户注册的验证码、密码找回。
  • 支付宝/微信支付充值,或者对接已有的支付 SaaS。
  • 用量看板和告警系统。
  • 模型多租户隔离。
  • 账单导出功能。
  • 多模型路由,按价格和效果自动选择模型。

其中支付对接这块,小程序和 Web 端的坑不太一样,特别是回调验签、退款原路退回、幂等处理这三块,建议单独做一期总结。这里不展开,但你要知道,计费系统上线前,支付回调的幂等是必须测试的。

9. 写在最后的实在建议

这个项目给我最大的收获,并不是“用 AI 写代码有多快”,而是“AI 辅助开发 + 清晰架构 + 计量思维”的组合,确实能大幅降低从想法到 MVP 的成本。

如果你也准备做一个 AI 类型的 SaaS,我的建议很简单:先别急着画大而全的架构图,找一个最常用的功能,比如文案生成、翻译、改写、摘要,把它和 token 计量、用户余额、账本流水这条链路跑通。跑通之后,你对“按 token 付费”的理解会比读任何文章都深刻。

收费模式这件事,本质上是把成本和价值对齐。订阅制适合标准化服务,token 计费适合成本波动大的 AI 场景。如果你的产品同时包含批量处理和低频使用,也可以考虑混合计费:基础功能走订阅,深度 AI 能力按 token 消耗。

最后留一个作业给你:把上面的 Demo 跑起来之后,试着给token_ledger增加一个每日汇总统计接口,统计每个用户当天消耗的 token 总量和预估费用。这个功能虽然不复杂,但做完之后,你就掌握了 SaaS 计费系统最核心的那根链条。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 4:24:28

DeepSeek API涨价后,本地部署MiniMax与MiMo模型替代方案实战评测

如果你正在使用 DeepSeek 的 API 服务&#xff0c;最近可能已经收到了价格调整的通知。对于依赖其进行代码生成、文本处理或日常开发的个人开发者和团队来说&#xff0c;成本突然增加是一个需要立刻应对的现实问题。与其被动接受&#xff0c;不如主动寻找替代方案。这次我们直接…

作者头像 李华
网站建设 2026/9/4 8:38:10

AI智能体的判断力:Agent从能执行到会判断的关键

最近看到几篇围绕AI智能体&#xff08;Agent&#xff09;的科研论文投稿反馈&#xff0c;题目各不相同&#xff0c;被拒理由却指向同一个词&#xff1a;判断力。论文里的Agent能规划任务、能调用工具、能跑完整流程&#xff0c;模型版本也用到了当时的主流方案&#xff0c;实验…

作者头像 李华
网站建设 2026/9/4 14:50:07

跨语言链路追踪:从单语言困境到统一可观测性架构

1. 这篇文章真正要解决的问题在微服务和分布式架构成为默认选项之后&#xff0c;很多团队会遇到一个非常尴尬的瞬间&#xff1a;一次用户请求从前端进来&#xff0c;先后经过了 API 网关、用户服务、订单服务、支付服务、消息队列、定时任务&#xff0c;最后还落到了一个 Pytho…

作者头像 李华
网站建设 2026/9/4 0:59:51

基于Qt/C++的网盘系统开发:从网络通信到多线程的实战解析

简介&#xff1a;这是一份面向C中级开发者与Qt学习者的综合性网盘项目实战资源&#xff0c;聚焦网络编程、多线程协同与GUI应用开发&#xff0c;解决桌面端云存储社交化文件协作的典型工程问题。压缩包共103个文件&#xff0c;含15个核心cpp源码&#xff08;如mytcpsocket.cpp、…

作者头像 李华
网站建设 2026/9/4 12:50:02

Python机器人迷宫探索:从DFS算法到硬件闭环控制的完整实现

简介&#xff1a;本资源是一份面向高校人工智能与机器人课程设计的Python实践项目&#xff0c;聚焦迷宫路径规划核心问题&#xff0c;完整实现基于基础搜索算法&#xff08;如DFS/BFS&#xff09;与深度强化学习&#xff08;Deep Q-Network&#xff09;的双方案机器人自动寻路系…

作者头像 李华