返回作品

AI 图文聊天扣费系统

创建于 2025 年 3 月 17 日

标签
  • Java 8
  • Spring Boot 2.5.15
  • RuoYi 3.8.9
  • Spring Security
  • JWT
  • easy-query 2.7.32
  • MyBatis-Plus
  • MySQL 8
  • Redis
  • Druid
  • fastjson2
  • Hutool
  • Vue 2
  • Element UI

项目介绍

这是一个基于若依 RuoYi 3.8.9 前后端分离脚手架二次开发的 AI 图文聊天扣费系统(目录 20250317-AI图文聊天扣费,含 -api 后端、-web 前端与全量 SQL)。业务形态很典型:给用户提供”按次计费”的 AI 对话能力——用户消耗套餐次数来调用视觉大模型,既可以输入文字提问,也可以上传图片让模型看图回答,次数用完后通过卡密充值或扫码购买套餐续费。

项目由三部分构成:后端 20250317-AI图文聊天扣费-api(ruoyi-admin 模块下新增 com.ruoyi.app 业务包)、前端 20250317-AI图文聊天扣费-web(若依 Vue 管理端 + 独立的深色聊天页),以及根目录 20250317.sql 全量数据库脚本。业务表一共六张:app_bigmodel(大模型调用记录)、app_bigmodel_config(模型配置)、app_card(卡号)、app_order(订单)、app_package(套餐)、app_user_package(用户套餐)。

从 SQL 里的数据能还原真实的开发过程:app_bigmodel_config 中依次配置了阿里 qwen-vl-max / qwen-vl-plus、Moonshot moonshot-v1-8k-vision-preview、智谱 GLM-4V / GLM-4V-Plus-0111 / GLM-4V-Flash 六个模型;app_bigmodel 调用记录里躺着 2025-03-19 到 03-28 的调试痕迹——先是 404、图片 URL 校验失败,到 03-20 成功返回”这是一张图标图片……”的看图回答,再到 03-27 换成 Moonshot 时踩到 url.not_foundInvalid Authentication 的坑,最后调通。订单表则记录了 03-27 到 03-28 从 0.01 元试单到支付成功、trade_no 回填的完整支付链路。

项目架构

系统沿用若依前后端分离结构,业务代码全部收敛在后端 ruoyi-admin/src/main/java/com/ruoyi/app 与前端 src/views/appsrc/views/chat 中:

模块技术栈说明
ruoyi-admincom.ruoyi.appJava 8、Spring Boot 2.5.15、Spring Security + JWT业务核心:controller/app/ApiController 是用户端唯一入口(额度查询、套餐列表、模型列表、卡号充值、调用扣费),controller/app/PayController 处理下单、订单查询与支付回调验签;controller/admin 下按套餐、卡号、模型配置、用户套餐、调用记录、订单拆成六个管理控制器;BigScreenController 提供数据大屏统计
ruoyi-commoneasy-query 2.7.32、MyBatis-Plus 3.5.4、fastjson2 2.0.53、Hutool 5.8.29、RedisORM 走 easy-query 的 EasyEntityQuery 链式查询,AppPackage / AppUserPackage 实体放 com.ruoyi.common.core.domain.entity 供跨模块复用
ruoyi-admin(工具类)Hutool HttpUtil、fastjson2PayUtil.pay 封装第三方易支付下单,OrderNoGenerator 生成 ORD + 时间戳 + 随机数 交易单号,SmsUtil 封装短信验证码登录
-web 前端Vue 2.6、Element UI 2.15.14、axios、Vuex、SCSSsrc/views/chat/index.vue 独立聊天页(路由 /chat,退出跳 /front-login),src/views/app 下六个若依风格 CRUD 管理页,src/api/app/api.js 集中封装用户端接口
20250317.sqlMySQL 8.0.35全量库脚本,六张业务表 + 若依框架表 + 真实测试数据,菜单 2000-2006 挂”业务管理”目录

模型接入统一走 OpenAI 兼容的 POST {base_url}/chat/completions 协议:ApiController.call 根据 configId 查出模型配置(模型名、请求路径、apikey、启停状态),用 Hutool 的 HttpUtil.createPost 拼接 Authorization: Bearer {apikey} 请求头调用,请求体按厂商微调——普通模型把图片放在 content 数组的 image_url 节点,智谱 glm-4v 系列则包一层 {"type":"image_url","image_url":{"url":...}},由 getMap / getMapGlm 两个方法区分组装。因为协议统一,新增一家模型厂商只需在”大模型配置”页面填模型名、请求路径和 apikey,一行 Java 代码都不用改。

核心功能

多模型图文聊天页。 src/views/chat/index.vue 是一个完全独立于若依布局的深色科技风页面:左侧栏展示”AI 助手”Logo、模型下拉框与用户信息(“余额: X 次 / 到期: Y”),主区域是消息流,底部支持 Ctrl+Enter 发送、图片上传(FileReader 转 Base64 随消息展示)与”清空”按钮。发送时前端组装 {text, img, configId}sendQuestion(POST /api/call),成功后从返回 JSON 中解析 choices[0].message.content 渲染 AI 回复,并调用 getInfo 刷新剩余次数。页面还内置了粒子动画、电路装饰线与”AI 正在思考中”加载动效,配合”购买套餐 / 充值 / 退出”三个渐变按钮,交互完成度很高。

双保险扣费与失败回滚。 /api/call 的扣费链路是项目核心,代码写得非常谨慎:

// 1. Redis 分布式锁:防止连点/并发请求重复扣费
String lockKey = "user_package_lock:" + userId;
locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (!locked) return AjaxResult.error("系统繁忙,请稍后重试");

// 2. 筛选可用套餐:排除过期、排除次数已用完的,按到期时间升序优先扣最早到期的
for (AppUserPackage userPackage : appUserPackage) {
    if (expiresTime 已过期) continue;
    if (nowCount >= maxCount) continue;
    subList.add(userPackage);
}
subList = subList.stream().sorted(Comparator.comparing(AppUserPackage::getExpiresTime)).collect(...);

// 3. 预扣一次:乐观锁 version 保证更新串行,rows == 0 说明被并发修改
subList.get(0).setNowCount(subList.get(0).getNowCount() + 1);
long rows = easyEntityQuery.updatable(subList.get(0)).executeRows();
if (rows == 0) return AjaxResult.error("次数不足或数据已被修改,请刷新后重试");

扣费成功后真正调用大模型,再校验返回体:body.contains("\"total_tokens\"") 才视为调用成功;请求抛异常或返回失败时,把 nowCount 减一”回滚”预扣的次数。每次调用无论成败都写入 app_bigmodel 调用日志(用户、时间、模型名、返回数据、错误信息),后台”模型调用”页可按用户、时间、模型、状态检索——调用了多少次、花在哪个模型上,全部可审计。

额度聚合与套餐规则。 GET /api/getInfo 聚合用户名下所有有效套餐计算可用次数:只要有一个套餐 count_limit=0(次数不限制)就显示”无限”,time_limit=0 则到期时间显示”无限”;否则累加所有未过期套餐的 maxCount - nowCount,并取最晚到期时间展示。套餐表 app_package 用两个开关组合出四种额度策略:duration_limit(时长是否受限)+ count_limit(次数是否受限),配合 duration(天数)、max_count(最大次数)、amount(价格)、reg_bestow(注册是否赠送)字段,SQL 里默认的”免费套餐”就是”30 天总共 10 次、注册赠送”。

卡密充值。 管理员在”卡号管理”页批量生成 app_card(卡号 + 绑定套餐 + 状态),用户在前端”充值”弹框输入卡号调 GET /api/recharge:后端校验卡号存在、未使用后,把卡置为已使用并绑定当前用户,同时按套餐规则生成一条 app_user_packageexpires_time = 当前时间 + duration 天max_count 按购买时套餐计算),完成兑换。

扫码支付闭环。 前端”购买套餐”弹框选择套餐与支付方式(微信/支付宝)后调 POST /pay/create:后端生成 ORD202503271408457512 格式的交易单号、落一条 app_order(状态 0 等待支付),再调 PayUtil.pay 拿第三方易支付的收款二维码返回前端展示;前端启动 3 秒轮询 GET /pay/queryByOutTradeNo,订单状态变为 1 即提示支付成功并刷新余额。支付回调 /pay/callback 是匿名接口,先校验 TRADE_SUCCESS,再把 pid/name/money/out_trade_no/trade_no/type/trade_status 按 key 升序拼接、末尾追加商户密钥、整体 MD5 后与 sign 比对,验签通过才更新订单(回填 pay_timetrade_no)并插入对应的 app_user_package 发放套餐——签名校验 + 幂等判断(只处理状态为 0 的订单)做得一丝不苟。

数据大屏。 BigScreenController.getCount 一次性统计新用户、总用户、在线用户(Redis token 数)、付费用户数(去重)、模型调用次数、注册赠送消耗金额与最近 7 天用户增长曲线,为运营提供看板数据。

功能截图

暂无运行截图。

快速上手

  1. 初始化数据库:执行根目录 20250317.sql(全量脚本,含框架表、六张业务表与测试数据),修改 ruoyi-admin/src/main/resources/application-druid.yml 中的 MySQL 连接与 application.yml 中的 Redis 配置。

  2. 启动后端与前端:

cd "20250317-AI图文聊天扣费-api"
mvn clean package -DskipTests
java -jar ruoyi-admin/target/ruoyi-admin.jar   # 默认端口 8080

cd "../20250317-AI图文聊天扣费-web"
npm install
npm run dev   # http://localhost:80,默认账号 admin / admin123
  1. 体验完整流程:先在”大模型配置”确认要用的模型已开启(默认 qwen-vl-maxmoonshot-v1-8k-vision-previewGLM-4V 等均为开启状态,若 apikey 过期需替换)→ 访问 /chat 聊天页,选模型、输入问题或上传图片发送,观察侧栏”余额”次数递减 → 用”卡号管理”里未使用的卡号(如 0000004)在聊天页”充值”弹框兑换套餐 → 也可在”购买套餐”选套餐调出支付二维码,配合支付回调完成购买闭环 → 在”模型调用”页查看每次调用的返回数据与错误信息,在”订单管理”页查看订单与 trade_no

总结

这个项目把”AI 能力按次计费”的产品闭环做得很完整,值得借鉴的有四点:一是扣费一致性设计——Redis setIfAbsent 分布式锁挡住并发请求,乐观锁 version 挡住并发更新,调用失败自动回滚次数,三层保护下”扣了费但没调成、调了两次却扣一次”这类资损场景都被兜住,total_tokens 校验还防止了把厂商错误响应当成成功;二是协议统一带来的模型可插拔——所有厂商都适配 OpenAI 兼容的 chat/completions 协议,模型切换只动数据库配置不动代码,getMap / getMapGlm 的差异封装也足够克制;三是额度模型简洁可扩展——时长/次数两个开关组合出四种限制策略,一张 app_user_package 表同时承载卡密兑换与支付发放两种来源;四是支付闭环的严谨度——回调验签、幂等判断、单号轮询查询、二维码即时展示,小额试单数据(0.01 元)也完整走通了全链路。当然也能看到二次开发的真实痕迹:SQL 里还留着测试时的真实 apikey 与 qwen-vl-max 404 调试记录,AppBigmodel 等实体保留了代码生成器风格,getInfo 对空套餐的边界处理略粗糙——但这正是它作为学习项目最真实的一面:从若依脚手架出发,一步步长出”聊天 → 扣费 → 充值 → 支付 → 统计”完整商业闭环的过程,对想做 AI 应用变现或积分计费系统的开发者很有参考价值。