尧图精选

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

🕒 发布时间:2026/9/2 6:12:41 📁 来源:尧图网络
最近在折腾 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. 需求设计与技术选型我们做一个什么样的 SaaS2.1 以“AI 文案生成器”作为克隆对象为了把问题讲清楚我挑选一个非常有代表性的 SaaS 类型AI 文案生成工具。这类工具通常包含用户注册登录。选择文案类型比如小红书种草文案、抖音标题、公众号开头。输入主题或关键词。调用大模型生成内容。展示结果并扣费。这个场景非常适合讲 token 计费因为每次生成内容的长度差异很大如果按条数收费或按套餐收费成本和收入很容易错配。2.2 功能模块拆分为了方便 AI 编程和后续维护系统按模块拆分如下模块职责用户模块注册、登录、查看余额密钥模块签发和管理 API Key生成模块根据参数调用大模型接口计量模块计算请求的 token 用量并扣费账本模块记录每笔收支明细计费模块支持预扣、实扣、返还三种状态2.3 计费流程设计按 token 付费不能等到模型返回后再扣费这样容易出现余额不足但请求已经发出的情况。更合理的流程是“预扣 - 实扣 - 返还”用户发起请求时根据预估模型消耗先冻结一笔费用。调用大模型接口。拿到实际 token 用量后计算真实费用。把预扣金额调整为实际金额多冻结的部分自动返还给用户。记录一笔完整的账本流水同时更新用户余额。这套流程在真实支付系统中很常见用在这里可以避免“先执行后扣费”带来的资损风险。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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTAPI 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTtoken账本表;字段设计上有一个容易被忽略的点balance_after一定要记录操作后的余额而不是只记录变化量。后续对账时只需要按照时间顺序回放账本就能反推每个时点的余额排查问题会容易很多。3. 环境准备与项目结构3.1 环境说明本文示例以常见 Java 技术栈为例版本信息如下JDK 17Spring Boot 3.xMySQL 8.xMaven 3.8IDEIntelliJ 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-url和ai.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 生成接口 ControllerController 层负责把用户请求、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 MapString, Object generateText(RequestAttribute(userId) Long userId, RequestBody MapString, 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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-url和api-key默认留空此时系统会走模拟生成方便本地跑通计费链路。当你准备好真实模型服务时再在环境变量或配置中心注入密钥即可。6.2 快速运行与验证首先初始化数据库执行第 2 节的建表 SQL。然后启动应用执行以下命令验证注册用户。curl -X POST http://localhost:8080/api/auth/register \ -H Content-Type: application/json \ -d {username: zhangsan, password: 123456}登录并获取 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 }调用生成接口。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 }查看账本。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_tokens和completion_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 计费系统最核心的那根链条。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →