Compare commits

...

64 Commits

Author SHA1 Message Date
Gsh
2e4f520dac fix: 更改版本号 2025-11-02 01:27:03 +08:00
ccnetcore
067b25b9af Merge remote-tracking branch 'origin/invitation' into invitation 2025-11-02 01:21:33 +08:00
ccnetcore
36370c215d fix: 修复周邀请次数统计时使用错误的用户ID字段 2025-11-02 01:21:28 +08:00
Gsh
e24731acfe fix: 分享词修改 2025-11-02 01:17:22 +08:00
Gsh
927e9df7de fix: 分享地址固定为海外地址 2025-11-02 00:55:47 +08:00
Gsh
114b41144e fix: 增加邀请链接逻辑 2025-11-02 00:51:14 +08:00
ccnetcore
5019a36138 fix: 优化邀请码不足提示文案 2025-11-02 00:32:04 +08:00
Gsh
e15eb6149b fix: 翻牌样式优化,动画效果完善 2025-11-01 18:48:17 +08:00
Gsh
9d401a9c93 fix: 翻牌样式优化,动画效果完善 2025-11-01 17:56:19 +08:00
Gsh
eacf86e118 fix: 翻牌样式优化,动画效果待完善 2025-10-31 00:41:21 +08:00
chenchun
c4b631c815 feat: 新增翻牌幸运值悬浮球及相关逻辑
- .claude/settings.local.json:新增 Read 权限路径(Read(//e/code/github/Yi/Yi.Ai.Vue3/**))
- Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue:
  - 新增 luckyValue 响应式状态与 updateLuckyValue() 方法,并在获取任务状态后更新幸运值
  - 新增悬浮球 UI(SVG 进度环、图标、百分比文本)及样式和动画
  - 调整了 v-loading 为 false,并注释了部分错误提示(可能为调试遗留)
- 说明:样式使用嵌套写法(scss/sass 风格),请确认构建流程支持;建议确认 v-loading 与错误提示变更是否为预期并视情况修正。
2025-10-30 21:16:19 +08:00
chenchun
fb25e75a3a feat: 完成邀请机制 2025-10-30 20:17:14 +08:00
chenchun
e9099bbe04 feat: 增加基于本周填写邀请码数量的邀请翻牌校验
- 注入 ISqlSugarRepository<InvitationRecordAggregateRoot> 到 CardFlipManager 并更新构造函数。
- 在邀请类型(FlipType.Invite)翻牌时,改为校验用户本周已填写的邀请码数量是否满足本次翻牌所需(根据 InviteFlipsUsed 计算所需数量),不足则抛出友好异常提示。
- 保持原有错误处理与日志逻辑不变。
2025-10-30 20:13:49 +08:00
chenchun
f02fb91175 feat: 增加邀请码每周使用上限并调整翻牌规则(扩展免费次数、移除赠送翻牌与翻倍提示) 2025-10-30 19:51:56 +08:00
chenchun
efd917d184 style: 全部样式更新2.0 2025-10-30 11:21:11 +08:00
chenchun
e906208f4a feat: 新增邀请翻牌验证及相关文案与界面调整
- CardFlipManager:注入 InviteCodeManager,新增对 Invite 类型翻牌的邀请校验(未使用邀请码则抛出异常),防止未被邀请的用户使用邀请类型翻牌。
- CardFlipService:调整提示文案,统一使用“本周”前缀,并在邀请解锁提示中强调必定中奖且每次中奖最大额度翻倍。
- 前端:
  - CardFlipActivity.vue:注释掉翻牌失败的全局提示,调整统计文案为“本周已翻/本周剩余/本周邀请”,并在邀请弹窗文案中说明必定中奖且奖励翻倍。
  - Avatar.vue:更新菜单项标签为“每日任务(限时)”和“每周邀请(限时)”。
2025-10-30 11:19:22 +08:00
chenchun
e6b991fe86 feat: 调整翻牌与邀请码逻辑,增加第8次奖励及前端骨架屏 2025-10-29 21:55:17 +08:00
Gsh
dd3f6325bb fix: 个人中心优化 2025-10-29 00:17:36 +08:00
chenchun
c6425ca206 fix: 优化对话异常提示信息
将抛出异常的消息从 "OpenAI对话异常{StatusCode}" 修改为更详细的中文提示,包含 StatusCode 与 Response 内容,便于排查。未改变逻辑,仅调整异常文本。
2025-10-28 16:02:01 +08:00
chenchun
acb359ec33 style: 删除多余的 SqlSugar InitTables 注释并调整注释格式
在 Yi.Abp.Web/YiAbpWebModule.cs 中移除两行多余的注释,调整剩余注释的空格格式,清理代码注释,不影响程序逻辑。
2025-10-27 22:01:57 +08:00
chenchun
a1395d9a33 feat: 新增翻牌顺序追踪并重构翻牌/邀请码逻辑到 Manager,更新前端
- 在 CardFlipStatusOutput 与前端 types 添加 FlipOrderIndex 字段以记录牌在翻牌顺序中的位置
- 在域实体 CardFlipTaskAggregateRoot 增加 FlippedOrder(Json 列)以保存用户实际翻牌顺序
- 将 CardFlipService 重构为调用 CardFlipManager 与 InviteCodeManager,移除大量内聚的业务实现与常量(职责下沉到 Manager)
- 调整翻牌、使用邀请码和查询相关流程为 Manager 驱动,更新返回结构与提示文本
- 更新前端 CardFlipActivity 组件与 types,允许任意未翻的卡片被点击并显示翻牌顺序位置
- 若干文案、格式与日志细节修正
2025-10-27 21:57:26 +08:00
chenchun
aec90ec9d6 feat: 新增翻牌活动入口与全局组件声明
- 在 Header Avatar 菜单新增翻牌活动(cardFlip)入口,并添加对应插槽 <card-flip-activity/>
- 在 types/components.d.ts 中添加 CardFlipActivity 与 ElCollapseTransition 类型声明
- 在 .eslintrc-auto-import.json 中新增 ElMessage 与 ElMessageBox 自动导入
- 从 import_meta.d.ts 中移除 VITE_BUILD_COMPRESS 环境声明
- 在 YiAbpWebModule.cs 中添加相关 using 并保留数据库建表初始化的注释(CodeFirst.InitTables)
2025-10-23 21:58:47 +08:00
chenchun
1aaff2942d fix: 调整 Anthropic DTO 属性为可空类型以避免反序列化错误 2025-10-21 16:55:05 +08:00
chenchun
cdbfc5383d feat: 为充值记录新增订单类型字段并区分VIP与套餐逻辑 2025-10-20 10:18:24 +08:00
ccnetcore
f302555e0c feat: 完善描述 2025-10-18 17:40:46 +08:00
ccnetcore
86c5890476 feat: 用户中心新增每日任务组件并在头像菜单中集成 2025-10-18 17:34:46 +08:00
ccnetcore
a13ee395c7 feat: 支持 x-api-key 认证并扩展 Anthropic 响应字段,优化工具调用处理 2025-10-18 13:23:54 +08:00
Gsh
9abcd72aca fix: 增加教程导航 2025-10-16 22:50:10 +08:00
ccnetcore
4ddea6d468 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-16 22:12:36 +08:00
ccnetcore
867a2dc861 fix: 修正Claude聊天响应的Token统计逻辑并优化AiGateWayManager使用条件,同时移除前端无用环境变量定义 2025-10-16 22:11:09 +08:00
chenchun
4a72e3fa0d Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-16 09:35:56 +08:00
chenchun
8b4371aabb feat: 尊享包购买流程新增充值记录保存功能 2025-10-16 09:35:25 +08:00
Gsh
799dd08ec0 feat: 模型提示词、剩余额度、对话状态优化 2025-10-16 01:20:11 +08:00
Gsh
c5c22224cf feat: 2.0发布 2025-10-15 23:44:18 +08:00
ccnetcore
2dae47e85c feat: 修复价格 2025-10-15 23:18:26 +08:00
ccnetcore
375dd4f797 fix: 修复支付3位数问题 2025-10-15 23:04:09 +08:00
ccnetcore
acb2db8397 fix: 商品类型返回值 2025-10-15 23:00:42 +08:00
ccnetcore
b7a3e76d0b Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-15 19:51:28 +08:00
ccnetcore
48150b712a refactor: 会话ID为空时不存储消息内容,并移除无用注释 2025-10-15 19:49:33 +08:00
chenchun
6db9dfc308 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-15 11:54:06 +08:00
chenchun
2d6c1f3c46 fix: 验证交易状态仅在成功时执行充值逻辑 2025-10-15 11:53:54 +08:00
Gsh
161e10d2d1 feat: 产品样式调整 2025-10-15 00:16:57 +08:00
Gsh
a9a2a91183 feat: 产品样式调整 2025-10-15 00:05:10 +08:00
Gsh
1c9a6f108e feat: 产品样式调整 2025-10-15 00:05:10 +08:00
ccnetcore
d6adf9b736 feat: 增加 Claude 模型 Token 使用量倍数调整功能 2025-10-14 23:41:26 +08:00
ccnetcore
959eb3f782 fix: 优化服务号与支付逻辑,增加AccessToken为空校验及优惠描述完善 2025-10-14 23:02:44 +08:00
ccnetcore
7a53e0c90c refactor: 简化尊享包Token扣减逻辑,移除多包分配与校验流程 2025-10-14 22:34:05 +08:00
ccnetcore
533b87fc5b fix: 修复统计近7天token消耗时角色过滤条件错误 2025-10-14 22:22:35 +08:00
ccnetcore
15713cf7fe feat: 支持Claude模型API类型及尊享包校验与扣减逻辑 2025-10-14 22:17:21 +08:00
Gsh
31dc756868 feat: 尊享模型效果 2025-10-14 21:29:20 +08:00
ccnetcore
52f6b6130f feat: 为 HttpClient 添加默认 User-Agent 请求头 2025-10-13 23:08:15 +08:00
ccnetcore
16945b3d5b fix: 修复剩余令牌统计逻辑,增加过期时间判断 2025-10-13 22:09:47 +08:00
Gsh
bdc664fc44 feat: 增加尊享token包产品 2025-10-13 01:14:40 +08:00
Gsh
9555ef10e0 feat: 增加尊享token包产品 2025-10-13 01:03:41 +08:00
Gsh
49e6cb26fc feat: 增加尊享产品 2025-10-12 23:00:08 +08:00
ccnetcore
3ace29e692 fix: 修复无会话时仍存储消息内容的问题 2025-10-12 22:46:20 +08:00
ccnetcore
aa9dd0129b refactor: 将尊享包Token统计逻辑从AiAccountService迁移至UsageStatisticsService,并移除AiUserRoleMenuDto相关字段 2025-10-12 21:51:51 +08:00
ccnetcore
1464271fbd Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-10-12 21:12:37 +08:00
ccnetcore
754f145559 fix: 允许尊享包扣减到负数并优化Token统计逻辑 2025-10-12 21:12:21 +08:00
Gsh
6afd0cb955 feat: 个人中心优化 2025-10-12 21:08:23 +08:00
ccnetcore
d32906702a feat: 商品枚举与支付服务优化,支持中文名称、参考价格及类别筛选 2025-10-12 21:04:08 +08:00
ccnetcore
9bcdaf6bd8 fix: 更新尊享包折扣规则为每10元减2.5元,最多减50元,并同步修改提示文案 2025-10-12 20:14:07 +08:00
ccnetcore
db82a8cf08 Merge branch 'premium' into ai-hub 2025-10-12 20:10:23 +08:00
Gsh
85bd1ce8d6 feat: 个人中心新增尊享服务、模型列表区分 2025-10-12 18:30:34 +08:00
78 changed files with 8908 additions and 931 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"WebFetch(domain:www.donet5.com)"
],
"deny": [],
"ask": []
}
}

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(dotnet build \"E:\\code\\github\\Yi\\Yi.Abp.Net8\\module\\ai-hub\\Yi.Framework.AiHub.Application\\Yi.Framework.AiHub.Application.csproj\" --no-restore)",
"Read(//e/code/github/Yi/Yi.Ai.Vue3/**)"
],
"deny": [],
"ask": []
}
}

View File

@@ -18,19 +18,4 @@ public class AiUserRoleMenuDto:UserRoleMenuDto
/// VIP到期时间 /// VIP到期时间
/// </summary> /// </summary>
public DateTime? VipExpireTime { get; set; } public DateTime? VipExpireTime { get; set; }
/// <summary>
/// 尊享包总Token数
/// </summary>
public long PremiumTotalTokens { get; set; }
/// <summary>
/// 尊享包已使用Token数
/// </summary>
public long PremiumUsedTokens { get; set; }
/// <summary>
/// 尊享包剩余Token数
/// </summary>
public long PremiumRemainingTokens { get; set; }
} }

View File

@@ -0,0 +1,93 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 翻牌任务状态输出
/// </summary>
public class CardFlipStatusOutput
{
/// <summary>
/// 本周总翻牌次数
/// </summary>
public int TotalFlips { get; set; }
/// <summary>
/// 剩余免费次数
/// </summary>
public int RemainingFreeFlips { get; set; }
/// <summary>
/// 剩余赠送次数
/// </summary>
public int RemainingBonusFlips { get; set; }
/// <summary>
/// 剩余邀请解锁次数
/// </summary>
public int RemainingInviteFlips { get; set; }
/// <summary>
/// 是否可以翻牌
/// </summary>
public bool CanFlip { get; set; }
/// <summary>
/// 用户的邀请码
/// </summary>
public string? MyInviteCode { get; set; }
/// <summary>
/// 本周邀请人数
/// </summary>
public int InvitedCount { get; set; }
/// <summary>
/// 是否已被邀请(被邀请后不可再提供邀请码)
/// </summary>
public bool IsInvited { get; set; }
/// <summary>
/// 翻牌记录
/// </summary>
public List<CardFlipRecord> FlipRecords { get; set; } = new();
/// <summary>
/// 下次可翻牌提示
/// </summary>
public string? NextFlipTip { get; set; }
}
/// <summary>
/// 翻牌记录
/// </summary>
public class CardFlipRecord
{
/// <summary>
/// 翻牌序号1-10
/// </summary>
public int FlipNumber { get; set; }
/// <summary>
/// 是否已翻
/// </summary>
public bool IsFlipped { get; set; }
/// <summary>
/// 是否中奖
/// </summary>
public bool IsWin { get; set; }
/// <summary>
/// 奖励金额token数
/// </summary>
public long? RewardAmount { get; set; }
/// <summary>
/// 翻牌类型描述
/// </summary>
public string? FlipTypeDesc { get; set; }
/// <summary>
/// 在翻牌顺序中的位置1-10表示第几个翻
/// </summary>
public int FlipOrderIndex { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 翻牌输入
/// </summary>
public class FlipCardInput
{
/// <summary>
/// 翻牌序号1-10
/// </summary>
public int FlipNumber { get; set; }
}

View File

@@ -0,0 +1,32 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 翻牌输出
/// </summary>
public class FlipCardOutput
{
/// <summary>
/// 翻牌序号1-10
/// </summary>
public int FlipNumber { get; set; }
/// <summary>
/// 是否中奖
/// </summary>
public bool IsWin { get; set; }
/// <summary>
/// 奖励金额token数
/// </summary>
public long? RewardAmount { get; set; }
/// <summary>
/// 奖励描述
/// </summary>
public string? RewardDesc { get; set; }
/// <summary>
/// 剩余可翻次数
/// </summary>
public int RemainingFlips { get; set; }
}

View File

@@ -0,0 +1,48 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 邀请码信息输出
/// </summary>
public class InviteCodeOutput
{
/// <summary>
/// 我的邀请码
/// </summary>
public string? MyInviteCode { get; set; }
/// <summary>
/// 本周邀请人数
/// </summary>
public int InvitedCount { get; set; }
/// <summary>
/// 是否已被邀请
/// </summary>
public bool IsInvited { get; set; }
/// <summary>
/// 邀请历史记录
/// </summary>
public List<InvitationHistoryItem> InvitationHistory { get; set; } = new();
}
/// <summary>
/// 邀请历史记录项
/// </summary>
public class InvitationHistoryItem
{
/// <summary>
/// 被邀请人昵称(脱敏)
/// </summary>
public string InvitedUserName { get; set; } = string.Empty;
/// <summary>
/// 邀请时间
/// </summary>
public DateTime InvitationTime { get; set; }
/// <summary>
/// 本周所在
/// </summary>
public string WeekDescription { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
/// <summary>
/// 使用邀请码输入
/// </summary>
public class UseInviteCodeInput
{
/// <summary>
/// 邀请码
/// </summary>
public string InviteCode { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
/// <summary>
/// 领取任务奖励输入
/// </summary>
public class ClaimTaskRewardInput
{
/// <summary>
/// 任务等级1=1000w任务2=3000w任务
/// </summary>
public int TaskLevel { get; set; }
}

View File

@@ -0,0 +1,58 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
/// <summary>
/// 每日任务状态输出
/// </summary>
public class DailyTaskStatusOutput
{
/// <summary>
/// 今日消耗的尊享包Token数
/// </summary>
public long TodayConsumedTokens { get; set; }
/// <summary>
/// 任务列表
/// </summary>
public List<DailyTaskItem> Tasks { get; set; } = new();
}
/// <summary>
/// 每日任务项
/// </summary>
public class DailyTaskItem
{
/// <summary>
/// 任务等级1=1000w任务2=3000w任务
/// </summary>
public int Level { get; set; }
/// <summary>
/// 任务名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 任务描述
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 任务要求的Token消耗量
/// </summary>
public long RequiredTokens { get; set; }
/// <summary>
/// 奖励的Token数量
/// </summary>
public long RewardTokens { get; set; }
/// <summary>
/// 任务状态0=未完成1=可领取2=已领取
/// </summary>
public int Status { get; set; }
/// <summary>
/// 任务进度百分比0-100
/// </summary>
public decimal Progress { get; set; }
}

View File

@@ -0,0 +1,16 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 获取商品列表输入DTO
/// </summary>
public class GetGoodsListInput
{
/// <summary>
/// 商品类别(可选)
/// 如果不传,则返回所有商品
/// 如果传了则只返回指定类别的商品VIP服务或尊享包
/// </summary>
public GoodsCategoryType? GoodsCategoryType { get; set; }
}

View File

@@ -17,28 +17,39 @@ public class GoodsListOutput
/// </summary> /// </summary>
public decimal OriginalPrice { get; set; } public decimal OriginalPrice { get; set; }
/// <summary>
/// 商品参考价格
/// </summary>
public decimal ReferencePrice { get; set; }
/// <summary> /// <summary>
/// 商品实际价格(折扣后的价格) /// 商品实际价格(折扣后的价格)
/// </summary> /// </summary>
public decimal GoodsPrice { get; set; } public decimal GoodsPrice { get; set; }
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 商品备注
/// </summary>
public string Remark { get; set; }
/// <summary> /// <summary>
/// 折扣金额(仅尊享包) /// 折扣金额(仅尊享包)
/// </summary> /// </summary>
public decimal? DiscountAmount { get; set; } public decimal? DiscountAmount { get; set; }
/// <summary>
/// 商品类别
/// </summary>
public string GoodsCategory { get; set; }
/// <summary>
/// 商品备注
/// </summary>
public string Remark { get; set; }
/// <summary> /// <summary>
/// 折扣说明(仅尊享包) /// 折扣说明(仅尊享包)
/// </summary> /// </summary>
public string? DiscountDescription { get; set; } public string? DiscountDescription { get; set; }
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
} }

View File

@@ -0,0 +1,22 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
/// <summary>
/// 尊享服务Token用量统计DTO
/// </summary>
public class PremiumTokenUsageDto
{
/// <summary>
/// 总Token数
/// </summary>
public long PremiumTotalTokens { get; set; }
/// <summary>
/// 已使用Token数
/// </summary>
public long PremiumUsedTokens { get; set; }
/// <summary>
/// 剩余Token数
/// </summary>
public long PremiumRemainingTokens { get; set; }
}

View File

@@ -0,0 +1,41 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 翻牌服务接口
/// </summary>
public interface ICardFlipService
{
/// <summary>
/// 获取本周翻牌任务状态
/// </summary>
/// <returns></returns>
Task<CardFlipStatusOutput> GetWeeklyTaskStatusAsync();
/// <summary>
/// 翻牌
/// </summary>
/// <param name="input">翻牌输入</param>
/// <returns></returns>
Task<FlipCardOutput> FlipCardAsync(FlipCardInput input);
/// <summary>
/// 使用邀请码解锁翻牌次数
/// </summary>
/// <param name="input">邀请码输入</param>
/// <returns></returns>
Task UseInviteCodeAsync(UseInviteCodeInput input);
/// <summary>
/// 获取我的邀请码信息
/// </summary>
/// <returns></returns>
Task<InviteCodeOutput> GetMyInviteCodeAsync();
/// <summary>
/// 生成我的邀请码(如果没有)
/// </summary>
/// <returns></returns>
Task<string> GenerateMyInviteCodeAsync();
}

View File

@@ -34,6 +34,7 @@ public interface IPayService : IApplicationService
/// <summary> /// <summary>
/// 获取商品列表 /// 获取商品列表
/// </summary> /// </summary>
/// <param name="input">获取商品列表输入</param>
/// <returns>商品列表</returns> /// <returns>商品列表</returns>
Task<List<GoodsListOutput>> GetGoodsListAsync(); Task<List<GoodsListOutput>> GetGoodsListAsync([FromQuery] GetGoodsListInput input);
} }

View File

@@ -18,4 +18,10 @@ public interface IUsageStatisticsService
/// </summary> /// </summary>
/// <returns>模型Token使用量列表</returns> /// <returns>模型Token使用量列表</returns>
Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync(); Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync();
/// <summary>
/// 获取当前用户尊享服务Token用量统计
/// </summary>
/// <returns>尊享服务Token用量统计</returns>
Task<PremiumTokenUsageDto> GetPremiumTokenUsageAsync();
} }

View File

@@ -9,7 +9,6 @@ using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.Rbac.Domain.Shared.Dtos; using Yi.Framework.Rbac.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.AiHub.Domain.Extensions; using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Shared.Consts;
namespace Yi.Framework.AiHub.Application.Services; namespace Yi.Framework.AiHub.Application.Services;
@@ -18,18 +17,15 @@ public class AiAccountService : ApplicationService
private IAccountService _accountService; private IAccountService _accountService;
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository; private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
private ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository; private ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository;
private ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
public AiAccountService( public AiAccountService(
IAccountService accountService, IAccountService accountService,
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository, ISqlSugarRepository<AiUserExtraInfoEntity> userRepository,
ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository, ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository)
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository)
{ {
_accountService = accountService; _accountService = accountService;
_userRepository = userRepository; _userRepository = userRepository;
_rechargeRepository = rechargeRepository; _rechargeRepository = rechargeRepository;
_premiumPackageRepository = premiumPackageRepository;
} }
/// <summary> /// <summary>
@@ -74,23 +70,6 @@ public class AiAccountService : ApplicationService
} }
} }
// 获取尊享包Token信息
var premiumPackages = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId && x.IsActive)
.ToListAsync();
if (premiumPackages.Any())
{
// 过滤掉已过期的包
var validPackages = premiumPackages
.Where(p => p.IsAvailable())
.ToList();
output.PremiumTotalTokens = validPackages.Sum(x => x.TotalTokens);
output.PremiumUsedTokens = validPackages.Sum(x => x.UsedTokens);
output.PremiumRemainingTokens = validPackages.Sum(x => x.RemainingTokens);
}
return output; return output;
} }
} }

View File

@@ -16,6 +16,7 @@ using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Extensions; using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers; using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Enums; using Yi.Framework.AiHub.Domain.Shared.Enums;
@@ -35,17 +36,19 @@ public class AiChatService : ApplicationService
private readonly AiBlacklistManager _aiBlacklistManager; private readonly AiBlacklistManager _aiBlacklistManager;
private readonly ILogger<AiChatService> _logger; private readonly ILogger<AiChatService> _logger;
private readonly AiGateWayManager _aiGateWayManager; private readonly AiGateWayManager _aiGateWayManager;
private readonly PremiumPackageManager _premiumPackageManager;
public AiChatService(IHttpContextAccessor httpContextAccessor, public AiChatService(IHttpContextAccessor httpContextAccessor,
AiBlacklistManager aiBlacklistManager, AiBlacklistManager aiBlacklistManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository, ISqlSugarRepository<AiModelEntity> aiModelRepository,
ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager) ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager, PremiumPackageManager premiumPackageManager)
{ {
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_aiBlacklistManager = aiBlacklistManager; _aiBlacklistManager = aiBlacklistManager;
_aiModelRepository = aiModelRepository; _aiModelRepository = aiModelRepository;
_logger = logger; _logger = logger;
_aiGateWayManager = aiGateWayManager; _aiGateWayManager = aiGateWayManager;
_premiumPackageManager = premiumPackageManager;
} }
@@ -118,6 +121,17 @@ public class AiChatService : ApplicationService
} }
} }
//如果是尊享包服务,需要校验是是否尊享包足够
if (CurrentUser.IsAuthenticated && PremiumPackageConst.ModeIds.Contains(input.Model))
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId());
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
}
//ai网关代理httpcontext //ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input, await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, sessionId, cancellationToken); CurrentUser.Id, sessionId, cancellationToken);

View File

@@ -0,0 +1,286 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 翻牌服务 - 应用层组合服务
/// </summary>
[Authorize]
public class CardFlipService : ApplicationService, ICardFlipService
{
private readonly CardFlipManager _cardFlipManager;
private readonly InviteCodeManager _inviteCodeManager;
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ILogger<CardFlipService> _logger;
public CardFlipService(
CardFlipManager cardFlipManager,
InviteCodeManager inviteCodeManager,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
ILogger<CardFlipService> logger)
{
_cardFlipManager = cardFlipManager;
_inviteCodeManager = inviteCodeManager;
_premiumPackageRepository = premiumPackageRepository;
_logger = logger;
}
/// <summary>
/// 获取本周翻牌任务状态
/// </summary>
public async Task<CardFlipStatusOutput> GetWeeklyTaskStatusAsync()
{
var userId = CurrentUser.GetId();
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
// 获取本周任务
var task = await _cardFlipManager.GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: false);
// 获取邀请码信息
var inviteCode = await _inviteCodeManager.GetUserInviteCodeAsync(userId);
// 统计本周邀请人数
var invitedCount = await _inviteCodeManager.GetWeeklyInvitationCountAsync(userId, weekStart);
// 检查用户是否已被邀请
var isInvited = await _inviteCodeManager.IsUserInvitedAsync(userId);
var output = new CardFlipStatusOutput
{
TotalFlips = task?.TotalFlips ?? 0,
RemainingFreeFlips = CardFlipManager.MAX_FREE_FLIPS - (task?.FreeFlipsUsed ?? 0),
RemainingBonusFlips = 0, // 已废弃
RemainingInviteFlips = CardFlipManager.MAX_INVITE_FLIPS - (task?.InviteFlipsUsed ?? 0),
CanFlip = _cardFlipManager.CanFlipCard(task),
MyInviteCode = inviteCode?.Code,
InvitedCount = invitedCount,
IsInvited = isInvited,
FlipRecords = BuildFlipRecords(task)
};
// 生成提示信息
output.NextFlipTip = GenerateNextFlipTip(output);
return output;
}
/// <summary>
/// 翻牌
/// </summary>
public async Task<FlipCardOutput> FlipCardAsync(FlipCardInput input)
{
var userId = CurrentUser.GetId();
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
// 执行翻牌逻辑(由Manager处理验证和翻牌)
var result = await _cardFlipManager.ExecuteFlipAsync(userId, input.FlipNumber, weekStart);
// 如果中奖,发放奖励
if (result.IsWin)
{
await GrantRewardAsync(userId, result.RewardAmount, $"翻牌活动第{input.FlipNumber}次中奖");
}
// 构建输出
var output = new FlipCardOutput
{
FlipNumber = result.FlipNumber,
IsWin = result.IsWin,
RewardAmount = result.RewardAmount,
RewardDesc = result.RewardDesc,
RemainingFlips = CardFlipManager.TOTAL_MAX_FLIPS - input.FlipNumber
};
return output;
}
/// <summary>
/// 使用邀请码解锁翻牌次数
/// </summary>
public async Task UseInviteCodeAsync(UseInviteCodeInput input)
{
var userId = CurrentUser.GetId();
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
// 获取本周任务
var task = await _cardFlipManager.GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: true);
// 验证是否已经使用了所有邀请解锁次数
if (task.InviteFlipsUsed >= CardFlipManager.MAX_INVITE_FLIPS)
{
throw new UserFriendlyException("本周邀请解锁次数已用完");
}
// 使用邀请码(由Manager处理验证和邀请逻辑)
await _inviteCodeManager.UseInviteCodeAsync(userId, input.InviteCode);
_logger.LogInformation($"用户 {userId} 使用邀请码 {input.InviteCode} 解锁翻牌次数成功");
}
/// <summary>
/// 获取我的邀请码信息
/// </summary>
public async Task<InviteCodeOutput> GetMyInviteCodeAsync()
{
var userId = CurrentUser.GetId();
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
// 获取我的邀请码
var inviteCode = await _inviteCodeManager.GetUserInviteCodeAsync(userId);
// 统计本周邀请人数
var invitedCount = await _inviteCodeManager.GetWeeklyInvitationCountAsync(userId, weekStart);
// 获取邀请历史
var invitationHistory = await _inviteCodeManager.GetInvitationHistoryAsync(userId, 10);
return new InviteCodeOutput
{
MyInviteCode = inviteCode?.Code,
InvitedCount = invitedCount,
IsInvited = inviteCode?.IsUserInvited ?? false,
InvitationHistory = invitationHistory.Select(x => new InvitationHistoryItem
{
InvitedUserName = x.InvitedUserName,
InvitationTime = x.InvitationTime,
WeekDescription = GetWeekDescription(x.InvitationTime)
}).ToList()
};
}
/// <summary>
/// 生成我的邀请码
/// </summary>
public async Task<string> GenerateMyInviteCodeAsync()
{
var userId = CurrentUser.GetId();
// 生成邀请码(由Manager处理)
var code = await _inviteCodeManager.GenerateInviteCodeForUserAsync(userId);
return code;
}
#region
/// <summary>
/// 构建翻牌记录列表
/// </summary>
private List<CardFlipRecord> BuildFlipRecords(CardFlipTaskAggregateRoot? task)
{
var records = new List<CardFlipRecord>();
// 获取已翻牌的顺序
var flippedOrder = task != null ? _cardFlipManager.GetFlippedOrder(task) : new List<int>();
var flippedNumbers = new HashSet<int>(flippedOrder);
// 构建记录按照原始序号1-10排列
for (int i = 1; i <= CardFlipManager.TOTAL_MAX_FLIPS; i++)
{
var record = new CardFlipRecord
{
FlipNumber = i,
IsFlipped = flippedNumbers.Contains(i),
IsWin = false,
FlipTypeDesc = CardFlipManager.GetFlipTypeDesc(i),
// 设置在翻牌顺序中的位置0表示未翻>0表示第几个翻的
FlipOrderIndex = flippedOrder.IndexOf(i) >= 0 ? flippedOrder.IndexOf(i) + 1 : 0
};
// 设置中奖信息
// 判断这张卡是第几次翻的
if (task != null && flippedNumbers.Contains(i))
{
var flipOrderIndex = flippedOrder.IndexOf(i) + 1; // 第几次翻的1-based
// 第8次翻的卡中奖
if (flipOrderIndex == 8)
{
record.IsWin = true;
record.RewardAmount = task.EighthRewardAmount;
}
// 第9次翻的卡中奖
else if (flipOrderIndex == 9)
{
record.IsWin = true;
record.RewardAmount = task.NinthRewardAmount;
}
// 第10次翻的卡中奖
else if (flipOrderIndex == 10)
{
record.IsWin = true;
record.RewardAmount = task.TenthRewardAmount;
}
}
records.Add(record);
}
return records;
}
/// <summary>
/// 生成下次翻牌提示
/// </summary>
private string GenerateNextFlipTip(CardFlipStatusOutput status)
{
if (status.TotalFlips >= CardFlipManager.TOTAL_MAX_FLIPS)
{
return "本周翻牌次数已用完,请下周再来!";
}
if (status.RemainingFreeFlips > 0)
{
return $"本周您还有{status.RemainingFreeFlips}次免费翻牌机会";
}
else if (status.RemainingInviteFlips > 0)
{
if (status.TotalFlips >= 7)
{
return $"本周使用他人邀请码可解锁{status.RemainingInviteFlips}次翻牌,且必中大奖!每次中奖最大额度将翻倍!";
}
return $"本周使用他人邀请码可解锁{status.RemainingInviteFlips}次翻牌,必中大奖!每次中奖最大额度将翻倍!";
}
return "继续加油!";
}
/// <summary>
/// 发放奖励
/// </summary>
private async Task GrantRewardAsync(Guid userId, long amount, string description)
{
var premiumPackage = new PremiumPackageAggregateRoot(userId, amount, description)
{
PurchaseAmount = 0, // 奖励不需要付费
Remark = $"翻牌活动奖励:{amount / 10000}w tokens"
};
await _premiumPackageRepository.InsertAsync(premiumPackage);
_logger.LogInformation($"用户 {userId} 获得翻牌奖励 {amount / 10000}w tokens");
}
/// <summary>
/// 获取周描述
/// </summary>
private string GetWeekDescription(DateTime date)
{
var weekStart = CardFlipManager.GetWeekStartDate(date);
var weekEnd = weekStart.AddDays(6);
return $"{weekStart:MM-dd} 至 {weekEnd:MM-dd}";
}
#endregion
}

View File

@@ -0,0 +1,185 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using SqlSugar;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 每日任务服务
/// </summary>
[Authorize]
public class DailyTaskService : ApplicationService
{
private readonly ISqlSugarRepository<DailyTaskRewardRecordAggregateRoot> _dailyTaskRepository;
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ILogger<DailyTaskService> _logger;
// 任务配置
private readonly Dictionary<int, (long RequiredTokens, long RewardTokens, string Name, string Description)>
_taskConfigs = new()
{
{ 1, (10000000, 2000000, "尊享包1000w token任务", "累积使用尊享包 1000w token") }, // 1000w消耗 -> 200w奖励
{ 2, (30000000, 4000000, "尊享包3000w token任务", "累积使用尊享包 3000w token") } // 3000w消耗 -> 600w奖励
};
public DailyTaskService(
ISqlSugarRepository<DailyTaskRewardRecordAggregateRoot> dailyTaskRepository,
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
ILogger<DailyTaskService> logger)
{
_dailyTaskRepository = dailyTaskRepository;
_messageRepository = messageRepository;
_premiumPackageRepository = premiumPackageRepository;
_logger = logger;
}
/// <summary>
/// 获取今日任务状态
/// </summary>
/// <returns></returns>
public async Task<DailyTaskStatusOutput> GetTodayTaskStatusAsync()
{
var userId = CurrentUser.GetId();
var today = DateTime.Today;
// 1. 统计今日尊享包Token消耗量
var todayConsumed = await GetTodayPremiumTokenConsumptionAsync(userId, today);
// 2. 查询今日已领取的任务
var claimedTasks = await _dailyTaskRepository._DbQueryable
.Where(x => x.UserId == userId && x.TaskDate == today)
.Select(x => new { x.TaskLevel, x.IsRewarded })
.ToListAsync();
// 3. 构建任务列表
var tasks = new List<DailyTaskItem>();
foreach (var (level, config) in _taskConfigs)
{
var claimed = claimedTasks.FirstOrDefault(x => x.TaskLevel == level);
int status;
if (claimed != null && claimed.IsRewarded)
{
status = 2; // 已领取
}
else if (todayConsumed >= config.RequiredTokens)
{
status = 1; // 可领取
}
else
{
status = 0; // 未完成
}
var progress = todayConsumed >= config.RequiredTokens
? 100
: Math.Round((decimal)todayConsumed / config.RequiredTokens * 100, 2);
tasks.Add(new DailyTaskItem
{
Level = level,
Name = config.Name,
Description = config.Description,
RequiredTokens = config.RequiredTokens,
RewardTokens = config.RewardTokens,
Status = status,
Progress = progress
});
}
return new DailyTaskStatusOutput
{
TodayConsumedTokens = todayConsumed,
Tasks = tasks
};
}
/// <summary>
/// 领取任务奖励
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task ClaimTaskRewardAsync(ClaimTaskRewardInput input)
{
var userId = CurrentUser.GetId();
var today = DateTime.Today;
// 1. 验证任务等级
if (!_taskConfigs.TryGetValue(input.TaskLevel, out var taskConfig))
{
throw new UserFriendlyException($"无效的任务等级: {input.TaskLevel}");
}
// 2. 检查是否已领取
var existingRecord = await _dailyTaskRepository._DbQueryable
.Where(x => x.UserId == userId && x.TaskDate == today && x.TaskLevel == input.TaskLevel)
.FirstAsync();
if (existingRecord != null)
{
throw new UserFriendlyException("今日该任务奖励已领取,请明天再来!");
}
// 3. 验证今日Token消耗是否达标
var todayConsumed = await GetTodayPremiumTokenConsumptionAsync(userId, today);
if (todayConsumed < taskConfig.RequiredTokens)
{
throw new UserFriendlyException(
$"Token消耗未达标需要 {taskConfig.RequiredTokens / 10000}w当前 {todayConsumed / 10000}w");
}
// 4. 创建奖励包(使用 PremiumPackageManager
var premiumPackage =
new PremiumPackageAggregateRoot(userId, taskConfig.RewardTokens, $"每日任务:{taskConfig.Name}")
{
PurchaseAmount = 0, // 奖励不需要付费
Remark = $"{today:yyyy-MM-dd} 每日任务奖励"
};
await _premiumPackageRepository.InsertAsync(premiumPackage);
// 5. 记录领取记录
var record = new DailyTaskRewardRecordAggregateRoot(userId, input.TaskLevel, today, taskConfig.RewardTokens)
{
Remark = $"完成任务{input.TaskLevel},名称:{taskConfig.Name},消耗 {todayConsumed / 10000}w token"
};
await _dailyTaskRepository.InsertAsync(record);
_logger.LogInformation(
$"用户 {userId} 领取每日任务 {input.TaskLevel} 奖励成功,获得 {taskConfig.RewardTokens / 10000}w tokens");
}
/// <summary>
/// 获取今日尊享包Token消耗量
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="today">今日日期</param>
/// <returns>消耗的Token总数</returns>
private async Task<long> GetTodayPremiumTokenConsumptionAsync(Guid userId, DateTime today)
{
var tomorrow = today.AddDays(1);
// 查询今日所有使用尊享包模型的消息role=system 表示消耗)
var totalTokens = await _messageRepository._DbQueryable
.Where(x => x.UserId == userId)
.Where(x => x.Role == "system") // system角色表示实际消耗
.Where(x => PremiumPackageConst.ModeIds.Contains(x.ModelId)) // 尊享包模型
.Where(x => x.CreationTime >= today && x.CreationTime < tomorrow)
.SumAsync(x => x.TokenUsage.TotalTokenCount);
return totalTokens;
}
}

View File

@@ -1,5 +1,4 @@
using System.Text; using System.Text;
using System.Text.Json;
using System.Xml.Serialization; using System.Xml.Serialization;
using Medallion.Threading; using Medallion.Threading;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -18,7 +17,7 @@ using Yi.Framework.Rbac.Application.Contracts.Dtos.Account;
using Yi.Framework.Rbac.Application.Contracts.IServices; using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services; namespace Yi.Framework.AiHub.Application.Services.Fuwuhao;
/// <summary> /// <summary>
/// 服务号服务 /// 服务号服务

View File

@@ -57,6 +57,18 @@ public class OpenApiService : ApplicationService
var httpContext = this._httpContextAccessor.HttpContext; var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext)); var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId); await _aiBlacklistManager.VerifiyAiBlacklist(userId);
//如果是尊享包服务,需要校验是是否尊享包足够
if (PremiumPackageConst.ModeIds.Contains(input.Model))
{
// 检查尊享token包用量
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
if (availableTokens <= 0)
{
throw new UserFriendlyException("尊享token包用量不足请先购买尊享token包");
}
}
//ai网关代理httpcontext //ai网关代理httpcontext
if (input.Stream == true) if (input.Stream == true)
{ {
@@ -181,11 +193,16 @@ public class OpenApiService : ApplicationService
private string? GetTokenByHttpContext(HttpContext httpContext) private string? GetTokenByHttpContext(HttpContext httpContext)
{ {
// 获取Authorization头 // 优先从 x-api-key 获取
string authHeader = httpContext.Request.Headers["Authorization"]; string apiKeyHeader = httpContext.Request.Headers["x-api-key"];
if (!string.IsNullOrWhiteSpace(apiKeyHeader))
{
return apiKeyHeader.Trim();
}
// 检查是否有Bearer token // 检查 Authorization 头
if (authHeader != null && authHeader.StartsWith("Bearer ")) string authHeader = httpContext.Request.Headers["Authorization"];
if (!string.IsNullOrWhiteSpace(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{ {
return authHeader.Substring("Bearer ".Length).Trim(); return authHeader.Substring("Bearer ".Length).Trim();
} }

View File

@@ -112,6 +112,13 @@ public class PayService : ApplicationService, IPayService
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus); _logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
// 验证交易状态,只有交易成功才执行充值逻辑
if (status != TradeStatusEnum.TRADE_SUCCESS)
{
_logger.LogError($"订单 {outTradeNo} 状态为 {tradeStatus},不执行充值逻辑");
return "success";
}
// 5. 根据商品类型进行不同的处理 // 5. 根据商品类型进行不同的处理
if (order.GoodsType.IsPremiumPackage()) if (order.GoodsType.IsPremiumPackage())
{ {
@@ -189,9 +196,10 @@ public class PayService : ApplicationService, IPayService
/// <summary> /// <summary>
/// 获取商品列表 /// 获取商品列表
/// </summary> /// </summary>
/// <param name="input">获取商品列表输入</param>
/// <returns>商品列表</returns> /// <returns>商品列表</returns>
[HttpGet("pay/GoodsList")] [HttpGet("pay/GoodsList")]
public async Task<List<GoodsListOutput>> GetGoodsListAsync() public async Task<List<GoodsListOutput>> GetGoodsListAsync([FromQuery] GetGoodsListInput input)
{ {
var goodsList = new List<GoodsListOutput>(); var goodsList = new List<GoodsListOutput>();
@@ -205,36 +213,56 @@ public class PayService : ApplicationService, IPayService
// 遍历所有商品枚举 // 遍历所有商品枚举
foreach (GoodsTypeEnum goodsType in Enum.GetValues(typeof(GoodsTypeEnum))) foreach (GoodsTypeEnum goodsType in Enum.GetValues(typeof(GoodsTypeEnum)))
{ {
// 如果指定了商品类别,则过滤
if (input.GoodsCategoryType.HasValue)
{
var goodsCategory = goodsType.GetGoodsCategory();
if (goodsCategory != input.GoodsCategoryType.Value)
{
continue; // 跳过不匹配的商品
}
}
var originalPrice = goodsType.GetTotalAmount(); var originalPrice = goodsType.GetTotalAmount();
decimal actualPrice = originalPrice; decimal actualPrice = originalPrice;
decimal? discountAmount = null; decimal? discountAmount = null;
string? discountDescription = null; string? discountDescription = null;
// 如果是尊享包商品,计算折扣 // 如果是尊享包商品,计算折扣
if (goodsType.IsPremiumPackage() && CurrentUser.IsAuthenticated) if (goodsType.IsPremiumPackage())
{
if (CurrentUser.IsAuthenticated)
{ {
discountAmount = goodsType.CalculateDiscount(totalRechargeAmount); discountAmount = goodsType.CalculateDiscount(totalRechargeAmount);
actualPrice = goodsType.GetDiscountedPrice(totalRechargeAmount); actualPrice = goodsType.GetDiscountedPrice(totalRechargeAmount);
if (discountAmount > 0) if (discountAmount > 0)
{ {
discountDescription = $"已优惠 ¥{discountAmount:F2}累计充值每10元减1元最多减20元"; discountDescription = $"根据累积充值已优惠 ¥{discountAmount:F2}";
} }
else else
{ {
discountDescription = "累充值每10元可减1元最多减20元"; discountDescription = $"累充值过低,暂无优惠";
}
}
else
{
discountDescription = $"登录后查看优惠";
} }
} }
var goodsItem = new GoodsListOutput var goodsItem = new GoodsListOutput
{ {
GoodsName = goodsType.GetDisplayName(), GoodsName = goodsType.GetChineseName(),
OriginalPrice = originalPrice, OriginalPrice = originalPrice,
ReferencePrice = goodsType.GetReferencePrice(),
GoodsPrice = actualPrice, GoodsPrice = actualPrice,
GoodsType = goodsType, GoodsCategory = goodsType.GetGoodsCategory().ToString(),
Remark = GetGoodsRemark(goodsType), Remark = goodsType.GetRemark(),
DiscountAmount = discountAmount, DiscountAmount = discountAmount,
DiscountDescription = discountDescription DiscountDescription = discountDescription,
GoodsType = goodsType
}; };
goodsList.Add(goodsItem); goodsList.Add(goodsItem);
@@ -243,27 +271,6 @@ public class PayService : ApplicationService, IPayService
return goodsList; return goodsList;
} }
/// <summary>
/// 获取商品备注信息
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>商品备注</returns>
private string GetGoodsRemark(GoodsTypeEnum goodsType)
{
if (goodsType.IsPremiumPackage())
{
var tokenAmount = goodsType.GetTokenAmount();
return $"尊享包服务,提供 {tokenAmount:N0} Tokens需要VIP资格";
}
else if (goodsType.IsVipService())
{
var validMonths = goodsType.GetValidMonths();
var monthlyPrice = goodsType.GetMonthlyPrice();
return $"VIP服务有效期 {validMonths} 个月,月均价 ¥{monthlyPrice:F2}";
}
return "未知商品类型";
}
/// <summary> /// <summary>
/// 获取交易状态描述 /// 获取交易状态描述
@@ -288,6 +295,7 @@ public class PayService : ApplicationService, IPayService
{ {
return result; return result;
} }
return TradeStatusEnum.WAIT_TRADE; return TradeStatusEnum.WAIT_TRADE;
} }
} }

View File

@@ -8,6 +8,7 @@ using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Managers; using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.Rbac.Application.Contracts.IServices; using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
@@ -64,6 +65,7 @@ namespace Yi.Framework.AiHub.Application.Services
{ {
// 直接查询该用户最大的过期时间 // 直接查询该用户最大的过期时间
var maxExpireTime = await _repository._DbQueryable var maxExpireTime = await _repository._DbQueryable
.Where(x => x.RechargeType == RechargeTypeEnum.Vip)
.Where(x => x.UserId == input.UserId && x.ExpireDateTime.HasValue) .Where(x => x.UserId == input.UserId && x.ExpireDateTime.HasValue)
.MaxAsync(x => x.ExpireDateTime); .MaxAsync(x => x.ExpireDateTime);
@@ -85,7 +87,8 @@ namespace Yi.Framework.AiHub.Application.Services
Content = input.Content, Content = input.Content,
ExpireDateTime = expireDateTime, ExpireDateTime = expireDateTime,
Remark = input.Remark, Remark = input.Remark,
ContactInfo = input.ContactInfo ContactInfo = input.ContactInfo,
RechargeType = RechargeTypeEnum.Vip
}; };
// 保存充值记录到数据库 // 保存充值记录到数据库

View File

@@ -6,6 +6,7 @@ using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
using Yi.Framework.AiHub.Application.Contracts.IServices; using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Chat; using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services; namespace Yi.Framework.AiHub.Application.Services;
@@ -18,13 +19,16 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
{ {
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository; private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository; private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
public UsageStatisticsService( public UsageStatisticsService(
ISqlSugarRepository<MessageAggregateRoot> messageRepository, ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository) ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository)
{ {
_messageRepository = messageRepository; _messageRepository = messageRepository;
_usageStatisticsRepository = usageStatisticsRepository; _usageStatisticsRepository = usageStatisticsRepository;
_premiumPackageRepository = premiumPackageRepository;
} }
/// <summary> /// <summary>
@@ -40,6 +44,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
// 从Message表统计近7天的token消耗 // 从Message表统计近7天的token消耗
var dailyUsage = await _messageRepository._DbQueryable var dailyUsage = await _messageRepository._DbQueryable
.Where(x => x.UserId == userId) .Where(x => x.UserId == userId)
.Where(x => x.Role == "assistant" || x.Role == "system")
.Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1)) .Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1))
.GroupBy(x => x.CreationTime.Date) .GroupBy(x => x.CreationTime.Date)
.Select(g => new .Select(g => new
@@ -102,4 +107,34 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
return result; return result;
} }
/// <summary>
/// 获取当前用户尊享服务Token用量统计
/// </summary>
/// <returns>尊享服务Token用量统计</returns>
public async Task<PremiumTokenUsageDto> GetPremiumTokenUsageAsync()
{
var userId = CurrentUser.GetId();
// 获取尊享包Token信息
var premiumPackages = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId && x.IsActive)
.ToListAsync();
var result = new PremiumTokenUsageDto();
if (premiumPackages.Any())
{
// 过滤掉已过期的包
var validPackages = premiumPackages
.Where(p => p.IsAvailable())
.ToList();
result.PremiumTotalTokens = validPackages.Sum(x => x.TotalTokens);
result.PremiumUsedTokens = validPackages.Sum(x => x.UsedTokens);
result.PremiumRemainingTokens = validPackages.Sum(x => x.RemainingTokens);
}
return result;
}
} }

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Yi.Framework.AiHub.Domain.Shared.Consts;
public class PremiumPackageConst
{
public static List<string> ModeIds = ["claude-sonnet-4-5-20250929"];
}

View File

@@ -32,6 +32,25 @@ public class AnthropicStreamDto
PromptTokensDetails = null, PromptTokensDetails = null,
CompletionTokensDetails = null CompletionTokensDetails = null
}; };
public void SupplementalMultiplier(double multiplier)
{
if (this.Usage is not null)
{
this.Usage.CacheCreationInputTokens =
(int)Math.Round((this.Usage.CacheCreationInputTokens ?? 0) * multiplier);
this.Usage.CacheReadInputTokens =
(int)Math.Round((this.Usage.CacheReadInputTokens ?? 0) * multiplier);
this.Usage.InputTokens =
(int)Math.Round((this.Usage.InputTokens ?? 0) * multiplier);
this.Usage.OutputTokens =
(int)Math.Round((this.Usage.OutputTokens ?? 0) * multiplier);
}
}
} }
public class AnthropicStreamErrorDto public class AnthropicStreamErrorDto
@@ -75,6 +94,8 @@ public class AnthropicChatCompletionDtoContentBlock
[JsonPropertyName("tool_use_id")] public string? ToolUseId { get; set; } [JsonPropertyName("tool_use_id")] public string? ToolUseId { get; set; }
[JsonPropertyName("content")] public object? Content { get; set; } [JsonPropertyName("content")] public object? Content { get; set; }
[JsonPropertyName("text")] public string? Text { get; set; }
} }
public class AnthropicChatCompletionDto public class AnthropicChatCompletionDto
@@ -93,7 +114,7 @@ public class AnthropicChatCompletionDto
public object stop_sequence { get; set; } public object stop_sequence { get; set; }
public AnthropicCompletionDtoUsage Usage { get; set; } public AnthropicCompletionDtoUsage? Usage { get; set; }
[JsonIgnore] [JsonIgnore]
public ThorUsageResponse TokenUsage => new ThorUsageResponse public ThorUsageResponse TokenUsage => new ThorUsageResponse
@@ -108,6 +129,24 @@ public class AnthropicChatCompletionDto
PromptTokensDetails = null, PromptTokensDetails = null,
CompletionTokensDetails = null CompletionTokensDetails = null
}; };
public void SupplementalMultiplier(double multiplier)
{
if (this.Usage is not null)
{
this.Usage.CacheCreationInputTokens =
(int)Math.Round((this.Usage?.CacheCreationInputTokens ?? 0) * multiplier);
this.Usage.CacheReadInputTokens =
(int)Math.Round((this.Usage?.CacheReadInputTokens ?? 0) * multiplier);
this.Usage.InputTokens =
(int)Math.Round((this.Usage?.InputTokens ?? 0) * multiplier);
this.Usage.OutputTokens =
(int)Math.Round((this.Usage?.OutputTokens ?? 0) * multiplier);
}
}
} }
public class AnthropicChatCompletionDtoContent public class AnthropicChatCompletionDtoContent

View File

@@ -108,9 +108,9 @@ public sealed class AnthropicInput
public class AnthropicThinkingInput public class AnthropicThinkingInput
{ {
[JsonPropertyName("type")] public string Type { get; set; } [JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("budget_tokens")] public int BudgetTokens { get; set; } [JsonPropertyName("budget_tokens")] public int? BudgetTokens { get; set; }
} }
public class AnthropicTooChoiceInput public class AnthropicTooChoiceInput
@@ -122,16 +122,16 @@ public class AnthropicTooChoiceInput
public class AnthropicMessageTool public class AnthropicMessageTool
{ {
[JsonPropertyName("name")] public string name { get; set; } [JsonPropertyName("name")] public string? name { get; set; }
[JsonPropertyName("description")] public string? Description { get; set; } [JsonPropertyName("description")] public string? Description { get; set; }
[JsonPropertyName("input_schema")] public Input_schema InputSchema { get; set; } [JsonPropertyName("input_schema")] public Input_schema? InputSchema { get; set; }
} }
public class Input_schema public class Input_schema
{ {
[JsonPropertyName("type")] public string Type { get; set; } [JsonPropertyName("type")] public string? Type { get; set; }
[JsonPropertyName("properties")] public Dictionary<string, InputSchemaValue>? Properties { get; set; } [JsonPropertyName("properties")] public Dictionary<string, InputSchemaValue>? Properties { get; set; }
@@ -140,9 +140,9 @@ public class Input_schema
public class InputSchemaValue public class InputSchemaValue
{ {
public string type { get; set; } public string? type { get; set; }
public string description { get; set; } public string? description { get; set; }
public InputSchemaValueItems? items { get; set; } public InputSchemaValueItems? items { get; set; }
} }

View File

@@ -60,4 +60,19 @@ public record ThorChatCompletionsResponse
/// </summary> /// </summary>
[JsonPropertyName("error")] [JsonPropertyName("error")]
public ThorError? Error { get; set; } public ThorError? Error { get; set; }
public void SupplementalMultiplier(double multiplier)
{
if (this.Usage is not null)
{
this.Usage.InputTokens =
(int)Math.Round((this.Usage.InputTokens ?? 0) * multiplier);
this.Usage.OutputTokens =
(int)Math.Round((this.Usage.OutputTokens ?? 0) * multiplier);
this.Usage.CompletionTokens =
(int)Math.Round((this.Usage.CompletionTokens ?? 0) * multiplier);
this.Usage.PromptTokens =
(int)Math.Round((this.Usage.PromptTokens ?? 0) * multiplier);
}
}
} }

View File

@@ -10,12 +10,16 @@ namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public class PriceAttribute : Attribute public class PriceAttribute : Attribute
{ {
public decimal Price { get; } public decimal Price { get; }
public decimal ReferencePrice { get; }
public int ValidMonths { get; } public int ValidMonths { get; }
public PriceAttribute(double price, int validMonths) public PriceAttribute(double price, int validMonths, double referencePrice)
{ {
Price = (decimal)price; Price = (decimal)price;
ValidMonths = validMonths; ValidMonths = validMonths;
ReferencePrice = (decimal)referencePrice;
} }
} }
@@ -26,10 +30,14 @@ public class PriceAttribute : Attribute
public class DisplayNameAttribute : Attribute public class DisplayNameAttribute : Attribute
{ {
public string DisplayName { get; } public string DisplayName { get; }
public string ChineseName { get; }
public string Remark { get; }
public DisplayNameAttribute(string displayName) public DisplayNameAttribute(string displayName, string chineseName = "", string remark = "")
{ {
DisplayName = displayName; DisplayName = displayName;
ChineseName = chineseName;
Remark = remark;
} }
} }
@@ -56,7 +64,7 @@ public enum GoodsCategoryType
/// <summary> /// <summary>
/// VIP服务 /// VIP服务
/// </summary> /// </summary>
VipService = 1, Vip = 1,
/// <summary> /// <summary>
/// 尊享包服务 /// 尊享包服务
@@ -85,39 +93,32 @@ public class TokenAmountAttribute : Attribute
public enum GoodsTypeEnum public enum GoodsTypeEnum
{ {
// VIP服务 // VIP服务
[Price(29.9, 1)] [Price(29.9, 1, 29.9)] [DisplayName("YiXinVip 1 month", "1个月", "灵活选择")] [GoodsCategory(GoodsCategoryType.Vip)]
[DisplayName("YiXinVip 1 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip1 = 1, YiXinVip1 = 1,
[Price(83.7, 3)] [Price(83.7, 3, 27.9)] [DisplayName("YiXinVip 3 month", "3个月", "短期体验")] [GoodsCategory(GoodsCategoryType.Vip)]
[DisplayName("YiXinVip 3 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip3 = 3, YiXinVip3 = 3,
[Price(155.4, 6)] [Price(155.4, 6, 25.9)] [DisplayName("YiXinVip 6 month", "6个月", "年度热销")] [GoodsCategory(GoodsCategoryType.Vip)]
[DisplayName("YiXinVip 6 month")]
[GoodsCategory(GoodsCategoryType.VipService)]
YiXinVip6 = 6, YiXinVip6 = 6,
[Price(183.2, 8)] [Price(183.2, 8, 22.9)]
[DisplayName("YiXinVip 8 month")] [DisplayName("YiXinVip 8 month", "8个月推荐", "限时活动,超高性价比")]
[GoodsCategory(GoodsCategoryType.VipService)] [GoodsCategory(GoodsCategoryType.Vip)]
YiXinVip8 = 8, YiXinVip8 = 8,
// 尊享包服务 - 需要VIP资格才能购买 // 尊享包服务 - 需要VIP资格才能购买
[Price(188.9, 0)] [Price(188.9, 0, 1750)]
[DisplayName("Premium Package 5000W Tokens")] [DisplayName("YiXinPremiumPackage 5000W Tokens", "5000万Tokens", "简单尝试")]
[GoodsCategory(GoodsCategoryType.PremiumPackage)] [GoodsCategory(GoodsCategoryType.PremiumPackage)]
[TokenAmount(5000)] [TokenAmount(50000000)]
PremiumPackage5000W = 101, PremiumPackage5000W = 101,
[Price(248.9, 0)] [Price(248.9, 0, 3500)]
[DisplayName("Premium Package 10000W Tokens")] [DisplayName("YiXinPremiumPackage 10000W Tokens", "1亿Tokens推荐", "极致性价比")]
[GoodsCategory(GoodsCategoryType.PremiumPackage)] [GoodsCategory(GoodsCategoryType.PremiumPackage)]
[TokenAmount(10000)] [TokenAmount(100000000)]
PremiumPackage10000W = 102, PremiumPackage10000W = 102,
} }
public static class GoodsTypeEnumExtensions public static class GoodsTypeEnumExtensions
@@ -181,6 +182,18 @@ public static class GoodsTypeEnumExtensions
return validMonths > 0 ? totalPrice / validMonths : 0m; return validMonths > 0 ? totalPrice / validMonths : 0m;
} }
/// <summary>
/// 获取商品参考价格
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>参考价格</returns>
public static decimal GetReferencePrice(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
return priceAttribute?.ReferencePrice ?? 0m;
}
/// <summary> /// <summary>
/// 获取商品类别 /// 获取商品类别
/// </summary> /// </summary>
@@ -190,7 +203,7 @@ public static class GoodsTypeEnumExtensions
{ {
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString()); var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var categoryAttribute = fieldInfo?.GetCustomAttribute<GoodsCategoryAttribute>(); var categoryAttribute = fieldInfo?.GetCustomAttribute<GoodsCategoryAttribute>();
return categoryAttribute?.Category ?? GoodsCategoryType.VipService; return categoryAttribute?.Category ?? GoodsCategoryType.Vip;
} }
/// <summary> /// <summary>
@@ -210,7 +223,7 @@ public static class GoodsTypeEnumExtensions
/// <returns>是否为VIP服务</returns> /// <returns>是否为VIP服务</returns>
public static bool IsVipService(this GoodsTypeEnum goodsType) public static bool IsVipService(this GoodsTypeEnum goodsType)
{ {
return goodsType.GetGoodsCategory() == GoodsCategoryType.VipService; return goodsType.GetGoodsCategory() == GoodsCategoryType.Vip;
} }
/// <summary> /// <summary>
@@ -225,9 +238,33 @@ public static class GoodsTypeEnumExtensions
return tokenAttribute?.TokenAmount ?? 0; return tokenAttribute?.TokenAmount ?? 0;
} }
/// <summary>
/// 获取商品中文名称
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>中文名称</returns>
public static string GetChineseName(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
return displayNameAttribute?.ChineseName ?? goodsType.ToString();
}
/// <summary>
/// 获取商品备注
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>备注信息</returns>
public static string GetRemark(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
return displayNameAttribute?.Remark ?? string.Empty;
}
/// <summary> /// <summary>
/// 计算折扣金额(仅用于尊享包) /// 计算折扣金额(仅用于尊享包)
/// 规则每累加充值10元减少1元,最多减少20元 /// 规则每累加充值10元减少2.5元,最多减少50元
/// </summary> /// </summary>
/// <param name="goodsType">商品类型</param> /// <param name="goodsType">商品类型</param>
/// <param name="totalRechargeAmount">用户累加充值金额</param> /// <param name="totalRechargeAmount">用户累加充值金额</param>
@@ -240,11 +277,11 @@ public static class GoodsTypeEnumExtensions
return 0m; return 0m;
} }
// 每10元减1 // 每10元减2.5
var discountAmount = Math.Floor(totalRechargeAmount / 10m); var discountAmount = Math.Floor(totalRechargeAmount / 2.5m);
// 最多减少20元 // 最多减少50元
return Math.Min(discountAmount, 20m); return Math.Min(discountAmount, 50m);
} }
/// <summary> /// <summary>
@@ -260,6 +297,6 @@ public static class GoodsTypeEnumExtensions
var discountedPrice = originalPrice - discount; var discountedPrice = originalPrice - discount;
// 确保价格不为负数至少为0.01元 // 确保价格不为负数至少为0.01元
return Math.Max(discountedPrice, 0.01m); return Math.Round(discountedPrice, 2);
} }
} }

View File

@@ -0,0 +1,7 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum ModelApiTypeEnum
{
OpenAi,
Claude
}

View File

@@ -0,0 +1,7 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum RechargeTypeEnum
{
Vip = 10,
PremiumPackage = 20
}

View File

@@ -9,9 +9,13 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats; namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactory,ILogger<AnthropicChatCompletionsService> logger) public class AnthropicChatCompletionsService(
IHttpClientFactory httpClientFactory,
ILogger<AnthropicChatCompletionsService> logger)
: IAnthropicChatCompletionService : IAnthropicChatCompletionService
{ {
public const double ClaudeMultiplier = 1.3d;
public async Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe options, AnthropicInput input, public async Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe options, AnthropicInput input,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
@@ -22,6 +26,7 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
{ {
options.Endpoint = "https://api.anthropic.com/"; options.Endpoint = "https://api.anthropic.com/";
} }
var client = httpClientFactory.CreateClient(); var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string> var headers = new Dictionary<string, string>
@@ -71,20 +76,23 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
if (response.StatusCode >= HttpStatusCode.BadRequest) if (response.StatusCode >= HttpStatusCode.BadRequest)
{ {
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint, logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error); response.StatusCode, error);
throw new Exception("OpenAI对话异常" + response.StatusCode.ToString()); throw new Exception( $"恭喜你运气爆棚遇到了错误尊享包对话异常StatusCode【{response.StatusCode}】Response【{error}】");
} }
var value = var value =
await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions, await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
value.SupplementalMultiplier(AnthropicChatCompletionsService.ClaudeMultiplier);
return value; return value;
} }
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe options, AnthropicInput input, public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe options,
AnthropicInput input,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
using var openai = using var openai =
@@ -117,7 +125,8 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
if (response.StatusCode >= HttpStatusCode.BadRequest) if (response.StatusCode >= HttpStatusCode.BadRequest)
{ {
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint, logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error); response.StatusCode, error);
throw new Exception("OpenAI对话异常" + response.StatusCode); throw new Exception("OpenAI对话异常" + response.StatusCode);
@@ -161,6 +170,7 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
var result = JsonSerializer.Deserialize<AnthropicStreamDto>(data, var result = JsonSerializer.Deserialize<AnthropicStreamDto>(data,
ThorJsonSerializer.DefaultOptions); ThorJsonSerializer.DefaultOptions);
result.SupplementalMultiplier(AnthropicChatCompletionsService.ClaudeMultiplier);
yield return (eventType, result); yield return (eventType, result);
} }
} }

View File

@@ -0,0 +1,879 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
public sealed class ClaudiaChatCompletionsService(
IHttpClientFactory httpClientFactory,
ILogger<ClaudiaChatCompletionsService> logger)
: IChatCompletionService
{
public List<ThorChatChoiceResponse> CreateResponse(AnthropicChatCompletionDto completionDto)
{
var response = new ThorChatChoiceResponse();
var chatMessage = new ThorChatMessage();
if (completionDto == null)
{
return new List<ThorChatChoiceResponse>();
}
if (completionDto.content.Any(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)))
{
// 将推理字段合并到返回对象去
chatMessage.ReasoningContent = completionDto.content
.First(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)).Thinking;
chatMessage.Role = completionDto.role;
chatMessage.Content = completionDto.content
.First(x => x.type.Equals("text", StringComparison.OrdinalIgnoreCase)).text;
}
else
{
chatMessage.Role = completionDto.role;
chatMessage.Content = completionDto.content
.FirstOrDefault()?.text;
}
response.Delta = chatMessage;
response.Message = chatMessage;
if (completionDto.content.Any(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase)))
{
var toolUse = completionDto.content
.First(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase));
chatMessage.ToolCalls =
[
new()
{
Id = toolUse.id,
Function = new ThorChatMessageFunction()
{
Name = toolUse.name,
Arguments = JsonSerializer.Serialize(toolUse.input,
ThorJsonSerializer.DefaultOptions),
},
Index = 0,
}
];
return
[
response
];
}
return new List<ThorChatChoiceResponse> { response };
}
private object CreateMessage(List<ThorChatMessage> messages, AiModelDescribe options)
{
var list = new List<object>();
foreach (var message in messages)
{
// 如果是图片
if (message.ContentCalculated is IList<ThorChatMessageContent> contentCalculated)
{
list.Add(new
{
role = message.Role,
content = (List<object>)contentCalculated.Select<ThorChatMessageContent, object>(x =>
{
if (x.Type == "text")
{
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
return new
{
type = "text",
text = x.Text,
cache_control = new
{
type = "ephemeral"
}
};
}
return new
{
type = "text",
text = x.Text
};
}
var isBase64 = x.ImageUrl?.Url.StartsWith("http") == true;
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
return new
{
type = "image",
source = new
{
type = isBase64 ? "base64" : "url",
media_type = "image/png",
data = x.ImageUrl?.Url,
},
cache_control = new
{
type = "ephemeral"
}
};
}
return new
{
type = "image",
source = new
{
type = isBase64 ? "base64" : "url",
media_type = "image/png",
data = x.ImageUrl?.Url,
}
};
})
});
}
else
{
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
{
if (message.Role == "system")
{
list.Add(new
{
type = "text",
text = message.Content,
cache_control = new
{
type = "ephemeral"
}
});
}
else
{
list.Add(new
{
role = message.Role,
content = message.Content
});
}
}
else
{
if (message.Role == "system")
{
list.Add(new
{
type = "text",
text = message.Content
});
}
else if (message.Role == "tool")
{
list.Add(new
{
role = "user",
content = new List<object>
{
new
{
type = "tool_result",
tool_use_id = message.ToolCallId,
content = message.Content
}
}
});
}
else if (message.Role == "assistant")
{
// {
// "role": "assistant",
// "content": [
// {
// "type": "text",
// "text": "<thinking>I need to use get_weather, and the user wants SF, which is likely San Francisco, CA.</thinking>"
// },
// {
// "type": "tool_use",
// "id": "toolu_01A09q90qw90lq917835lq9",
// "name": "get_weather",
// "input": {
// "location": "San Francisco, CA",
// "unit": "celsius"
// }
// }
// ]
// },
if (message.ToolCalls?.Count > 0)
{
var content = new List<object>();
if (!string.IsNullOrEmpty(message.Content))
{
content.Add(new
{
type = "text",
text = message.Content
});
}
foreach (var toolCall in message.ToolCalls)
{
content.Add(new
{
type = "tool_use",
id = toolCall.Id,
name = toolCall.Function?.Name,
input = JsonSerializer.Deserialize<Dictionary<string, object>>(
toolCall.Function?.Arguments, ThorJsonSerializer.DefaultOptions)
});
}
list.Add(new
{
role = "assistant",
content
});
}
else
{
list.Add(new
{
role = "assistant",
content = new List<object>
{
new
{
type = "text",
text = message.Content
}
}
});
}
}
else
{
list.Add(new
{
role = message.Role,
content = message.Content
});
}
}
}
}
return list;
}
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("Claudia 对话补全");
if (string.IsNullOrEmpty(options.Endpoint))
{
options.Endpoint = "https://api.anthropic.com/";
}
var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string>
{
{ "x-api-key", options.ApiKey },
{ "anthropic-version", "2023-06-01" }
};
var isThinking = input.Model.EndsWith("thinking");
input.Model = input.Model.Replace("-thinking", string.Empty);
var budgetTokens = 1024;
if (input.MaxTokens is < 2048)
{
input.MaxTokens = 2048;
}
if (input.MaxTokens != null && input.MaxTokens / 2 < 1024)
{
budgetTokens = input.MaxTokens.Value / (4 * 3);
}
// budgetTokens最大4096
budgetTokens = Math.Min(budgetTokens, 4096);
object tool_choice;
if (input.ToolChoice is not null && input.ToolChoice.Type == "auto")
{
tool_choice = new
{
type = "auto",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "any")
{
tool_choice = new
{
type = "any",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool")
{
tool_choice = new
{
type = "tool",
name = input.ToolChoice.Function?.Name,
disable_parallel_tool_use = false,
};
}
else
{
tool_choice = null;
}
var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/v1/messages", new
{
model = input.Model,
max_tokens = input.MaxTokens ?? 2048,
stream = true,
tool_choice,
system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options),
messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options),
top_p = isThinking ? null : input.TopP,
thinking = isThinking
? new
{
type = "enabled",
budget_tokens = budgetTokens,
}
: null,
tools = input.Tools?.Select(x => new
{
name = x.Function?.Name,
description = x.Function?.Description,
input_schema = new
{
type = x.Function?.Parameters?.Type,
required = x.Function?.Parameters?.Required,
properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new
{
description = y.Value.Description,
type = y.Value.Type,
@enum = y.Value.Enum
})
}
}).ToArray(),
temperature = isThinking ? null : input.Temperature
}, string.Empty, headers);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 大于等于400的状态码都认为是异常
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error);
throw new Exception("OpenAI对话异常" + response.StatusCode);
}
using var stream = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken));
using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken));
string? line = string.Empty;
var first = true;
var isThink = false;
string? toolId = null;
string? toolName = null;
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null)
{
line += Environment.NewLine;
if (line.StartsWith('{'))
{
logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode,
line);
throw new Exception("OpenAI对话异常" + line);
}
if (line.StartsWith(OpenAIConstant.Data))
line = line[OpenAIConstant.Data.Length..];
line = line.Trim();
if (string.IsNullOrWhiteSpace(line)) continue;
if (line == OpenAIConstant.Done)
{
break;
}
if (line.StartsWith(':'))
{
continue;
}
if (line.StartsWith("event: ", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var result = JsonSerializer.Deserialize<AnthropicStreamDto>(line,
ThorJsonSerializer.DefaultOptions);
if (result?.Type == "content_block_delta")
{
if (result.Delta.Type is "text" or "text_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new()
{
Message = new ThorChatMessage()
{
Content = result.Delta.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Id = result?.Message?.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = result?.Message?.Usage?.OutputTokens,
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
else if (result.Delta.Type == "input_json_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCalls =
[
new ThorToolCall()
{
Id = toolId,
Function = new ThorChatMessageFunction()
{
Name = toolName,
Arguments = result.Delta.PartialJson
}
}
],
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
else
{
yield return new ThorChatCompletionsResponse()
{
Choices = new List<ThorChatChoiceResponse>()
{
new()
{
Message = new ThorChatMessage()
{
ReasoningContent = result.Delta.Thinking,
Role = "assistant",
}
}
},
Model = input.Model,
Id = result?.Message?.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = result?.Message?.Usage?.OutputTokens,
PromptTokens = result?.Message?.Usage?.InputTokens
}
};
}
continue;
}
if (result?.Type == "content_block_start")
{
if (result?.ContentBlock?.Id is not null)
{
toolId = result.ContentBlock.Id;
}
if (result?.ContentBlock?.Name is not null)
{
toolName = result.ContentBlock.Name;
}
if (toolId is null)
{
continue;
}
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCalls =
[
new ThorToolCall()
{
Id = toolId,
Function = new ThorChatMessageFunction()
{
Name = toolName
}
}
],
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
}
if (result.Type == "content_block_delta")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
ToolCallId = result?.ContentBlock?.Id,
FunctionCall = new ThorChatMessageFunction()
{
Name = result?.ContentBlock?.Name,
Arguments = result?.Delta?.PartialJson
},
Role = "tool",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens
}
};
continue;
}
if (result.Type == "message_start")
{
yield return new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
Content = result?.Delta?.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse()
{
PromptTokens = result?.Message?.Usage?.InputTokens,
}
};
continue;
}
if (result.Type == "message_delta")
{
var deltaOutput = new ThorChatCompletionsResponse()
{
Choices =
[
new ThorChatChoiceResponse()
{
Message = new ThorChatMessage()
{
Content = result.Delta?.Text,
Role = "assistant",
}
}
],
Model = input.Model,
Usage = new ThorUsageResponse
{
InputTokens = result.Usage?.InputTokens + result.Usage?.CacheCreationInputTokens +
result.Usage?.CacheReadInputTokens,
OutputTokens = result.Usage?.OutputTokens,
}
};
deltaOutput.Usage.PromptTokens = deltaOutput.Usage.InputTokens;
deltaOutput.Usage.CompletionTokens = deltaOutput.Usage.OutputTokens;
deltaOutput.Usage.TotalTokens = deltaOutput.Usage.InputTokens + deltaOutput.Usage.OutputTokens;
yield return deltaOutput;
continue;
}
if (result.Message == null)
{
continue;
}
var chat = CreateResponse(result.Message);
var content = chat?.FirstOrDefault()?.Delta;
if (first && string.IsNullOrWhiteSpace(content?.Content) && string.IsNullOrEmpty(content?.ReasoningContent))
{
continue;
}
if (first && content.Content == OpenAIConstant.ThinkStart)
{
isThink = true;
continue;
// 需要将content的内容转换到其他字段
}
if (isThink && content.Content.Contains(OpenAIConstant.ThinkEnd))
{
isThink = false;
// 需要将content的内容转换到其他字段
continue;
}
if (isThink)
{
// 需要将content的内容转换到其他字段
foreach (var choice in chat)
{
choice.Delta.ReasoningContent = choice.Delta.Content;
choice.Delta.Content = string.Empty;
}
}
first = false;
var output = new ThorChatCompletionsResponse()
{
Choices = chat,
Model = input.Model,
Id = result.Message.id,
Usage = new ThorUsageResponse()
{
InputTokens = result.Message.Usage?.InputTokens + result.Message.Usage?.CacheCreationInputTokens +
result.Message.Usage?.CacheReadInputTokens,
OutputTokens = result.Message.Usage?.OutputTokens,
}
};
output.Usage.PromptTokens = output.Usage.InputTokens;
output.Usage.CompletionTokens = output.Usage.OutputTokens;
output.Usage.TotalTokens = output.Usage.InputTokens + output.Usage.OutputTokens;
output.SupplementalMultiplier(AnthropicChatCompletionsService.ClaudeMultiplier);
yield return output;
}
}
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
ThorChatCompletionsRequest input,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("Claudia 对话补全");
if (string.IsNullOrEmpty(options.Endpoint))
{
options.Endpoint = "https://api.anthropic.com/";
}
var client = httpClientFactory.CreateClient();
var headers = new Dictionary<string, string>
{
{ "x-api-key", options.ApiKey },
{ "anthropic-version", "2023-06-01" }
};
bool isThink = input.Model.EndsWith("-thinking");
input.Model = input.Model.Replace("-thinking", string.Empty);
var budgetTokens = 1024;
if (input.MaxTokens is < 2048)
{
input.MaxTokens = 2048;
}
if (input.MaxTokens != null && input.MaxTokens / 2 < 1024)
{
budgetTokens = input.MaxTokens.Value / (4 * 3);
}
object tool_choice;
if (input.ToolChoice is not null && input.ToolChoice.Type == "auto")
{
tool_choice = new
{
type = "auto",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "any")
{
tool_choice = new
{
type = "any",
disable_parallel_tool_use = false,
};
}
else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool")
{
tool_choice = new
{
type = "tool",
name = input.ToolChoice.Function?.Name,
disable_parallel_tool_use = false,
};
}
else
{
tool_choice = null;
}
// budgetTokens最大4096
budgetTokens = Math.Min(budgetTokens, 4096);
var response = await client.PostJsonAsync(options.Endpoint.TrimEnd('/') + "/v1/messages", new
{
model = input.Model,
max_tokens = input.MaxTokens ?? 2000,
system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options),
messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options),
top_p = isThink ? null : input.TopP,
tool_choice,
thinking = isThink
? new
{
type = "enabled",
budget_tokens = budgetTokens,
}
: null,
tools = input.Tools?.Select(x => new
{
name = x.Function?.Name,
description = x.Function?.Description,
input_schema = new
{
type = x.Function?.Parameters?.Type,
required = x.Function?.Parameters?.Required,
properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new
{
description = y.Value.Description,
type = y.Value.Type,
@enum = y.Value.Enum
})
}
}).ToArray(),
temperature = isThink ? null : input.Temperature
}, string.Empty, headers);
openai?.SetTag("Model", input.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
// 大于等于400的状态码都认为是异常
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
options.Endpoint,
response.StatusCode, error);
throw new Exception("OpenAI对话异常" + response.StatusCode.ToString());
}
var value =
await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions,
cancellationToken: cancellationToken);
var thor = new ThorChatCompletionsResponse()
{
Choices = CreateResponse(value),
Model = input.Model,
Id = value.id,
Usage = new ThorUsageResponse()
{
CompletionTokens = value.Usage.OutputTokens,
PromptTokens = value.Usage.InputTokens
}
};
if (value.Usage.CacheReadInputTokens != null)
{
thor.Usage.PromptTokensDetails ??= new ThorUsageResponsePromptTokensDetails()
{
CachedTokens = value.Usage.CacheReadInputTokens.Value,
};
if (value.Usage.InputTokens > 0)
{
thor.Usage.InputTokens = value.Usage.InputTokens;
}
if (value.Usage.OutputTokens > 0)
{
thor.Usage.CompletionTokens = value.Usage.OutputTokens;
thor.Usage.OutputTokens = value.Usage.OutputTokens;
}
}
thor.Usage.TotalTokens = thor.Usage.InputTokens + thor.Usage.OutputTokens;
thor.SupplementalMultiplier(AnthropicChatCompletionsService.ClaudeMultiplier);
return thor;
}
}

View File

@@ -1,5 +1,6 @@
using SqlSugar; using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Domain.Entities; namespace Yi.Framework.AiHub.Domain.Entities;
@@ -38,4 +39,9 @@ public class AiRechargeAggregateRoot : FullAuditedAggregateRoot<Guid>
/// 联系方式 /// 联系方式
/// </summary> /// </summary>
public string? ContactInfo { get; set; } public string? ContactInfo { get; set; }
/// <summary>
/// 订单类型
/// </summary>
public RechargeTypeEnum RechargeType { get; set; }
} }

View File

@@ -0,0 +1,187 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 翻牌任务记录
/// </summary>
[SugarTable("Ai_CardFlipTask")]
[SugarIndex($"index_{nameof(UserId)}_{nameof(WeekStartDate)}",
nameof(UserId), OrderByType.Asc,
nameof(WeekStartDate), OrderByType.Desc)]
public class CardFlipTaskAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public CardFlipTaskAggregateRoot()
{
}
public CardFlipTaskAggregateRoot(Guid userId, DateTime weekStartDate)
{
UserId = userId;
WeekStartDate = weekStartDate.Date; // 确保只存储日期部分
TotalFlips = 0;
FreeFlipsUsed = 0;
BonusFlipsUsed = 0;
InviteFlipsUsed = 0;
IsFirstFlipDone = false;
HasNinthReward = false;
HasTenthReward = false;
FlippedOrder = new List<int>();
}
/// <summary>
/// 用户ID
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 本周开始日期(每周一)
/// </summary>
public DateTime WeekStartDate { get; set; }
/// <summary>
/// 总共已翻牌次数
/// </summary>
public int TotalFlips { get; set; }
/// <summary>
/// 已使用的免费次数最多7次
/// </summary>
public int FreeFlipsUsed { get; set; }
/// <summary>
/// 已使用的赠送次数已废弃保持为0
/// </summary>
public int BonusFlipsUsed { get; set; }
/// <summary>
/// 已使用的邀请解锁次数最多3次
/// </summary>
public int InviteFlipsUsed { get; set; }
/// <summary>
/// 是否已完成首次翻牌(用于判断是否创建任务)
/// </summary>
public bool IsFirstFlipDone { get; set; }
/// <summary>
/// 是否已获得第8次奖励
/// </summary>
public bool HasEighthReward { get; set; }
/// <summary>
/// 第8次奖励金额100-300w
/// </summary>
public long? EighthRewardAmount { get; set; }
/// <summary>
/// 是否已获得第9次奖励
/// </summary>
public bool HasNinthReward { get; set; }
/// <summary>
/// 第9次奖励金额100-500w
/// </summary>
public long? NinthRewardAmount { get; set; }
/// <summary>
/// 是否已获得第10次奖励
/// </summary>
public bool HasTenthReward { get; set; }
/// <summary>
/// 第10次奖励金额100-1000w
/// </summary>
public long? TenthRewardAmount { get; set; }
/// <summary>
/// 备注信息
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
/// <summary>
/// 已翻牌的顺序(存储用户实际翻牌的序号列表,如[3,7,1,5]表示依次翻了3号、7号、1号、5号牌
/// </summary>
[SugarColumn(IsJson = true, IsNullable = true)]
public List<int>? FlippedOrder { get; set; }
/// <summary>
/// 增加翻牌次数
/// </summary>
/// <param name="flipType">翻牌类型</param>
public void IncrementFlip(FlipType flipType)
{
TotalFlips++;
switch (flipType)
{
case FlipType.Free:
FreeFlipsUsed++;
break;
case FlipType.Bonus:
BonusFlipsUsed++;
break;
case FlipType.Invite:
InviteFlipsUsed++;
break;
}
if (!IsFirstFlipDone)
{
IsFirstFlipDone = true;
}
}
/// <summary>
/// 记录第8次奖励
/// </summary>
/// <param name="amount">奖励金额</param>
public void SetEighthReward(long amount)
{
HasEighthReward = true;
EighthRewardAmount = amount;
}
/// <summary>
/// 记录第9次奖励
/// </summary>
/// <param name="amount">奖励金额</param>
public void SetNinthReward(long amount)
{
HasNinthReward = true;
NinthRewardAmount = amount;
}
/// <summary>
/// 记录第10次奖励
/// </summary>
/// <param name="amount">奖励金额</param>
public void SetTenthReward(long amount)
{
HasTenthReward = true;
TenthRewardAmount = amount;
}
}
/// <summary>
/// 翻牌类型枚举
/// </summary>
public enum FlipType
{
/// <summary>
/// 免费翻牌1-7次
/// </summary>
Free = 0,
/// <summary>
/// 赠送翻牌(已废弃)
/// </summary>
Bonus = 1,
/// <summary>
/// 邀请解锁翻牌8-10次
/// </summary>
Invite = 2
}

View File

@@ -24,7 +24,8 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
{ {
UserId = userId; UserId = userId;
SessionId = sessionId; SessionId = sessionId;
Content = content; //如果没有会话,不存储对话内容
Content = sessionId is null ? null : content;
Role = role; Role = role;
ModelId = modelId; ModelId = modelId;
if (tokenUsage is not null) if (tokenUsage is not null)

View File

@@ -0,0 +1,57 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 每日任务奖励领取记录
/// </summary>
[SugarTable("Ai_DailyTaskRewardRecord")]
[SugarIndex($"index_{nameof(UserId)}_{nameof(TaskDate)}",
nameof(UserId), OrderByType.Asc,
nameof(TaskDate), OrderByType.Desc)]
public class DailyTaskRewardRecordAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public DailyTaskRewardRecordAggregateRoot()
{
}
public DailyTaskRewardRecordAggregateRoot(Guid userId, int taskLevel, DateTime taskDate, long rewardTokens)
{
UserId = userId;
TaskLevel = taskLevel;
TaskDate = taskDate.Date; // 确保只存储日期部分
RewardTokens = rewardTokens;
IsRewarded = true;
}
/// <summary>
/// 用户ID
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 任务等级1=1000w任务2=3000w任务
/// </summary>
public int TaskLevel { get; set; }
/// <summary>
/// 任务日期(只包含日期,不包含时间)
/// </summary>
public DateTime TaskDate { get; set; }
/// <summary>
/// 奖励的Token数量
/// </summary>
public long RewardTokens { get; set; }
/// <summary>
/// 是否已发放奖励
/// </summary>
public bool IsRewarded { get; set; }
/// <summary>
/// 备注信息
/// </summary>
public string? Remark { get; set; }
}

View File

@@ -0,0 +1,76 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 邀请记录
/// </summary>
[SugarTable("Ai_InvitationRecord")]
[SugarIndex($"index_{nameof(InviterId)}_{nameof(InvitedUserId)}",
nameof(InviterId), OrderByType.Asc,
nameof(InvitedUserId), OrderByType.Asc)]
[SugarIndex($"index_{nameof(InvitedUserId)}", nameof(InvitedUserId), OrderByType.Asc)]
public class InvitationRecordAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public InvitationRecordAggregateRoot()
{
}
public InvitationRecordAggregateRoot(Guid inviterId, Guid invitedUserId, string inviteCode)
{
InviterId = inviterId;
InvitedUserId = invitedUserId;
InviteCode = inviteCode;
InvitationTime = DateTime.Now;
Status = InvitationStatus.Valid;
}
/// <summary>
/// 邀请人ID
/// </summary>
public Guid InviterId { get; set; }
/// <summary>
/// 被邀请人ID
/// </summary>
public Guid InvitedUserId { get; set; }
/// <summary>
/// 使用的邀请码
/// </summary>
[SugarColumn(Length = 50)]
public string InviteCode { get; set; } = string.Empty;
/// <summary>
/// 邀请时间
/// </summary>
public DateTime InvitationTime { get; set; }
/// <summary>
/// 邀请状态0=有效1=已撤销)
/// </summary>
public InvitationStatus Status { get; set; }
/// <summary>
/// 备注信息
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
/// <summary>
/// 邀请状态枚举
/// </summary>
public enum InvitationStatus
{
/// <summary>
/// 有效
/// </summary>
Valid = 0,
/// <summary>
/// 已撤销
/// </summary>
Revoked = 1
}

View File

@@ -0,0 +1,88 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
namespace Yi.Framework.AiHub.Domain.Entities;
/// <summary>
/// 用户邀请码
/// </summary>
[SugarTable("Ai_InviteCode")]
[SugarIndex($"index_{nameof(UserId)}", nameof(UserId), OrderByType.Asc, true)]
[SugarIndex($"index_{nameof(Code)}", nameof(Code), OrderByType.Asc, true)]
public class InviteCodeAggregateRoot : FullAuditedAggregateRoot<Guid>
{
public InviteCodeAggregateRoot()
{
}
public InviteCodeAggregateRoot(Guid userId, string code)
{
UserId = userId;
Code = code;
IsUsed = false;
IsUserInvited = false;
UsedCount = 0;
}
/// <summary>
/// 用户ID邀请码拥有者
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 邀请码(唯一)
/// </summary>
[SugarColumn(Length = 50)]
public string Code { get; set; } = string.Empty;
/// <summary>
/// 是否已被使用(一个邀请码只能被使用一次)
/// </summary>
public bool IsUsed { get; set; }
/// <summary>
/// 邀请码拥有者是否已被他人邀请(被邀请后不可再提供邀请码)
/// </summary>
public bool IsUserInvited { get; set; }
/// <summary>
/// 被使用次数(统计用)
/// </summary>
public int UsedCount { get; set; }
/// <summary>
/// 使用时间
/// </summary>
public DateTime? UsedTime { get; set; }
/// <summary>
/// 使用人ID
/// </summary>
public Guid? UsedByUserId { get; set; }
/// <summary>
/// 备注信息
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
/// <summary>
/// 标记邀请码已被使用
/// </summary>
/// <param name="usedByUserId">使用者ID</param>
public void MarkAsUsed(Guid usedByUserId)
{
IsUsed = true;
UsedTime = DateTime.Now;
UsedByUserId = usedByUserId;
UsedCount++;
}
/// <summary>
/// 标记用户已被邀请
/// </summary>
public void MarkUserAsInvited()
{
IsUserInvited = true;
}
}

View File

@@ -52,7 +52,12 @@ public class AiModelEntity : Entity<Guid>, IOrderNum, ISoftDelete
public string? ExtraInfo { get; set; } public string? ExtraInfo { get; set; }
/// <summary> /// <summary>
/// 模型类型 /// 模型类型(聊天/图片等)
/// </summary> /// </summary>
public ModelTypeEnum ModelType { get; set; } public ModelTypeEnum ModelType { get; set; }
/// <summary>
/// 模型Api类型现支持同一个模型id多种接口格式
/// </summary>
public ModelApiTypeEnum ModelApiType { get; set; }
} }

View File

@@ -76,23 +76,9 @@ public class PremiumPackageAggregateRoot : FullAuditedAggregateRoot<Guid>
/// <returns>是否消耗成功</returns> /// <returns>是否消耗成功</returns>
public bool ConsumeTokens(long tokenCount) public bool ConsumeTokens(long tokenCount)
{ {
if (RemainingTokens < tokenCount)
{
return false;
}
if (!IsActive)
{
return false;
}
if (ExpireDateTime.HasValue && ExpireDateTime.Value < DateTime.Now)
{
return false;
}
RemainingTokens -= tokenCount; RemainingTokens -= tokenCount;
UsedTokens += tokenCount; UsedTokens += tokenCount;
return true; return true;
} }

View File

@@ -12,11 +12,13 @@ using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.AiGateWay; using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions; using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Shared.Consts;
using Yi.Framework.AiHub.Domain.Shared.Dtos; using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic; using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images; using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.Core.Extensions; using Yi.Framework.Core.Extensions;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
using JsonSerializer = System.Text.Json.JsonSerializer; using JsonSerializer = System.Text.Json.JsonSerializer;
@@ -27,21 +29,24 @@ namespace Yi.Framework.AiHub.Domain.Managers;
public class AiGateWayManager : DomainService public class AiGateWayManager : DomainService
{ {
private readonly ISqlSugarRepository<AiAppAggregateRoot> _aiAppRepository; private readonly ISqlSugarRepository<AiAppAggregateRoot> _aiAppRepository;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly ILogger<AiGateWayManager> _logger; private readonly ILogger<AiGateWayManager> _logger;
private readonly AiMessageManager _aiMessageManager; private readonly AiMessageManager _aiMessageManager;
private readonly UsageStatisticsManager _usageStatisticsManager; private readonly UsageStatisticsManager _usageStatisticsManager;
private readonly ISpecialCompatible _specialCompatible; private readonly ISpecialCompatible _specialCompatible;
private PremiumPackageManager? _premiumPackageManager; private PremiumPackageManager? _premiumPackageManager;
public AiGateWayManager(ISqlSugarRepository<AiAppAggregateRoot> aiAppRepository, ILogger<AiGateWayManager> logger, public AiGateWayManager(ISqlSugarRepository<AiAppAggregateRoot> aiAppRepository, ILogger<AiGateWayManager> logger,
AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager, AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager,
ISpecialCompatible specialCompatible) ISpecialCompatible specialCompatible, ISqlSugarRepository<AiModelEntity> aiModelRepository)
{ {
_aiAppRepository = aiAppRepository; _aiAppRepository = aiAppRepository;
_logger = logger; _logger = logger;
_aiMessageManager = aiMessageManager; _aiMessageManager = aiMessageManager;
_usageStatisticsManager = usageStatisticsManager; _usageStatisticsManager = usageStatisticsManager;
_specialCompatible = specialCompatible; _specialCompatible = specialCompatible;
_aiModelRepository = aiModelRepository;
} }
private PremiumPackageManager PremiumPackageManager => private PremiumPackageManager PremiumPackageManager =>
@@ -50,17 +55,17 @@ public class AiGateWayManager : DomainService
/// <summary> /// <summary>
/// 获取模型 /// 获取模型
/// </summary> /// </summary>
/// <param name="modelApiType"></param>
/// <param name="modelId"></param> /// <param name="modelId"></param>
/// <returns></returns> /// <returns></returns>
private async Task<AiModelDescribe> GetModelAsync(string modelId) private async Task<AiModelDescribe> GetModelAsync(ModelApiTypeEnum modelApiType, string modelId)
{ {
var allApp = await _aiAppRepository._DbQueryable.Includes(x => x.AiModels).ToListAsync(); var aiModelDescribe = await _aiModelRepository._DbQueryable
foreach (var app in allApp) .LeftJoin<AiAppAggregateRoot>((model, app) => model.AiAppId == app.Id)
{ .Where((model, app) => model.ModelId == modelId)
var model = app.AiModels.FirstOrDefault(x => x.ModelId == modelId); .Where((model, app) => model.ModelApiType == modelApiType)
if (model is not null) .Select((model, app) =>
{ new AiModelDescribe
return new AiModelDescribe
{ {
AppId = app.Id, AppId = app.Id,
AppName = app.Name, AppName = app.Name,
@@ -73,11 +78,14 @@ public class AiGateWayManager : DomainService
Description = model.Description, Description = model.Description,
AppExtraUrl = app.ExtraUrl, AppExtraUrl = app.ExtraUrl,
ModelExtraInfo = model.ExtraInfo ModelExtraInfo = model.ExtraInfo
}; })
} .FirstAsync();
if (aiModelDescribe is null)
{
throw new UserFriendlyException($"【{modelId}】模型当前版本【{modelApiType}】格式不支持");
} }
throw new UserFriendlyException($"{modelId}模型当前版本不支持"); return aiModelDescribe;
} }
@@ -92,7 +100,7 @@ public class AiGateWayManager : DomainService
[EnumeratorCancellation] CancellationToken cancellationToken) [EnumeratorCancellation] CancellationToken cancellationToken)
{ {
_specialCompatible.Compatible(request); _specialCompatible.Compatible(request);
var modelDescribe = await GetModelAsync(request.Model); var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model);
var chatService = var chatService =
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName); LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
@@ -122,7 +130,7 @@ public class AiGateWayManager : DomainService
var response = httpContext.Response; var response = httpContext.Response;
// 设置响应头,声明是 json // 设置响应头,声明是 json
//response.ContentType = "application/json; charset=UTF-8"; //response.ContentType = "application/json; charset=UTF-8";
var modelDescribe = await GetModelAsync(request.Model); var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model);
var chatService = var chatService =
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName); LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
var data = await chatService.CompleteChatAsync(modelDescribe, request, cancellationToken); var data = await chatService.CompleteChatAsync(modelDescribe, request, cancellationToken);
@@ -131,7 +139,7 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId, await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = request.Messages?.LastOrDefault().Content ?? string.Empty, Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault().Content ?? string.Empty,
ModelId = request.Model, ModelId = request.Model,
TokenUsage = data.Usage, TokenUsage = data.Usage,
}); });
@@ -139,7 +147,8 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = data.Choices?.FirstOrDefault()?.Delta.Content, Content =
sessionId is null ? "不予存储" : data.Choices?.FirstOrDefault()?.Delta.Content ?? string.Empty,
ModelId = request.Model, ModelId = request.Model,
TokenUsage = data.Usage TokenUsage = data.Usage
}); });
@@ -216,7 +225,7 @@ public class AiGateWayManager : DomainService
{ {
await foreach (var data in completeChatResponse) await foreach (var data in completeChatResponse)
{ {
if (data.Usage is not null) if (data.Usage is not null&&(data.Usage.CompletionTokens>0||data.Usage.OutputTokens>0))
{ {
tokenUsage = data.Usage; tokenUsage = data.Usage;
} }
@@ -263,7 +272,7 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId, await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = request.Messages?.LastOrDefault()?.Content ?? string.Empty, Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty,
ModelId = request.Model, ModelId = request.Model,
TokenUsage = tokenUsage, TokenUsage = tokenUsage,
}); });
@@ -271,12 +280,22 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = backupSystemContent.ToString(), Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(),
ModelId = request.Model, ModelId = request.Model,
TokenUsage = tokenUsage TokenUsage = tokenUsage
}); });
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage); await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
// 扣减尊享token包用量
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
{
var totalTokens = tokenUsage.TotalTokens ?? 0;
if (totalTokens > 0)
{
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
}
}
} }
@@ -297,7 +316,7 @@ public class AiGateWayManager : DomainService
var model = request.Model; var model = request.Model;
if (string.IsNullOrEmpty(model)) model = "dall-e-2"; if (string.IsNullOrEmpty(model)) model = "dall-e-2";
var modelDescribe = await GetModelAsync(model); var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, model);
// 获取渠道指定的实现类型的服务 // 获取渠道指定的实现类型的服务
var imageService = var imageService =
@@ -315,7 +334,7 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId, await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = request.Prompt, Content = sessionId is null ? "不予存储" : request.Prompt,
ModelId = model, ModelId = model,
TokenUsage = response.Usage, TokenUsage = response.Usage,
}); });
@@ -323,12 +342,22 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = response.Results?.FirstOrDefault()?.Url, Content = sessionId is null ? "不予存储" : response.Results?.FirstOrDefault()?.Url,
ModelId = model, ModelId = model,
TokenUsage = response.Usage TokenUsage = response.Usage
}); });
await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage); await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage);
// 扣减尊享token包用量
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
{
var totalTokens = response.Usage.TotalTokens ?? 0;
if (totalTokens > 0)
{
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
}
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -357,7 +386,7 @@ public class AiGateWayManager : DomainService
using var embedding = using var embedding =
Activity.Current?.Source.StartActivity("向量模型调用"); Activity.Current?.Source.StartActivity("向量模型调用");
var modelDescribe = await GetModelAsync(input.Model); var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, input.Model);
// 获取渠道指定的实现类型的服务 // 获取渠道指定的实现类型的服务
var embeddingService = var embeddingService =
@@ -461,7 +490,7 @@ public class AiGateWayManager : DomainService
[EnumeratorCancellation] CancellationToken cancellationToken) [EnumeratorCancellation] CancellationToken cancellationToken)
{ {
_specialCompatible.AnthropicCompatible(request); _specialCompatible.AnthropicCompatible(request);
var modelDescribe = await GetModelAsync(request.Model); var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model);
var chatService = var chatService =
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName); LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
@@ -491,7 +520,7 @@ public class AiGateWayManager : DomainService
var response = httpContext.Response; var response = httpContext.Response;
// 设置响应头,声明是 json // 设置响应头,声明是 json
//response.ContentType = "application/json; charset=UTF-8"; //response.ContentType = "application/json; charset=UTF-8";
var modelDescribe = await GetModelAsync(request.Model); var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model);
var chatService = var chatService =
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName); LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
var data = await chatService.ChatCompletionsAsync(modelDescribe, request, cancellationToken); var data = await chatService.ChatCompletionsAsync(modelDescribe, request, cancellationToken);
@@ -500,7 +529,7 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId, await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = request.Messages?.FirstOrDefault()?.Content ?? string.Empty, Content = sessionId is null ? "不予存储" : request.Messages?.FirstOrDefault()?.Content ?? string.Empty,
ModelId = request.Model, ModelId = request.Model,
TokenUsage = data.TokenUsage, TokenUsage = data.TokenUsage,
}); });
@@ -508,7 +537,7 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId, await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = data.content?.FirstOrDefault()?.text, Content = sessionId is null ? "不予存储" : data.content?.FirstOrDefault()?.text,
ModelId = request.Model, ModelId = request.Model,
TokenUsage = data.TokenUsage TokenUsage = data.TokenUsage
}); });
@@ -516,14 +545,10 @@ public class AiGateWayManager : DomainService
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage); await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage);
// 扣减尊享token包用量 // 扣减尊享token包用量
var totalTokens = (data.TokenUsage?.InputTokens ?? 0) + (data.TokenUsage?.OutputTokens ?? 0); var totalTokens = data.TokenUsage.TotalTokens ?? 0;
if (totalTokens > 0) if (totalTokens > 0)
{ {
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens); await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
if (!consumeSuccess)
{
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败消耗token数: {totalTokens}");
}
} }
} }
@@ -562,10 +587,11 @@ public class AiGateWayManager : DomainService
await foreach (var responseResult in completeChatResponse) await foreach (var responseResult in completeChatResponse)
{ {
//message_start是为了保底机制 //message_start是为了保底机制
if (responseResult.Item1.Contains("message_delta")||responseResult.Item1.Contains("message_start")) if (responseResult.Item1.Contains("message_delta") || responseResult.Item1.Contains("message_start"))
{ {
tokenUsage = responseResult.Item2?.TokenUsage; tokenUsage = responseResult.Item2?.TokenUsage;
} }
backupSystemContent.Append(responseResult.Item2?.Delta?.Text); backupSystemContent.Append(responseResult.Item2?.Delta?.Text);
await WriteAsEventStreamDataAsync(httpContext, responseResult.Item1, responseResult.Item2, await WriteAsEventStreamDataAsync(httpContext, responseResult.Item1, responseResult.Item2,
cancellationToken); cancellationToken);
@@ -576,35 +602,12 @@ public class AiGateWayManager : DomainService
_logger.LogError(e, $"Ai对话异常"); _logger.LogError(e, $"Ai对话异常");
var errorContent = $"对话Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}"; var errorContent = $"对话Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
throw new UserFriendlyException(errorContent); throw new UserFriendlyException(errorContent);
// var model = new AnthropicStreamDto
// {
// Message = new AnthropicChatCompletionDto
// {
// content =
// [
// new AnthropicChatCompletionDtoContent
// {
// text = errorContent,
// }
// ],
// },
// Error = new AnthropicStreamErrorDto
// {
// Type = null,
// Message = errorContent
// }
// };
// var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
// {
// ContractResolver = new CamelCasePropertyNamesContractResolver()
// });
// await response.WriteAsJsonAsync(message, ThorJsonSerializer.DefaultOptions);
} }
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId, await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = request.Messages?.LastOrDefault()?.Content ?? string.Empty, Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty,
ModelId = request.Model, ModelId = request.Model,
TokenUsage = tokenUsage, TokenUsage = tokenUsage,
}); });
@@ -612,7 +615,7 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId, await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
new MessageInputDto new MessageInputDto
{ {
Content = backupSystemContent.ToString(), Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(),
ModelId = request.Model, ModelId = request.Model,
TokenUsage = tokenUsage TokenUsage = tokenUsage
}); });
@@ -622,14 +625,10 @@ public class AiGateWayManager : DomainService
// 扣减尊享token包用量 // 扣减尊享token包用量
if (userId.HasValue && tokenUsage is not null) if (userId.HasValue && tokenUsage is not null)
{ {
var totalTokens = tokenUsage.TotalTokens??0; var totalTokens = tokenUsage.TotalTokens ?? 0;
if (totalTokens > 0) if (tokenUsage.TotalTokens > 0)
{ {
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens); await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
if (!consumeSuccess)
{
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败消耗token数: {totalTokens}");
}
} }
} }
} }

View File

@@ -2,6 +2,7 @@
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.OpenApi; using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers; namespace Yi.Framework.AiHub.Domain.Managers;
@@ -28,7 +29,7 @@ public class AiRechargeManager : DomainService
var currentTime = DateTime.Now; var currentTime = DateTime.Now;
// 查找所有充值记录,按用户分组 // 查找所有充值记录,按用户分组
var allRecharges = await _rechargeRepository._DbQueryable var allRecharges = await _rechargeRepository._DbQueryable.Where(x => x.RechargeType == RechargeTypeEnum.Vip)
.ToListAsync(); .ToListAsync();
if (!allRecharges.Any()) if (!allRecharges.Any())

View File

@@ -0,0 +1,346 @@
using Microsoft.Extensions.Logging;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
/// <summary>
/// 翻牌管理器 - 负责翻牌核心业务逻辑
/// </summary>
public class CardFlipManager : DomainService
{
private readonly ISqlSugarRepository<CardFlipTaskAggregateRoot> _cardFlipTaskRepository;
private readonly ISqlSugarRepository<InvitationRecordAggregateRoot> _invitationRecordRepository;
private readonly InviteCodeManager _inviteCodeManager;
private readonly ILogger<CardFlipManager> _logger;
// 翻牌规则配置
public const int MAX_FREE_FLIPS = 7; // 免费翻牌次数
public const int MAX_INVITE_FLIPS = 3; // 邀请解锁翻牌次数
public const int TOTAL_MAX_FLIPS = 10; // 总最大翻牌次数
private const int EIGHTH_FLIP = 8; // 第8次翻牌
private const int NINTH_FLIP = 9; // 第9次翻牌
private const int TENTH_FLIP = 10; // 第10次翻牌
private const long EIGHTH_MIN_REWARD = 1000000; // 第8次最小奖励 100w
private const long EIGHTH_MAX_REWARD = 3000000; // 第8次最大奖励 300w
private const long NINTH_MIN_REWARD = 1000000; // 第9次最小奖励 100w
private const long NINTH_MAX_REWARD = 5000000; // 第9次最大奖励 500w
private const long TENTH_MIN_REWARD = 1000000; // 第10次最小奖励 100w
private const long TENTH_MAX_REWARD = 10000000; // 第10次最大奖励 1000w
public CardFlipManager(
ISqlSugarRepository<CardFlipTaskAggregateRoot> cardFlipTaskRepository,
ISqlSugarRepository<InvitationRecordAggregateRoot> invitationRecordRepository,
InviteCodeManager inviteCodeManager,
ILogger<CardFlipManager> logger)
{
_cardFlipTaskRepository = cardFlipTaskRepository;
_invitationRecordRepository = invitationRecordRepository;
_inviteCodeManager = inviteCodeManager;
_logger = logger;
}
/// <summary>
/// 获取或创建本周任务
/// </summary>
public async Task<CardFlipTaskAggregateRoot?> GetOrCreateWeeklyTaskAsync(
Guid userId,
DateTime weekStart,
bool createIfNotExists)
{
var task = await _cardFlipTaskRepository._DbQueryable
.Where(x => x.UserId == userId && x.WeekStartDate == weekStart)
.FirstAsync();
if (task == null && createIfNotExists)
{
task = new CardFlipTaskAggregateRoot(userId, weekStart);
await _cardFlipTaskRepository.InsertAsync(task);
}
return task;
}
/// <summary>
/// 获取已翻牌的顺序列表
/// </summary>
public List<int> GetFlippedOrder(CardFlipTaskAggregateRoot task)
{
return task.FlippedOrder ?? new List<int>();
}
/// <summary>
/// 执行翻牌逻辑
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="flipNumber">翻牌序号</param>
/// <param name="weekStart">本周开始日期</param>
/// <returns>翻牌结果</returns>
public async Task<FlipResult> ExecuteFlipAsync(Guid userId, int flipNumber, DateTime weekStart)
{
// 验证翻牌序号
if (flipNumber < 1 || flipNumber > TOTAL_MAX_FLIPS)
{
throw new UserFriendlyException($"翻牌序号必须在1-{TOTAL_MAX_FLIPS}之间");
}
// 获取或创建本周任务
var task = await GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: true);
// 验证翻牌次数
if (task.TotalFlips >= TOTAL_MAX_FLIPS)
{
throw new UserFriendlyException("本周翻牌次数已用完,请下周再来!");
}
// 验证该牌是否已经翻过
var flippedOrder = GetFlippedOrder(task);
if (flippedOrder.Contains(flipNumber))
{
throw new UserFriendlyException($"第 {flipNumber} 号牌已经翻过了!");
}
// 判断翻牌类型
var flipType = DetermineFlipType(task);
// 验证是否有足够的次数
if (!CanUseFlipType(task, flipType))
{
throw new UserFriendlyException(GetFlipTypeErrorMessage(flipType));
}
// 如果是邀请类型翻牌,必须验证用户本周填写的邀请码数量足够
if (flipType == FlipType.Invite)
{
// 查询本周已使用的邀请码数量
var weeklyInviteCodeUsedCount = await _invitationRecordRepository._DbQueryable
.Where(x => x.InvitedUserId == userId)
.Where(x => x.InvitationTime >= weekStart)
.CountAsync();
// 本周填写的邀请码数量必须 >= 即将使用的邀请翻牌次数
// 例如: 要翻第8次(InviteFlipsUsed=0->1), 需要至少填写了1个邀请码
// 要翻第9次(InviteFlipsUsed=1->2), 需要至少填写了2个邀请码
// 要翻第10次(InviteFlipsUsed=2->3), 需要至少填写了3个邀请码
var requiredInviteCodeCount = task.InviteFlipsUsed + 1;
if (weeklyInviteCodeUsedCount < requiredInviteCodeCount)
{
throw new UserFriendlyException($"需本周累积使用{requiredInviteCodeCount}个他人邀请码才能解锁第{task.TotalFlips + 1}次翻牌,您还差一个~");
}
}
// 计算翻牌结果(基于当前是第几次翻牌,而不是卡片序号)
var flipCount = task.TotalFlips + 1; // 当前这次翻牌是第几次
var result = CalculateFlipResult(flipCount);
// 将卡片序号信息也返回
result.FlipNumber = flipNumber;
// 更新翻牌次数(必须在记录奖励之前,因为需要先确定是第几次)
task.IncrementFlip(flipType);
// 记录翻牌顺序
if (task.FlippedOrder == null)
{
task.FlippedOrder = new List<int>();
}
task.FlippedOrder.Add(flipNumber);
// 如果中奖,记录奖励金额(用于后续查询显示)
if (result.IsWin)
{
if (flipCount == EIGHTH_FLIP)
{
task.SetEighthReward(result.RewardAmount);
}
else if (flipCount == NINTH_FLIP)
{
task.SetNinthReward(result.RewardAmount);
}
else if (flipCount == TENTH_FLIP)
{
task.SetTenthReward(result.RewardAmount);
}
}
await _cardFlipTaskRepository.UpdateAsync(task);
_logger.LogInformation($"用户 {userId} 完成第 {flipNumber} 次翻牌,中奖:{result.IsWin}");
return result;
}
/// <summary>
/// 判断是否可以翻牌
/// </summary>
public bool CanFlipCard(CardFlipTaskAggregateRoot? task)
{
if (task == null) return true; // 没有任务记录,可以开始翻牌
return task.TotalFlips < TOTAL_MAX_FLIPS;
}
/// <summary>
/// 判断翻牌类型
/// </summary>
public FlipType DetermineFlipType(CardFlipTaskAggregateRoot task)
{
if (task.FreeFlipsUsed < MAX_FREE_FLIPS)
{
return FlipType.Free;
}
else
{
return FlipType.Invite;
}
}
/// <summary>
/// 判断是否可以使用该翻牌类型
/// </summary>
public bool CanUseFlipType(CardFlipTaskAggregateRoot task, FlipType flipType)
{
return flipType switch
{
FlipType.Free => task.FreeFlipsUsed < MAX_FREE_FLIPS,
FlipType.Invite => task.InviteFlipsUsed < MAX_INVITE_FLIPS,
_ => false
};
}
/// <summary>
/// 计算翻牌结果
/// </summary>
/// <param name="flipCount">第几次翻牌1-10</param>
private FlipResult CalculateFlipResult(int flipCount)
{
var result = new FlipResult
{
FlipNumber = 0, // 稍后会被设置为实际的卡片序号
IsWin = false
};
// 前7次固定失败
if (flipCount <= 7)
{
result.IsWin = false;
result.RewardDesc = "很遗憾,未中奖";
}
// 第8次中奖 (邀请码解锁)
else if (flipCount == EIGHTH_FLIP)
{
var rewardAmount = GenerateRandomReward(EIGHTH_MIN_REWARD, EIGHTH_MAX_REWARD);
result.IsWin = true;
result.RewardAmount = rewardAmount;
result.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000}w tokens!";
}
// 第9次中奖 (邀请码解锁)
else if (flipCount == NINTH_FLIP)
{
var rewardAmount = GenerateRandomReward(NINTH_MIN_REWARD, NINTH_MAX_REWARD);
result.IsWin = true;
result.RewardAmount = rewardAmount;
result.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000}w tokens!";
}
// 第10次中奖 (邀请码解锁)
else if (flipCount == TENTH_FLIP)
{
var rewardAmount = GenerateRandomReward(TENTH_MIN_REWARD, TENTH_MAX_REWARD);
result.IsWin = true;
result.RewardAmount = rewardAmount;
result.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000}w tokens!";
}
return result;
}
/// <summary>
/// 获取翻牌类型错误提示
/// </summary>
private string GetFlipTypeErrorMessage(FlipType flipType)
{
return flipType switch
{
FlipType.Free => "免费翻牌次数已用完",
FlipType.Invite => "需要使用邀请码解锁更多次数",
_ => "无法翻牌"
};
}
/// <summary>
/// 生成随机奖励金额 (最小单位100w)
/// </summary>
private long GenerateRandomReward(long min, long max)
{
var random = new Random();
const long unit = 1000000; // 100w的单位
// 将min和max转换为100w的倍数
long minUnits = min / unit;
long maxUnits = max / unit;
// 在倍数范围内随机
long randomUnits = random.Next((int)minUnits, (int)maxUnits + 1);
// 返回100w的倍数
return randomUnits * unit;
}
/// <summary>
/// 获取本周开始日期(周一)
/// </summary>
public static DateTime GetWeekStartDate(DateTime date)
{
var dayOfWeek = (int)date.DayOfWeek;
// 将周日(0)转换为7
if (dayOfWeek == 0) dayOfWeek = 7;
// 计算本周一的日期
var monday = date.Date.AddDays(-(dayOfWeek - 1));
return monday;
}
/// <summary>
/// 获取翻牌类型描述
/// </summary>
public static string GetFlipTypeDesc(int flipNumber)
{
if (flipNumber <= MAX_FREE_FLIPS)
{
return "免费";
}
else
{
return "邀请解锁";
}
}
}
/// <summary>
/// 翻牌结果
/// </summary>
public class FlipResult
{
/// <summary>
/// 翻牌序号
/// </summary>
public int FlipNumber { get; set; }
/// <summary>
/// 是否中奖
/// </summary>
public bool IsWin { get; set; }
/// <summary>
/// 奖励金额
/// </summary>
public long RewardAmount { get; set; }
/// <summary>
/// 奖励描述
/// </summary>
public string RewardDesc { get; set; } = string.Empty;
}

View File

@@ -19,6 +19,7 @@ public class FuwuhaoManager : DomainService
private IDistributedCache<AccessTokenResponse> _accessTokenCache; private IDistributedCache<AccessTokenResponse> _accessTokenCache;
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository; private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
private readonly ILogger<FuwuhaoManager> _logger; private readonly ILogger<FuwuhaoManager> _logger;
public FuwuhaoManager(IOptions<FuwuhaoOptions> options, IHttpClientFactory httpClientFactory, public FuwuhaoManager(IOptions<FuwuhaoOptions> options, IHttpClientFactory httpClientFactory,
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository, ISqlSugarRepository<AiUserExtraInfoEntity> userRepository,
IDistributedCache<AccessTokenResponse> accessTokenCache, ILogger<FuwuhaoManager> logger) IDistributedCache<AccessTokenResponse> accessTokenCache, ILogger<FuwuhaoManager> logger)
@@ -49,6 +50,11 @@ public class FuwuhaoManager : DomainService
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
}); });
if (result is null || string.IsNullOrEmpty(result.AccessToken))
{
throw new UserFriendlyException("微信服务号AccessToken为空");
}
return result; return result;
}, () => new DistributedCacheEntryOptions() }, () => new DistributedCacheEntryOptions()
{ {
@@ -175,7 +181,8 @@ public class FuwuhaoManager : DomainService
/// <param name="title">图文消息标题</param> /// <param name="title">图文消息标题</param>
/// <param name="description">图文消息描述</param> /// <param name="description">图文消息描述</param>
/// <returns>XML格式的图文消息体</returns> /// <returns>XML格式的图文消息体</returns>
public string BuildRegisterMessage(string toUser, string title="意社区点击一键注册账号", string description="来自意社区SSO统一注册安全中心") public string BuildRegisterMessage(string toUser, string title = "意社区点击一键注册账号",
string description = "来自意社区SSO统一注册安全中心")
{ {
var createTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var createTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var fromUser = _options.FromUser; var fromUser = _options.FromUser;
@@ -207,7 +214,8 @@ public class FuwuhaoManager : DomainService
/// <param name="openId"></param> /// <param name="openId"></param>
/// <param name="bindUserId"></param> /// <param name="bindUserId"></param>
/// <returns></returns> /// <returns></returns>
public async Task<(SceneResultEnum SceneResult,Guid? UserId)> CallBackHandlerAsync(SceneTypeEnum sceneType, string openId, Guid? bindUserId) public async Task<(SceneResultEnum SceneResult, Guid? UserId)> CallBackHandlerAsync(SceneTypeEnum sceneType,
string openId, Guid? bindUserId)
{ {
var aiUserInfo = await _userRepository._DbQueryable.Where(x => x.FuwuhaoOpenId == openId).FirstAsync(); var aiUserInfo = await _userRepository._DbQueryable.Where(x => x.FuwuhaoOpenId == openId).FirstAsync();
switch (sceneType) switch (sceneType)
@@ -216,12 +224,12 @@ public class FuwuhaoManager : DomainService
//有openid说明登录成功 //有openid说明登录成功
if (aiUserInfo is not null) if (aiUserInfo is not null)
{ {
return (SceneResultEnum.Login,aiUserInfo.UserId); return (SceneResultEnum.Login, aiUserInfo.UserId);
} }
//无openid说明需要进行注册 //无openid说明需要进行注册
else else
{ {
return (SceneResultEnum.Register,null); return (SceneResultEnum.Register, null);
} }
break; break;
@@ -240,7 +248,7 @@ public class FuwuhaoManager : DomainService
//说明没有绑定过,直接绑定 //说明没有绑定过,直接绑定
await _userRepository.InsertAsync(new AiUserExtraInfoEntity(bindUserId.Value, openId)); await _userRepository.InsertAsync(new AiUserExtraInfoEntity(bindUserId.Value, openId));
return (SceneResultEnum.Bind,bindUserId); return (SceneResultEnum.Bind, bindUserId);
break; break;
default: default:
throw new ArgumentOutOfRangeException(nameof(sceneType), sceneType, null); throw new ArgumentOutOfRangeException(nameof(sceneType), sceneType, null);

View File

@@ -0,0 +1,224 @@
using Microsoft.Extensions.Logging;
using SqlSugar;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
/// <summary>
/// 邀请码管理器 - 负责邀请码核心业务逻辑
/// </summary>
public class InviteCodeManager : DomainService
{
private readonly ISqlSugarRepository<InviteCodeAggregateRoot> _inviteCodeRepository;
private readonly ISqlSugarRepository<InvitationRecordAggregateRoot> _invitationRecordRepository;
private readonly ILogger<InviteCodeManager> _logger;
public InviteCodeManager(
ISqlSugarRepository<InviteCodeAggregateRoot> inviteCodeRepository,
ISqlSugarRepository<InvitationRecordAggregateRoot> invitationRecordRepository,
ILogger<InviteCodeManager> logger)
{
_inviteCodeRepository = inviteCodeRepository;
_invitationRecordRepository = invitationRecordRepository;
_logger = logger;
}
/// <summary>
/// 生成用户的邀请码
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>邀请码</returns>
public async Task<string> GenerateInviteCodeForUserAsync(Guid userId)
{
// 检查是否已有邀请码
var existingCode = await _inviteCodeRepository._DbQueryable
.Where(x => x.UserId == userId)
.FirstAsync();
if (existingCode != null)
{
return existingCode.Code;
}
// 生成新邀请码
var code = GenerateUniqueInviteCode();
var inviteCode = new InviteCodeAggregateRoot(userId, code);
await _inviteCodeRepository.InsertAsync(inviteCode);
_logger.LogInformation($"用户 {userId} 生成邀请码 {code}");
return code;
}
/// <summary>
/// 获取用户的邀请码信息
/// </summary>
public async Task<InviteCodeAggregateRoot?> GetUserInviteCodeAsync(Guid userId)
{
return await _inviteCodeRepository._DbQueryable
.Where(x => x.UserId == userId)
.FirstAsync();
}
/// <summary>
/// 统计用户本周邀请人数
/// </summary>
public async Task<int> GetWeeklyInvitationCountAsync(Guid userId, DateTime weekStart)
{
return await _invitationRecordRepository._DbQueryable
.Where(x => x.InvitedUserId == userId)
.Where(x => x.InvitationTime >= weekStart)
.CountAsync();
}
/// <summary>
/// 获取邀请历史记录
/// </summary>
public async Task<List<InvitationHistoryDto>> GetInvitationHistoryAsync(Guid userId, int limit = 10)
{
return await _invitationRecordRepository._DbQueryable
.Where(x => x.InviterId == userId)
.OrderBy(x => x.InvitationTime, OrderByType.Desc)
.Take(limit)
.Select(x => new InvitationHistoryDto
{
InvitedUserName = "用户***", // 脱敏处理
InvitationTime = x.InvitationTime
})
.ToListAsync();
}
/// <summary>
/// 使用邀请码
/// </summary>
/// <param name="userId">使用者ID</param>
/// <param name="inviteCode">邀请码</param>
/// <returns>邀请人ID</returns>
public async Task<Guid> UseInviteCodeAsync(Guid userId, string inviteCode)
{
if (string.IsNullOrWhiteSpace(inviteCode))
{
throw new UserFriendlyException("邀请码不能为空");
}
// 查找邀请码
var inviteCodeEntity = await _inviteCodeRepository._DbQueryable
.Where(x => x.Code == inviteCode)
.FirstAsync();
if (inviteCodeEntity == null)
{
throw new UserFriendlyException("邀请码不存在");
}
// 验证不能使用自己的邀请码
if (inviteCodeEntity.UserId == userId)
{
throw new UserFriendlyException("不能使用自己的邀请码");
}
// 验证邀请码是否已被使用
if (inviteCodeEntity.IsUsed)
{
throw new UserFriendlyException("该邀请码已被使用");
}
// 验证邀请码拥有者是否已被邀请
if (inviteCodeEntity.IsUserInvited)
{
throw new UserFriendlyException("该用户已被邀请,邀请码无效");
}
// 验证本周邀请码使用次数
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
var weeklyUseCount = await _invitationRecordRepository._DbQueryable
.Where(x => x.InvitedUserId == userId)
.Where(x => x.InvitationTime >= weekStart)
.CountAsync();
if (weeklyUseCount >= CardFlipManager.MAX_INVITE_FLIPS)
{
throw new UserFriendlyException($"本周邀请码使用次数已达上限({CardFlipManager.MAX_INVITE_FLIPS}次),请下周再来");
}
// 检查当前用户的邀请码信息
var myInviteCode = await _inviteCodeRepository._DbQueryable
.Where(x => x.UserId == userId)
.FirstAsync();
// 标记邀请码为已使用
inviteCodeEntity.MarkAsUsed(userId);
await _inviteCodeRepository.UpdateAsync(inviteCodeEntity);
// 标记当前用户已被邀请(仅第一次使用邀请码时标记)
if (myInviteCode == null)
{
myInviteCode = new InviteCodeAggregateRoot(userId, GenerateUniqueInviteCode());
myInviteCode.MarkUserAsInvited();
await _inviteCodeRepository.InsertAsync(myInviteCode);
}
else if (!myInviteCode.IsUserInvited)
{
myInviteCode.MarkUserAsInvited();
await _inviteCodeRepository.UpdateAsync(myInviteCode);
}
// 创建邀请记录
var invitationRecord = new InvitationRecordAggregateRoot(
inviteCodeEntity.UserId,
userId,
inviteCode);
await _invitationRecordRepository.InsertAsync(invitationRecord);
_logger.LogInformation($"用户 {userId} 使用邀请码 {inviteCode} 成功");
return inviteCodeEntity.UserId;
}
/// <summary>
/// 检查用户是否已被邀请
/// </summary>
public async Task<bool> IsUserInvitedAsync(Guid userId)
{
var inviteCode = await _inviteCodeRepository._DbQueryable
.Where(x => x.UserId == userId)
.FirstAsync();
return inviteCode?.IsUserInvited ?? false;
}
/// <summary>
/// 生成唯一邀请码
/// </summary>
private string GenerateUniqueInviteCode()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var code = new char[8];
for (int i = 0; i < code.Length; i++)
{
code[i] = chars[random.Next(chars.Length)];
}
return new string(code);
}
}
/// <summary>
/// 邀请历史记录DTO
/// </summary>
public class InvitationHistoryDto
{
/// <summary>
/// 被邀请人名称(脱敏)
/// </summary>
public string InvitedUserName { get; set; } = string.Empty;
/// <summary>
/// 邀请时间
/// </summary>
public DateTime InvitationTime { get; set; }
}

View File

@@ -13,13 +13,14 @@ public class PremiumPackageManager : DomainService
{ {
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> _premiumPackageRepository; private readonly ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> _premiumPackageRepository;
private readonly ILogger<PremiumPackageManager> _logger; private readonly ILogger<PremiumPackageManager> _logger;
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository;
public PremiumPackageManager( public PremiumPackageManager(
ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> premiumPackageRepository, ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> premiumPackageRepository,
ILogger<PremiumPackageManager> logger) ILogger<PremiumPackageManager> logger, ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository)
{ {
_premiumPackageRepository = premiumPackageRepository; _premiumPackageRepository = premiumPackageRepository;
_logger = logger; _logger = logger;
_rechargeRepository = rechargeRepository;
} }
/// <summary> /// <summary>
@@ -57,6 +58,21 @@ public class PremiumPackageManager : DomainService
await _premiumPackageRepository.InsertAsync(premiumPackage); await _premiumPackageRepository.InsertAsync(premiumPackage);
// 创建充值记录
var rechargeRecord = new AiRechargeAggregateRoot
{
UserId = userId,
RechargeAmount = totalAmount,
Content = packageName,
ExpireDateTime = premiumPackage.ExpireDateTime,
Remark = "自助充值",
ContactInfo = null,
RechargeType = RechargeTypeEnum.PremiumPackage
};
// 保存充值记录到数据库
await _rechargeRepository.InsertAsync(rechargeRecord);
_logger.LogInformation( _logger.LogInformation(
$"用户 {userId} 购买尊享包成功: {packageName}, Token数量: {tokenAmount}, 金额: {totalAmount}"); $"用户 {userId} 购买尊享包成功: {packageName}, Token数量: {tokenAmount}, 金额: {totalAmount}");
@@ -69,12 +85,12 @@ public class PremiumPackageManager : DomainService
/// <param name="userId">用户ID</param> /// <param name="userId">用户ID</param>
/// <param name="tokenCount">需要消耗的Token数量</param> /// <param name="tokenCount">需要消耗的Token数量</param>
/// <returns>是否消耗成功</returns> /// <returns>是否消耗成功</returns>
public async Task<bool> ConsumeTokensAsync(Guid userId, long tokenCount) public async Task<bool> TryConsumeTokensAsync(Guid userId, long tokenCount)
{ {
// 获取用户所有可用的尊享包按剩余token升序排列优先消耗快用完的 // 获取用户所有可用的尊享包按剩余token升序排列优先消耗快用完的
var availablePackages = await _premiumPackageRepository._DbQueryable var availablePackages = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0) .Where(x => x.UserId == userId && x.IsActive)
.OrderBy(x => x.RemainingTokens) .OrderBy(x => x.CreationTime)
.ToListAsync(); .ToListAsync();
if (!availablePackages.Any()) if (!availablePackages.Any())
@@ -94,34 +110,12 @@ public class PremiumPackageManager : DomainService
return false; return false;
} }
// 计算总可用Token var firstPackage = validPackages.First();
var totalAvailableTokens = validPackages.Sum(p => p.RemainingTokens); // 直接扣除最早的token包需要消耗的token允许扣减到负数
if (totalAvailableTokens < tokenCount) firstPackage.ConsumeTokens(tokenCount);
{ await _premiumPackageRepository.UpdateAsync(firstPackage);
_logger.LogWarning(
$"用户 {userId} 尊享包Token不足需要: {tokenCount}, 可用: {totalAvailableTokens}");
return false;
}
// 从可用的包中逐个扣除Token return true;
var remainingToConsume = tokenCount;
foreach (var package in validPackages)
{
if (remainingToConsume <= 0)
break;
var toConsume = Math.Min(remainingToConsume, package.RemainingTokens);
if (package.ConsumeTokens(toConsume))
{
await _premiumPackageRepository.UpdateAsync(package);
remainingToConsume -= toConsume;
_logger.LogInformation(
$"用户 {userId} 从尊享包 {package.Id} 消耗 {toConsume} tokens, 剩余: {package.RemainingTokens}");
}
}
return remainingToConsume == 0;
} }
/// <summary> /// <summary>
@@ -130,55 +124,11 @@ public class PremiumPackageManager : DomainService
/// <param name="userId">用户ID</param> /// <param name="userId">用户ID</param>
/// <returns>可用Token总数</returns> /// <returns>可用Token总数</returns>
public async Task<long> GetAvailableTokensAsync(Guid userId) public async Task<long> GetAvailableTokensAsync(Guid userId)
{
var packages = await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
.ToListAsync();
return packages
.Where(p => p.IsAvailable())
.Sum(p => p.RemainingTokens);
}
/// <summary>
/// 获取用户的所有尊享包
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>尊享包列表</returns>
public async Task<List<PremiumPackageAggregateRoot>> GetUserPremiumPackagesAsync(Guid userId)
{ {
return await _premiumPackageRepository._DbQueryable return await _premiumPackageRepository._DbQueryable
.Where(x => x.UserId == userId) .Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
.OrderByDescending(x => x.CreationTime) .Where(p => p.IsActive)
.ToListAsync(); .Where(p => !p.ExpireDateTime.HasValue || p.ExpireDateTime.Value >= DateTime.Now)
} .SumAsync(p => p.RemainingTokens);
/// <summary>
/// 停用过期的尊享包
/// </summary>
/// <returns>停用的包数量</returns>
public async Task<int> DeactivateExpiredPackagesAsync()
{
_logger.LogInformation("开始执行尊享包过期自动停用任务");
var now = DateTime.Now;
var expiredPackages = await _premiumPackageRepository._DbQueryable
.Where(x => x.IsActive && x.ExpireDateTime.HasValue && x.ExpireDateTime.Value < now)
.ToListAsync();
if (!expiredPackages.Any())
{
_logger.LogInformation("没有找到过期的尊享包");
return 0;
}
foreach (var package in expiredPackages)
{
package.Deactivate();
await _premiumPackageRepository.UpdateAsync(package);
}
_logger.LogInformation($"成功停用 {expiredPackages.Count} 个过期的尊享包");
return expiredPackages.Count;
} }
} }

View File

@@ -42,7 +42,8 @@ namespace Yi.Framework.AiHub.Domain
nameof(DeepSeekChatCompletionsService)); nameof(DeepSeekChatCompletionsService));
services.AddKeyedTransient<IChatCompletionService, OpenAiChatCompletionsService>( services.AddKeyedTransient<IChatCompletionService, OpenAiChatCompletionsService>(
nameof(OpenAiChatCompletionsService)); nameof(OpenAiChatCompletionsService));
services.AddKeyedTransient<IChatCompletionService, ClaudiaChatCompletionsService>(
nameof(ClaudiaChatCompletionsService));
#endregion #endregion
#region Anthropic ChatCompletion #region Anthropic ChatCompletion
@@ -127,6 +128,7 @@ namespace Yi.Framework.AiHub.Domain
{ {
builder.ConfigureHttpClient(client => builder.ConfigureHttpClient(client =>
{ {
client.DefaultRequestHeaders.Add("User-Agent","Apifox/1.0.0 (https://apifox.com)");
client.Timeout = TimeSpan.FromMinutes(10); client.Timeout = TimeSpan.FromMinutes(10);
}); });
}); });

View File

@@ -15,7 +15,8 @@ namespace Yi.Framework.Bbs.Domain.Entities.Integral
[SugarTable("SignIn")] [SugarTable("SignIn")]
[SugarIndex($"index_{nameof(CreatorId)}", nameof(CreatorId), OrderByType.Asc)] [SugarIndex($"index_{nameof(CreatorId)}", nameof(CreatorId), OrderByType.Asc)]
public class SignInAggregateRoot : AggregateRoot<Guid>, ICreationAuditedObject public class
SignInAggregateRoot : AggregateRoot<Guid>, ICreationAuditedObject
{ {
[SugarColumn(IsPrimaryKey = true)] [SugarColumn(IsPrimaryKey = true)]

View File

@@ -1,3 +1,4 @@
using System.Text.Json;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
using Yi.Abp.Web; using Yi.Abp.Web;

View File

@@ -29,6 +29,7 @@ using Volo.Abp.Swashbuckle;
using Yi.Abp.Application; using Yi.Abp.Application;
using Yi.Abp.SqlsugarCore; using Yi.Abp.SqlsugarCore;
using Yi.Framework.AiHub.Application; using Yi.Framework.AiHub.Application;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AspNetCore; using Yi.Framework.AspNetCore;
using Yi.Framework.AspNetCore.Authentication.OAuth; using Yi.Framework.AspNetCore.Authentication.OAuth;
using Yi.Framework.AspNetCore.Authentication.OAuth.Gitee; using Yi.Framework.AspNetCore.Authentication.OAuth.Gitee;
@@ -46,6 +47,7 @@ using Yi.Framework.Rbac.Application;
using Yi.Framework.Rbac.Domain.Authorization; using Yi.Framework.Rbac.Domain.Authorization;
using Yi.Framework.Rbac.Domain.Shared.Consts; using Yi.Framework.Rbac.Domain.Shared.Consts;
using Yi.Framework.Rbac.Domain.Shared.Options; using Yi.Framework.Rbac.Domain.Shared.Options;
using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.Stock.Application; using Yi.Framework.Stock.Application;
using Yi.Framework.TenantManagement.Application; using Yi.Framework.TenantManagement.Application;
@@ -350,6 +352,10 @@ namespace Yi.Abp.Web
var app = context.GetApplicationBuilder(); var app = context.GetApplicationBuilder();
app.UseRouting(); app.UseRouting();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<CardFlipTaskAggregateRoot>();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<InvitationRecordAggregateRoot>();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<InviteCodeAggregateRoot>();
//跨域 //跨域
app.UseCors(DefaultCorsPolicyName); app.UseCors(DefaultCorsPolicyName);

View File

@@ -54,6 +54,13 @@ pnpm lint:stylelint # 样式格式化
pnpm cz # 规范提交自动执行lint pnpm cz # 规范提交自动执行lint
``` ```
### 服务端启动
目录E:\devDemo\Yi\Yi.Abp.Net8\src\Yi.Abp.Web
```bash
dotnet run
```
## 🧸 即将推出 (含 接口联调) ## 🧸 即将推出 (含 接口联调)
- [x] 会话管理 - [x] 会话管理
- [x] 发送消息 - [x] 发送消息

View File

@@ -6,9 +6,9 @@
<link rel="icon" href="/favicon.ico"/> <link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="baidu-site-verification" content="codeva-mkVpSFmYJm"/> <meta name="baidu-site-verification" content="codeva-mkVpSFmYJm"/>
<meta name="description" content="意心AI一站式多模型 AI 平台,提供 GPT-4o、DeepSeek 等服务"/> <meta name="description" content="意心AI一站式多模型 AI 平台,提供 AI 服务"/>
<meta name="description" content="各大主流AI无限制使用直连AIclaude ,DeepSeek,open-ai"/> <meta name="description" content="各大主流AI无限制使用直连AIclaude ,DeepSeek,open-ai"/>
<meta name="keywords" content="意心AI, GPT-4.5, 多模型AI, AI工具"/> <meta name="keywords" content="意心AI, 多模型AI, AI工具"/>
<meta name="keywords" content="橙子chengzi,橙子老哥ccnetcore意社区"/> <meta name="keywords" content="橙子chengzi,橙子老哥ccnetcore意社区"/>
<meta name="author" content="橙子chengzi,橙子老哥ccnetcore"/> <meta name="author" content="橙子chengzi,橙子老哥ccnetcore"/>
<meta name="version" content="%VITE_APP_VERSION%"/> <meta name="version" content="%VITE_APP_VERSION%"/>
@@ -112,7 +112,7 @@
<body> <body>
<!-- 加载动画容器 --> <!-- 加载动画容器 -->
<div id="yixinai-loader" class="loader-container"> <div id="yixinai-loader" class="loader-container">
<div class="loader-title">意心Ai</div> <div class="loader-title">意心Ai 2.2</div>
<div class="loader-subtitle">海外地址仅首次访问预计加载约10秒</div> <div class="loader-subtitle">海外地址仅首次访问预计加载约10秒</div>
<div class="loader-logo"> <div class="loader-logo">
<div class="pulse-box"></div> <div class="pulse-box"></div>

View File

@@ -0,0 +1,33 @@
import { get, post } from '@/utils/request';
import type {
CardFlipStatusOutput,
FlipCardInput,
FlipCardOutput,
UseInviteCodeInput,
InviteCodeOutput
} from './types';
// 获取本周翻牌任务状态
export function getWeeklyTaskStatus() {
return get<CardFlipStatusOutput>('/card-flip/weekly-task-status').json();
}
// 翻牌
export function flipCard(data: FlipCardInput) {
return post<FlipCardOutput>('/card-flip/flip-card', data).json();
}
// 使用邀请码解锁翻牌次数
export function useInviteCode(data: UseInviteCodeInput) {
return post<void>('/card-flip/use-invite-code', data).json();
}
// 获取我的邀请码信息
export function getMyInviteCode() {
return get<InviteCodeOutput>('/card-flip/my-invite-code').json();
}
// 生成我的邀请码(如果没有)
export function generateMyInviteCode() {
return post<string>('/card-flip/generate-my-invite-code').json();
}

View File

@@ -0,0 +1,58 @@
// 翻牌任务状态输出
export interface CardFlipStatusOutput {
totalFlips: number; // 本周总翻牌次数
remainingFreeFlips: number; // 剩余免费次数
remainingBonusFlips: number; // 剩余赠送次数
remainingInviteFlips: number; // 剩余邀请解锁次数
canFlip: boolean; // 是否可以翻牌
myInviteCode?: string; // 用户的邀请码
invitedCount: number; // 本周邀请人数
isInvited: boolean; // 是否已被邀请
flipRecords: CardFlipRecord[]; // 翻牌记录
nextFlipTip?: string; // 下次可翻牌提示
}
// 翻牌记录
export interface CardFlipRecord {
flipNumber: number; // 翻牌序号1-10
isFlipped: boolean; // 是否已翻
isWin: boolean; // 是否中奖
rewardAmount?: number; // 奖励金额token数
flipTypeDesc?: string; // 翻牌类型描述
flipOrderIndex: number; // 在翻牌顺序中的位置1-10表示第几个翻
}
// 翻牌输入
export interface FlipCardInput {
flipNumber: number; // 翻牌序号1-10
}
// 翻牌输出
export interface FlipCardOutput {
flipNumber: number; // 翻牌序号1-10
isWin: boolean; // 是否中奖
rewardAmount?: number; // 奖励金额token数
rewardDesc?: string; // 奖励描述
showDoubleRewardTip: boolean; // 是否显示翻倍包提示
remainingFlips: number; // 剩余可翻次数
}
// 使用邀请码输入
export interface UseInviteCodeInput {
inviteCode: string; // 邀请码
}
// 邀请码信息输出
export interface InviteCodeOutput {
myInviteCode?: string; // 我的邀请码
invitedCount: number; // 本周邀请人数
isInvited: boolean; // 是否已被邀请
invitationHistory: InvitationHistoryItem[]; // 邀请历史记录
}
// 邀请历史记录项
export interface InvitationHistoryItem {
invitedUserName: string; // 被邀请人昵称(脱敏)
invitationTime: string; // 邀请时间
weekDescription: string; // 本周所在
}

View File

@@ -0,0 +1,12 @@
import { get, post } from '@/utils/request';
import type { DailyTaskStatusOutput, ClaimTaskRewardInput } from './types';
// 获取今日任务状态
export function getTodayTaskStatus() {
return get<DailyTaskStatusOutput>('/daily-task/today-task-status').json();
}
// 领取任务奖励
export function claimTaskReward(data: ClaimTaskRewardInput) {
return post<void>('/daily-task/claim-task-reward', data).json();
}

View File

@@ -0,0 +1,21 @@
// 每日任务状态
export interface DailyTaskStatusOutput {
todayConsumedTokens: number; // 今日消耗的尊享包Token数
tasks: DailyTaskItem[]; // 任务列表
}
// 每日任务项
export interface DailyTaskItem {
level: number; // 任务等级1=1000w任务2=3000w任务
name: string; // 任务名称
description: string; // 任务描述
requiredTokens: number; // 任务要求的Token消耗量
rewardTokens: number; // 奖励的Token数量
status: number; // 任务状态0=未完成1=可领取2=已领取
progress: number; // 任务进度百分比0-100
}
// 领取任务奖励输入
export interface ClaimTaskRewardInput {
taskLevel: number; // 任务等级1=1000w任务2=3000w任务
}

View File

@@ -1,5 +1,29 @@
import { get, post } from '@/utils/request.ts'; import { get, post } from '@/utils/request.ts';
// 商品分类类型
export enum GoodsCategoryType {
Vip = 'Vip',
PremiumPackage = 'PremiumPackage',
}
// 商品信息接口
export interface GoodsItem {
goodsName: string; // 商品名称
originalPrice: number; // 原价
referencePrice: number; // 参考价格(月均价)
goodsPrice: number; // 实际价格
discountAmount: number | null; // 折扣金额
goodsCategory: string; // 商品分类
remark: string | null; // 备注(标签)
discountDescription: string | null; // 折扣描述
goodsType: string | null; // 折扣描述
}
// 获取商品列表
export function getGoodsList(categoryType: GoodsCategoryType) {
return get<GoodsItem[]>(`/pay/GoodsList?GoodsCategoryType=${categoryType}`).json();
}
// 创建订单并发起支付 // 创建订单并发起支付
export function createOrder(params: any) { export function createOrder(params: any) {
return post<any>(`/pay/Order`, params).json(); return post<any>(`/pay/Order`, params).json();

View File

@@ -19,3 +19,8 @@ export function getQrCodeResult(data: any) {
export function getWechatAuth(data: any) { export function getWechatAuth(data: any) {
return post<any>('/fuwuhao/register', data).json(); return post<any>('/fuwuhao/register', data).json();
} }
// 获取尊享服务Token包额度
export function getPremiumTokenPackage() {
return get<any>('/usage-statistics/premium-token-usage').json();
}

View File

@@ -77,7 +77,6 @@ async function onReLogin() {
} }
function handleThirdPartyLogin(type: any) { function handleThirdPartyLogin(type: any) {
const redirectUri = encodeURIComponent(`${window.location.origin}/chat`); const redirectUri = encodeURIComponent(`${window.location.origin}/chat`);
console.log('cccc', type);
const popup = window.open( const popup = window.open(
`${SSO_SEVER_URL}/login?client_id=${type}&redirect_uri=${redirectUri}`, `${SSO_SEVER_URL}/login?client_id=${type}&redirect_uri=${redirectUri}`,
'SSOLogin', 'SSOLogin',
@@ -149,7 +148,6 @@ function handleLoginAgainYi() {
&& event.data.type === 'SSO_LOGIN_SUCCESS' && event.data.type === 'SSO_LOGIN_SUCCESS'
&& !isHandled) { && !isHandled) {
isHandled = true; isHandled = true;
console.log('111');
try { try {
// 清理监听 // 清理监听
window.removeEventListener('message', messageHandler); window.removeEventListener('message', messageHandler);
@@ -362,7 +360,20 @@ function openContact() {
联系我们 联系我们
</el-button> </el-button>
</div> </div>
<div>
<a
href="https://ccnetcore.com/login"
target="_blank"
class="mt5 flex items-center gap-2 group"
style="color: #101d2c;"
title="点击跳转YiXinAI玩法指南专栏"
>
<span class="pc-text">前往 意社区 👉</span>
</a>
</div> </div>
</div>
<div v-if="loginFormType === 'RegistrationForm'" class="form-container"> <div v-if="loginFormType === 'RegistrationForm'" class="form-container">
<span class="content-title"> 登录后免费使用完整功能 </span> <span class="content-title"> 登录后免费使用完整功能 </span>

View File

@@ -57,7 +57,6 @@ async function showPopover() {
// 点击 // 点击
// 处理模型点击 // 处理模型点击
function handleModelClick(item: GetSessionListVO) { function handleModelClick(item: GetSessionListVO) {
console.log('modelStore.modelList', modelStore.modelList);
if (!isModelAvailable(item)) { if (!isModelAvailable(item)) {
ElMessageBox.confirm( ElMessageBox.confirm(
` `
@@ -103,6 +102,57 @@ function handleModelClick(item: GetSessionListVO) {
modelStore.setCurrentModelInfo(item); modelStore.setCurrentModelInfo(item);
popoverRef.value?.hide?.(); popoverRef.value?.hide?.();
} }
/* -------------------------------
模型样式规则
规则1普通灰色免费模型
规则2金色光泽VIP/付费)
规则3彩色流光尊享/高级)
-------------------------------- */
function getModelStyleClass(modelName: any) {
const name = modelName.toLowerCase();
// 规则3彩色流光
if (name.includes('claude-sonnet-4-5-20250929')) {
return `
text-transparent bg-clip-text
bg-[linear-gradient(45deg,#ff0000,#ff8000,#ffff00,#00ff00,#00ffff,#0000ff,#8000ff,#ff0080)]
bg-[length:400%_400%] animate-gradientFlow
`;
}
// 规则2普通灰
if (name.includes('deepseek-r1')) {
return 'text-gray-700';
}
// 规则1金色光泽
return `
text-[#B38728] font-semibold relative overflow-hidden
before:content-[''] before:absolute before:-inset-2 before:-z-10
before:animate-goldShine
`;
// 金色背景
// before:bg-[linear-gradient(135deg,#BF953F,#FCF6BA,#B38728,#FBF5B7,#AA771C)]
}
/* -------------------------------
外层卡片样式(选中态 + hover 动效)
-------------------------------- */
function getWrapperClass(item: GetSessionListVO) {
const isSelected = item.modelName === currentModelName.value;
const available = isModelAvailable(item);
return [
'p-2 rounded-md text-sm transition-all duration-300 relative select-none flex items-center justify-between',
available
? 'hover:scale-[1.03] hover:shadow-[0_0_8px_rgba(0,0,0,0.1)] hover:border-gray-300'
: 'opacity-60 cursor-not-allowed',
isSelected
? 'border-2 border-blue-700 shadow-[0_0_10px_rgba(29,78,216,1)]'
: 'border border-transparent cursor-pointer',
];
}
</script> </script>
<template> <template>
@@ -119,12 +169,12 @@ function handleModelClick(item: GetSessionListVO) {
<!-- 触发元素插槽 --> <!-- 触发元素插槽 -->
<template #trigger> <template #trigger>
<div <div
class="model-select-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()]" class="model-select-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()] leading-snug"
> >
<div class="model-select-box-icon"> <div class="model-select-box-icon">
<SvgIcon name="models" size="12" /> <SvgIcon name="models" size="12" />
</div> </div>
<div class="model-select-box-text font-size-12px"> <div :class="getModelStyleClass(currentModelName)" class="model-select-box-text font-size-12px">
{{ currentModelName }} {{ currentModelName }}
</div> </div>
</div> </div>
@@ -134,7 +184,8 @@ function handleModelClick(item: GetSessionListVO) {
<div <div
v-for="item in popoverList" v-for="item in popoverList"
:key="item.id" :key="item.id"
class="popover-content-box-items w-full rounded-8px select-none transition-all transition-duration-300 flex items-center hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]" :class="getWrapperClass(item)"
@click="handleModelClick(item)"
> >
<Popover <Popover
trigger-class="popover-trigger-item-text" trigger-class="popover-trigger-item-text"
@@ -144,23 +195,9 @@ function handleModelClick(item: GetSessionListVO) {
:offset="[12, 0]" :offset="[12, 0]"
> >
<template #trigger> <template #trigger>
<div <span :class="getModelStyleClass(item.modelName)">
class="popover-content-box-item p-4px font-size-12px text-overflow line-height-16px relative"
:class="[
{ 'bg-[rgba(0,0,0,.04)] is-select': item.modelName === currentModelName },
{ 'cursor-not-allowed opacity-60': !isModelAvailable(item) },
]"
@click="handleModelClick(item)"
>
{{ item.modelName }} {{ item.modelName }}
<!-- VIP锁定图标 --> </span>
<el-icon
v-if="!isModelAvailable(item)"
class="absolute right-1 top-1/2 transform -translate-y-1/2"
>
<Lock />
</el-icon>
</div>
</template> </template>
<div <div
class="popover-content-box-item-text text-wrap max-w-200px rounded-lg p-8px font-size-12px line-height-tight" class="popover-content-box-item-text text-wrap max-w-200px rounded-lg p-8px font-size-12px line-height-tight"
@@ -168,6 +205,14 @@ function handleModelClick(item: GetSessionListVO) {
{{ item.remark }} {{ item.remark }}
</div> </div>
</Popover> </Popover>
<!-- VIP锁定图标 -->
<el-icon
v-if="!isModelAvailable(item)"
class="absolute right-1 top-1/2 transform -translate-y-1/2"
>
<Lock />
</el-icon>
</div> </div>
</div> </div>
</Popover> </Popover>
@@ -181,23 +226,18 @@ function handleModelClick(item: GetSessionListVO) {
border: 1px solid var(--el-color-primary, #409eff); border: 1px solid var(--el-color-primary, #409eff);
border-radius: 10px; border-radius: 10px;
} }
.popover-content-box-item.is-select {
font-weight: 700;
color: var(--el-color-primary, #409eff);
}
.popover-content-box { .popover-content-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
height: 200px; height: 300px;
overflow: hidden auto; overflow: hidden auto;
.popover-content-box-items {
:deep() { :deep(.popover-trigger-item-text) {
.popover-trigger-item-text {
width: 100%; width: 100%;
} }
}
}
.popover-content-box-item-text { .popover-content-box-item-text {
color: white; color: white;
background-color: black; background-color: black;
@@ -215,4 +255,32 @@ function handleModelClick(item: GetSessionListVO) {
border-radius: 4px; border-radius: 4px;
} }
} }
/* 彩色流光动画 */
@keyframes gradientFlow {
0%, 100% { background-position: 0 50%; }
50% { background-position: 100% 50%; }
}
/* 金色光泽动画 */
@keyframes goldShine {
0% { transform: translateX(-100%) translateY(-100%); }
100% { transform: translateX(100%) translateY(100%); }
}
/* 柔光 hover 动效 */
@keyframes glowPulse {
0%, 100% { box-shadow: 0 0 6px rgba(37,99,235,0.2); }
50% { box-shadow: 0 0 10px rgba(37,99,235,0.5); }
}
.animate-gradientFlow {
animation: gradientFlow 3s ease infinite;
}
.animate-goldShine {
animation: goldShine 4s linear infinite;
}
.animate-glowPulse {
animation: glowPulse 2s ease-in-out infinite;
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { FullScreen } from '@element-plus/icons-vue';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
interface NavItem { interface NavItem {
@@ -17,7 +18,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
title: '弹窗标题', title: '弹窗标题',
width: '1000px', width: '75%',
defaultActive: '', defaultActive: '',
}); });
@@ -25,9 +26,13 @@ const emit = defineEmits(['update:modelValue', 'confirm', 'close', 'nav-change']
const visible = ref(false); const visible = ref(false);
const activeNav = ref(props.defaultActive || (props.navItems.length > 0 ? props.navItems[0].name : '')); const activeNav = ref(props.defaultActive || (props.navItems.length > 0 ? props.navItems[0].name : ''));
const isFullscreen = ref(false);
watch(() => props.modelValue, (val) => { watch(() => props.modelValue, (val) => {
visible.value = val; visible.value = val;
if (!val) {
isFullscreen.value = false; // 关闭时重置全屏状态
}
}); });
watch(() => props.defaultActive, (val) => { watch(() => props.defaultActive, (val) => {
@@ -51,17 +56,46 @@ function handleConfirm() {
emit('confirm', activeNav.value); emit('confirm', activeNav.value);
handleClose(); handleClose();
} }
function toggleFullscreen() {
isFullscreen.value = !isFullscreen.value;
}
</script> </script>
<template> <template>
<el-dialog <el-dialog
v-model="visible" v-model="visible"
:title="title" :title="title"
:width="width" :width="isFullscreen ? '100%' : width"
:before-close="handleClose" :before-close="handleClose"
:fullscreen="isFullscreen"
:top="isFullscreen ? '0' : '5vh'"
class="nav-dialog" class="nav-dialog"
> >
<template #header="{ titleId, titleClass }">
<div class="dialog-header">
<h4 :id="titleId" :class="titleClass">
{{ title }}
</h4>
<!-- 全屏按钮暂不做 -->
<div v-if="false" class="header-actions">
<slot name="extra-actions" />
<el-button
circle
plain
size="small"
class="fullscreen-btn"
:title="isFullscreen ? '退出全屏' : '全屏'"
@click="toggleFullscreen"
>
<el-icon>
<FullScreen />
</el-icon>
</el-button>
</div>
</div>
</template>
<div class="dialog-container"> <div class="dialog-container">
<!-- 左侧导航 --> <!-- 左侧导航 -->
<div class="nav-side"> <div class="nav-side">
@@ -104,24 +138,52 @@ function handleConfirm() {
</template> </template>
<style scoped> <style scoped>
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.fullscreen-btn {
transition: all 0.3s;
}
.fullscreen-btn:hover {
transform: scale(1.1);
}
.dialog-container { .dialog-container {
display: flex; display: flex;
height: 500px; height: 70vh;
min-height: 500px;
}
:deep(.el-dialog.is-fullscreen) .dialog-container {
height: calc(100vh - 120px);
} }
.nav-side { .nav-side {
width: 200px; width: 200px;
border-right: 1px solid #e6e6e6; border-right: 1px solid #e6e6e6;
flex-shrink: 0;
} }
.nav-menu { .nav-menu {
border-right: none; border-right: none;
height: 100%;
} }
.content-main { .content-main {
flex: 1; flex: 1;
padding: 0 20px; padding: 0 20px;
overflow: auto; overflow: auto;
min-width: 0;
} }
.empty-content { .empty-content {

View File

@@ -262,7 +262,7 @@ onMounted(async () => {
<!-- 自适应缩放 iframe --> <!-- 自适应缩放 iframe -->
<iframe <iframe
src="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde" src="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde"
class="min-w-full h-[700px] scale-100 duration-300" class="min-w-full iframe-responsive scale-100 duration-300"
loading="lazy" loading="lazy"
sandbox="allow-scripts allow-same-origin allow-popups" sandbox="allow-scripts allow-same-origin allow-popups"
@load="document.querySelector('.iframe-loading')?.remove()" @load="document.querySelector('.iframe-loading')?.remove()"
@@ -315,9 +315,17 @@ onMounted(async () => {
<style scoped> <style scoped>
.api-key-management { .api-key-management {
padding: 20px; padding:5px 20px;
max-width: 600px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
min-height: 200px;
}
/* iframe 响应式高度 */
.iframe-responsive {
height: 60vh;
min-height: 400px;
max-height: 800px;
} }
/* 未领取状态样式 */ /* 未领取状态样式 */
@@ -389,20 +397,37 @@ onMounted(async () => {
/* 已领取状态样式 */ /* 已领取状态样式 */
.claimed-state { .claimed-state {
margin: 30px 0; margin: 30px 0;
padding: 30px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
} }
.key-title { .key-title {
text-align: center; text-align: center;
color: #333; color: #333;
margin-bottom: 20px; margin-bottom: 30px;
font-size: 22px;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
} }
.key-display { .key-display {
margin: 20px 0; margin: 20px 0;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
} }
.key-input { .key-input {
font-family: monospace; font-family: 'Courier New', monospace;
font-size: 14px;
}
.key-input :deep(.el-input__inner) {
font-weight: 600;
letter-spacing: 1px;
} }
/* 添加按钮间距 */ /* 添加按钮间距 */
@@ -414,71 +439,89 @@ onMounted(async () => {
.key-input :deep(.el-input-group__append .el-button + .el-button) { .key-input :deep(.el-input-group__append .el-button + .el-button) {
margin-left: 4px; margin-left: 4px;
} }
.key-actions { .key-actions {
text-align: center; text-align: center;
margin-top: 30px; margin-top: 30px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
} }
.key-hint { .key-hint {
color: #999; color: #999;
font-size: 14px; font-size: 14px;
margin-top: 10px; margin-top: 12px;
font-style: italic;
} }
/* 使用说明样式 */ /* 使用说明样式 */
.usage-guide { .usage-guide {
margin-top: 40px; margin-top: 10px;
padding: 10px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
} }
.guide-content { .guide-content {
background: #f8f9fa; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 15px; padding: 20px;
border-radius: 4px; border-radius: 8px;
line-height: 1.8; line-height: 1.8;
border-left: 4px solid #409eff;
} }
/* 成功弹窗样式 */ /* 成功弹窗样式 */
.success-dialog { .success-dialog {
text-align: center; text-align: center;
padding: 20px 0; padding: 30px 0;
} }
.success-message { .success-message {
font-size: 18px; font-size: 20px;
font-weight: 600;
color: #333; color: #333;
margin: 15px 0 5px; margin: 20px 0 10px;
} }
.success-tip { .success-tip {
color: #999; color: #666;
font-size: 14px; font-size: 15px;
margin-top: 8px;
} }
/* 未领取状态样式 */ /* 未领取状态样式 */
.unclaimed-state { .unclaimed-state {
text-align: center; text-align: center;
margin: 30px 0; margin: 40px 0;
padding: 40px;
perspective: 600px; perspective: 600px;
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
border-radius: 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
} }
.gift-container { .gift-container {
position: relative; position: relative;
width: 150px; width: 180px;
height: 150px; height: 180px;
margin: 0 auto; margin: 0 auto 30px;
cursor: pointer; cursor: pointer;
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.15));
} }
.gift-box { .gift-box {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: all 0.3s; transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform-style: preserve-3d; transform-style: preserve-3d;
} }
.gift-box:hover:not(.opening) { .gift-box:hover:not(.opening) {
transform: translateY(-5px); transform: translateY(-10px) scale(1.05);
} }
.gift-box.opening .gift-lid { .gift-box.opening .gift-lid {
@@ -592,16 +635,23 @@ onMounted(async () => {
.claim-text { .claim-text {
margin-top: 30px; margin-top: 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
backdrop-filter: blur(10px);
} }
.claim-text h3 { .claim-text h3 {
color: #e74c3c; color: #e74c3c;
margin-bottom: 10px; margin-bottom: 15px;
font-size: 18px; font-size: 20px;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.claim-text p { .claim-text p {
color: #666; color: #555;
font-size: 14px; font-size: 15px;
font-weight: 500;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,436 @@
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { getTodayTaskStatus, claimTaskReward } from '@/api/dailyTask';
import type { DailyTaskStatusOutput, DailyTaskItem } from '@/api/dailyTask/types';
const taskData = ref<DailyTaskStatusOutput | null>(null);
const loading = ref(false);
const claiming = ref<{ [key: number]: boolean }>({});
onMounted(() => {
fetchTaskStatus();
});
async function fetchTaskStatus() {
loading.value = true;
try {
const res = await getTodayTaskStatus();
taskData.value = res.data;
} catch (error: any) {
ElMessage.error(error?.message || '获取任务状态失败');
} finally {
loading.value = false;
}
}
async function handleClaim(task: DailyTaskItem) {
if (task.status !== 1) return;
claiming.value[task.level] = true;
try {
await claimTaskReward({ taskLevel: task.level });
ElMessage.success(`恭喜!获得 ${formatTokenDisplay(task.rewardTokens)} token`);
// 刷新任务状态
await fetchTaskStatus();
} catch (error: any) {
ElMessage.error(error?.message || '领取奖励失败');
} finally {
claiming.value[task.level] = false;
}
}
// 格式化 Token 显示(单位:万)
function formatTokenDisplay(tokens: number): string {
return `${(tokens / 10000).toFixed(0)}w`;
}
// 获取任务状态文本
function getStatusText(task: DailyTaskItem): string {
switch (task.status) {
case 0:
return '未达成';
case 1:
return `领取 ${formatTokenDisplay(task.rewardTokens)}`;
case 2:
return '✓ 已领取';
default:
return '未知';
}
}
// 获取按钮样式类
function getButtonClass(task: DailyTaskItem): string {
switch (task.status) {
case 0:
return 'btn-disabled';
case 1:
return 'btn-claimable';
case 2:
return 'btn-claimed';
default:
return '';
}
}
// 获取进度条颜色
function getProgressColor(task: DailyTaskItem): string {
if (task.status === 2) return '#FFD700'; // 已完成:金色
if (task.status === 1) return '#67C23A'; // 可领取:绿色
return '#409EFF'; // 进行中:蓝色
}
</script>
<template>
<div v-loading="loading" class="daily-task-container">
<div class="task-header">
<h2>每日任务</h2>
<p class="task-desc">完成每日任务领取额外尊享包 Token 奖励可累加重复</p>
</div>
<div v-if="taskData" class="task-content">
<!-- 今日消耗统计 -->
<div class="consumption-card">
<div class="consumption-icon">🔥</div>
<div class="consumption-info">
<div class="consumption-label">今日尊享包消耗</div>
<div class="consumption-value">
{{ formatTokenDisplay(taskData.todayConsumedTokens) }} Tokens
</div>
</div>
</div>
<!-- 任务列表 -->
<div class="task-list">
<div
v-for="task in taskData.tasks"
:key="task.level"
class="task-item"
:class="{
'task-completed': task.status === 2,
'task-claimable': task.status === 1
}"
>
<div class="task-icon">
<span v-if="task.status === 2">🎁</span>
<span v-else-if="task.status === 1"></span>
<span v-else>📦</span>
</div>
<div class="task-main">
<div class="task-title">
<span class="task-name">{{ task.name }}</span>
<span class="task-badge" :class="`badge-status-${task.status}`">
{{ task.status === 0 ? '未完成' : task.status === 1 ? '可领取' : '已完成' }}
</span>
</div>
<div class="task-description">
{{ task.description }}
</div>
<div class="task-progress-section">
<div class="progress-info">
<span class="progress-text">
{{ formatTokenDisplay(taskData.todayConsumedTokens) }} / {{ formatTokenDisplay(task.requiredTokens) }}
</span>
<span class="progress-percent">{{ task.progress.toFixed(0) }}%</span>
</div>
<el-progress
:percentage="Math.min(task.progress, 100)"
:color="getProgressColor(task)"
:show-text="false"
:stroke-width="8"
/>
</div>
<div class="task-reward">
<span class="reward-label">奖励</span>
<span class="reward-value">{{ formatTokenDisplay(task.rewardTokens) }} Tokens</span>
</div>
</div>
<div class="task-action">
<el-button
:class="getButtonClass(task)"
:disabled="task.status !== 1 || claiming[task.level]"
:loading="claiming[task.level]"
size="large"
@click="handleClaim(task)"
>
{{ getStatusText(task) }}
</el-button>
</div>
</div>
</div>
<!-- 提示信息 -->
<div class="task-tips">
<el-alert
title="温馨提示"
type="info"
:closable="false"
>
<template #default>
<ul>
<li>任务每日 0 点自动重置</li>
<li>使用尊享包模型消耗的 Token 计入任务进度</li>
<li>完成任务后立即领取奖励奖励直接发放到您的尊享包账户</li>
</ul>
</template>
</el-alert>
</div>
</div>
</div>
</template>
<style scoped>
.daily-task-container {
padding: 20px;
min-height: 400px;
height: 100%;
overflow-y: auto;
}
.task-header {
margin-bottom: 24px;
}
.task-header h2 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: bold;
color: #303133;
}
.task-desc {
margin: 0;
color: #909399;
font-size: 14px;
}
.task-content {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 消耗统计卡片 */
.consumption-card {
display: flex;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.consumption-icon {
font-size: 48px;
margin-right: 20px;
}
.consumption-info {
flex: 1;
}
.consumption-label {
font-size: 14px;
opacity: 0.9;
margin-bottom: 4px;
}
.consumption-value {
font-size: 32px;
font-weight: bold;
}
/* 任务列表 */
.task-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.task-item {
display: flex;
align-items: stretch;
padding: 20px;
background: #ffffff;
border: 2px solid #e4e7ed;
border-radius: 12px;
transition: all 0.3s;
}
.task-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.task-item.task-claimable {
border-color: #67C23A;
background: linear-gradient(to right, rgba(103, 194, 58, 0.05) 0%, transparent 100%);
}
.task-item.task-completed {
border-color: #FFD700;
background: linear-gradient(to right, rgba(255, 215, 0, 0.05) 0%, transparent 100%);
}
.task-icon {
font-size: 48px;
margin-right: 20px;
display: flex;
align-items: center;
}
.task-main {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.task-title {
display: flex;
align-items: center;
gap: 12px;
}
.task-name {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.task-badge {
padding: 2px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.badge-status-0 {
background: #f4f4f5;
color: #909399;
}
.badge-status-1 {
background: #f0f9ff;
color: #67C23A;
animation: pulse 2s infinite;
}
.badge-status-2 {
background: #fffbf0;
color: #FFD700;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.task-description {
color: #606266;
font-size: 14px;
}
.task-progress-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 13px;
color: #909399;
}
.progress-text {
font-weight: 500;
}
.progress-percent {
font-weight: bold;
}
.task-reward {
font-size: 14px;
}
.reward-label {
color: #909399;
}
.reward-value {
color: #F56C6C;
font-weight: bold;
font-size: 16px;
}
.task-action {
display: flex;
align-items: center;
margin-left: 20px;
}
/* 按钮样式 */
.btn-disabled {
background: #f4f4f5;
border-color: #e4e7ed;
color: #909399;
}
.btn-claimable {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
border: none;
color: white;
font-weight: bold;
animation: shimmer 2s infinite;
}
.btn-claimable:hover {
transform: scale(1.05);
box-shadow: 0 4px 16px rgba(255, 215, 0, 0.5);
}
@keyframes shimmer {
0%, 100% {
box-shadow: 0 0 20px rgba(255, 215, 0, 0.6);
}
50% {
box-shadow: 0 0 40px rgba(255, 215, 0, 0.8);
}
}
.btn-claimed {
background: #f4f4f5;
border-color: #e4e7ed;
color: #67C23A;
}
/* 提示信息 */
.task-tips {
margin-top: 8px;
}
.task-tips ul {
margin: 8px 0 0 0;
padding-left: 20px;
}
.task-tips li {
margin: 4px 0;
font-size: 13px;
color: #606266;
}
</style>

View File

@@ -0,0 +1,536 @@
<script lang="ts" setup>
import { Clock, Coin, TrophyBase, WarningFilled } from '@element-plus/icons-vue';
import { getPremiumTokenPackage } from '@/api/user';
import { showProductPackage } from '@/utils/product-package.ts';
// 尊享服务数据
const loading = ref(false);
const packageData = ref<any>({
totalQuota: 0, // 购买总额度
usedQuota: 0, // 已使用额度
remainingQuota: 0, // 剩余额度
usagePercentage: 0, // 使用百分比
packageName: 'Token包', // 套餐名称
expireDate: '', // 过期时间
});
// 计算属性
const usagePercent = computed(() => {
if (packageData.value.totalQuota === 0)
return 0;
return Number(((packageData.value.usedQuota / packageData.value.totalQuota) * 100).toFixed(2));
});
const remainingPercent = computed(() => {
if (packageData.value.totalQuota === 0)
return 0;
return Number(((packageData.value.remainingQuota / packageData.value.totalQuota) * 100).toFixed(2));
});
// 获取进度条颜色(基于剩余百分比)
const progressColor = computed(() => {
const percent = remainingPercent.value;
if (percent <= 10)
return '#f56c6c'; // 红色 - 剩余很少
if (percent <= 30)
return '#e6a23c'; // 橙色 - 剩余较少
return '#67c23a'; // 绿色 - 剩余充足
});
// 格式化数字 - 转换为万为单位
function formatNumber(num: number): string {
if (num === 0)
return '0';
const wan = num / 10000;
// 保留2位小数去掉末尾的0
return wan.toFixed(2).replace(/\.?0+$/, '');
}
// 格式化原始数字(带千分位)
function formatRawNumber(num: number): string {
return num.toLocaleString();
}
/*
前端已准备好后端需要提供以下API
接口地址: GET /account/premium/token-package
返回数据格式:
{
"success": true,
"data": {
"totalQuota": 1000000, // 购买总额度
"usedQuota": 350000, // 已使用额度
"remainingQuota": 650000, // 剩余额度(可选,前端会自动计算)
"usagePercentage": 35, // 使用百分比(可选)
"packageName": "尊享VIP套餐", // 套餐名称(可选)
"expireDate": "2024-12-31" // 过期时间(可选)
}
} */
// 获取尊享服务Token包数据
async function fetchPremiumTokenPackage() {
loading.value = true;
try {
const res = await getPremiumTokenPackage();
if (res.data) {
// 适配新的接口字段名
const data = res.data;
packageData.value = {
totalQuota: data.premiumTotalTokens || 0, // 尊享包总token
usedQuota: data.premiumUsedTokens || 0, // 尊享包已使用token
remainingQuota: data.premiumRemainingTokens || 0, // 尊享包剩余token
usagePercentage: data.usagePercentage || 0,
packageName: data.packageName || '尊享Token包',
expireDate: data.expireDate || '',
};
// 计算剩余额度(如果接口没返回)
if (packageData.value.remainingQuota === 0 && packageData.value.totalQuota > 0) {
packageData.value.remainingQuota = packageData.value.totalQuota - packageData.value.usedQuota;
}
}
else {
ElMessage.warning('暂无尊享服务数据');
}
}
catch (error) {
console.error('获取尊享服务数据失败:', error);
ElMessage.error('获取尊享服务数据失败');
}
finally {
loading.value = false;
}
}
// 刷新数据
function refreshData() {
fetchPremiumTokenPackage();
}
onMounted(() => {
fetchPremiumTokenPackage();
});
function onProductPackage() {
showProductPackage();
}
</script>
<template>
<div v-loading="loading" class="premium-service">
<div class="header">
<h2>
<el-icon><TrophyBase /></el-icon>
尊享服务
</h2>
<el-button
type="primary"
size="small"
@click="refreshData"
>
刷新数据
</el-button>
</div>
<!-- 套餐信息卡片 -->
<el-card class="package-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon class="header-icon">
<Coin />
</el-icon>
<span class="header-title">{{ packageData.packageName }}</span>
</div>
</template>
<div class="package-content">
<!-- 统计数据 -->
<div class="stats-grid">
<div class="stat-item total">
<div class="stat-label">
购买总额度
</div>
<div class="stat-value">
{{ formatNumber(packageData.totalQuota) }}
</div>
<div class="stat-unit">
Tokens
</div>
<div class="stat-raw" :title="`原始值: ${formatRawNumber(packageData.totalQuota)} Tokens`">
({{ formatRawNumber(packageData.totalQuota) }})
</div>
</div>
<div class="stat-item used">
<div class="stat-label">
已用额度
</div>
<div class="stat-value">
{{ formatNumber(packageData.usedQuota) }}
</div>
<div class="stat-unit">
Tokens
</div>
<div class="stat-raw" :title="`原始值: ${formatRawNumber(packageData.usedQuota)} Tokens`">
({{ formatRawNumber(packageData.usedQuota) }})
</div>
</div>
<div class="stat-item remaining">
<div class="stat-label">
剩余额度
</div>
<div class="stat-value">
{{ formatNumber(packageData.remainingQuota) }}
</div>
<div class="stat-unit">
Tokens
</div>
<div class="stat-raw" :title="`原始值: ${formatRawNumber(packageData.remainingQuota)} Tokens`">
({{ formatRawNumber(packageData.remainingQuota) }})
</div>
</div>
</div>
<!-- 进度条 -->
<div class="progress-section">
<div class="progress-header">
<span class="progress-label">剩余进度</span>
<span class="progress-percent" :style="{ color: progressColor }">
{{ remainingPercent }}%
</span>
</div>
<el-progress
:percentage="remainingPercent"
:color="progressColor"
:stroke-width="20"
:show-text="false"
/>
<div class="progress-legend">
<div class="legend-item">
<span class="legend-dot " :style="{ background: progressColor }" />
<span class="legend-text">剩余: {{ remainingPercent }}%</span>
</div>
<div class="legend-item">
<span class="legend-dot used-dot" />
<span class="legend-text">已使用: {{ usagePercent }}%</span>
</div>
</div>
</div>
<!-- 购买提示卡片额度不足时显示 -->
<el-card
v-if="remainingPercent < 20"
class="warning-card"
shadow="hover"
>
<div class="warning-content">
<el-icon class="warning-icon" color="#e6a23c">
<WarningFilled />
</el-icon>
<div class="warning-text">
<h3>额度即将用完</h3>
<p>您的Token额度已使用{{ usagePercent }}%剩余额度较少建议及时充值</p>
</div>
<el-button type="warning" @click="onProductPackage()">
立即充值
</el-button>
</div>
</el-card>
<!-- 过期时间 -->
<div v-if="packageData.expireDate" class="expire-info">
<el-icon><Clock /></el-icon>
<span>有效期至: {{ packageData.expireDate }}</span>
</div>
<!-- 温馨提示 -->
<el-alert
class="tips-alert"
type="info"
:closable="false"
show-icon
>
<template #title>
<div class="tips-content">
<p>温馨提示</p>
<ul>
<li>Token额度根据不同模型消耗速率不同</li>
<li>建议合理使用避免额度过快消耗</li>
<li>额度不足时请及时充值避免影响使用</li>
</ul>
</div>
</template>
</el-alert>
</div>
</el-card>
</div>
</template>
<style scoped>
.premium-service {
padding: 20px;
position: relative;
height: 100%;
overflow-y: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
display: flex;
align-items: center;
margin: 0;
font-size: 20px;
color: #333;
}
.header .el-icon {
margin-right: 8px;
color: #f59e0b;
}
/* 套餐卡片 */
.package-card {
margin-bottom: 20px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: #333;
}
.header-icon {
font-size: 20px;
color: #f59e0b;
}
.package-content {
display: flex;
flex-direction: column;
gap: 24px;
}
/* 统计数据网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.stat-item {
padding: 20px;
border-radius: 8px;
text-align: center;
transition: transform 0.2s;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.stat-item:hover {
transform: translateY(-4px);
}
.stat-item.total {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stat-item.used {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-item.remaining {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-label {
font-size: 14px;
opacity: 0.9;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
margin-bottom: 4px;
}
.stat-unit {
font-size: 12px;
opacity: 0.8;
}
.stat-raw {
font-size: 11px;
opacity: 0.7;
margin-top: 4px;
cursor: help;
transition: opacity 0.2s;
}
.stat-raw:hover {
opacity: 1;
}
/* 进度条部分 */
.progress-section {
padding: 20px;
background: #f7f8fa;
border-radius: 8px;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.progress-label {
font-size: 16px;
font-weight: 600;
color: #333;
}
.progress-percent {
font-size: 20px;
font-weight: 700;
}
.progress-legend {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.used-dot {
background: #409eff;
}
.remaining-dot {
background: #e4e7ed;
}
.legend-text {
font-size: 14px;
color: #606266;
}
/* 过期信息 */
.expire-info {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #f0f9ff;
border-radius: 6px;
color: #0369a1;
font-size: 14px;
}
/* 温馨提示 */
.tips-alert {
border-radius: 8px;
}
.tips-content p {
margin: 0 0 8px 0;
font-weight: 600;
}
.tips-content ul {
margin: 0;
padding-left: 20px;
}
.tips-content li {
margin: 4px 0;
font-size: 13px;
}
/* 警告卡片 */
.warning-card {
border-radius: 12px;
border: 2px solid #e6a23c;
background: #fef3e9;
}
.warning-content {
display: flex;
align-items: center;
gap: 16px;
}
.warning-icon {
font-size: 40px;
flex-shrink: 0;
}
.warning-text {
flex: 1;
}
.warning-text h3 {
margin: 0 0 8px 0;
color: #e6a23c;
font-size: 18px;
}
.warning-text p {
margin: 0;
color: #606266;
font-size: 14px;
}
/* 响应式布局 */
@media (max-width: 768px) {
.premium-service {
padding: 10px;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.stat-value {
font-size: 24px;
}
.progress-legend {
flex-direction: column;
gap: 8px;
}
.warning-content {
flex-direction: column;
text-align: center;
}
}
</style>

View File

@@ -104,6 +104,11 @@ function contactCustomerService() {
innerVisibleContact.value = !innerVisibleContact.value; innerVisibleContact.value = !innerVisibleContact.value;
} }
// 暴露方法给父组件使用
defineExpose({
contactCustomerService,
});
// 过滤和排序后的数据 // 过滤和排序后的数据
const filteredData = computed(() => { const filteredData = computed(() => {
let data = [...logData.value]; let data = [...logData.value];
@@ -156,7 +161,7 @@ onMounted(() => {
> >
<h3 class="text-lg font-bold mb-3"> <h3 class="text-lg font-bold mb-3">
请扫码加入微信交流群<br> 请扫码加入微信交流群<br>
备注ai获取专属客服支持 备注AI获取专属客服支持
</h3> </h3>
<div class="mb-4 flex items-center justify-center space-x-2"> <div class="mb-4 flex items-center justify-center space-x-2">
<span class="font-semibold">站长微信账号</span> <span class="font-semibold">站长微信账号</span>
@@ -182,15 +187,15 @@ onMounted(() => {
@click="showWechatFullscreenImage" @click="showWechatFullscreenImage"
> >
</div> </div>
<div class="px-4"> <!-- <div class="px-4"> -->
<h4>微信交流群</h4> <!-- <h4>微信交流群</h4> -->
<img <!-- <img -->
:src="wxGroupQD" <!-- :src="wxGroupQD" -->
class="w-50 py-5 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105" <!-- class="w-50 py-5 h-70 border border-gray-200 rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-transform hover:scale-105" -->
alt="微信二维码" <!-- alt="微信二维码" -->
@click="showWxGroupFullscreenImage" <!-- @click="showWxGroupFullscreenImage" -->
> <!-- > -->
</div> <!-- </div> -->
</div> </div>
<!-- 全屏放大二维码 --> <!-- 全屏放大二维码 -->
@@ -253,7 +258,6 @@ onMounted(() => {
</div> </div>
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="filteredData" :data="filteredData"
style="width: 100%" style="width: 100%"
@@ -265,7 +269,7 @@ onMounted(() => {
<el-table-column <el-table-column
prop="content" prop="content"
label="套餐类型" label="套餐类型"
width="150" min-width="150"
sortable="custom" sortable="custom"
show-overflow-tooltip show-overflow-tooltip
/> />
@@ -273,7 +277,7 @@ onMounted(() => {
show-overflow-tooltip show-overflow-tooltip
prop="rechargeAmount" prop="rechargeAmount"
label="金额(元)" label="金额(元)"
width="110" min-width="110"
sortable="custom" sortable="custom"
> >
<template #default="{ row }"> <template #default="{ row }">
@@ -283,14 +287,14 @@ onMounted(() => {
<el-table-column <el-table-column
prop="creationTime" prop="creationTime"
label="充值时间" label="充值时间"
width="160" min-width="160"
sortable="custom" sortable="custom"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
prop="expireDateTime" prop="expireDateTime"
label="到期时间" label="到期时间"
width="160" min-width="160"
sortable="custom" sortable="custom"
show-overflow-tooltip show-overflow-tooltip
/> />
@@ -302,7 +306,7 @@ onMounted(() => {
<span v-else>{{ row.contactInfo || '-' }}</span> <span v-else>{{ row.contactInfo || '-' }}</span>
</template> </template>
</el-table-column> --> </el-table-column> -->
<el-table-column show-overflow-tooltip prop="remark" label="备注" width="160"> <el-table-column show-overflow-tooltip prop="remark" label="备注" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip v-if="row.remark && row.remark.length > 10" :content="row.remark" placement="top"> <el-tooltip v-if="row.remark && row.remark.length > 10" :content="row.remark" placement="top">
<span class="ellipsis-text">{{ row.remark }}</span> <span class="ellipsis-text">{{ row.remark }}</span>
@@ -334,59 +338,96 @@ onMounted(() => {
.fullscreen-image-overlay { .fullscreen-image-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px);
z-index: 9999; z-index: 9999;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
} }
.fullscreen-image { .fullscreen-image {
max-width: 90%; max-width: 90%;
max-height: 90%; max-height: 90%;
border: 8px solid white; border: 8px solid white;
border-radius: 16px; border-radius: 20px;
box-shadow: 0 0 40px rgba(255, 255, 255, 0.2); box-shadow: 0 0 60px rgba(255, 255, 255, 0.3);
animation: zoomIn 0.3s ease;
}
@keyframes zoomIn {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
} }
.recharge-log-container { .recharge-log-container {
padding: 20px; padding: 10px;
background: #fff; background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
border-radius: 8px; border-radius: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
height: 100%;
overflow-y: auto;
}
.recharge-log-container:hover {
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.12);
} }
.log-header { .log-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 25px;
padding-bottom: 15px; padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0; border-bottom: 2px solid #e9ecef;
} }
.log-title { .log-title {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0; margin: 0;
font-size: 18px; font-size: 22px;
font-weight: 600;
color: #333; color: #333;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
} }
.log-title .el-icon { .log-title .el-icon {
margin-right: 8px; margin-right: 10px;
color: #409eff; color: #409eff;
font-size: 24px;
} }
.header-actions { .header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px;
} }
.amount-cell { .amount-cell {
font-family: 'Arial', sans-serif; font-family: 'Arial', sans-serif;
font-weight: bold; font-weight: 700;
font-size: 15px;
color: #e74c3c; color: #e74c3c;
text-shadow: 0 1px 2px rgba(231, 76, 60, 0.2);
} }
.ellipsis-text { .ellipsis-text {
@@ -402,38 +443,60 @@ onMounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-top: 20px; margin-top: 25px;
padding-top: 15px; padding-top: 20px;
border-top: 1px solid #f0f0f0; border-top: 2px solid #e9ecef;
} }
.summary { .summary {
color: #666; color: #666;
font-size: 14px; font-size: 15px;
font-weight: 500;
} }
.highlight { .highlight {
color: #409eff; color: #409eff;
font-weight: bold; font-weight: 700;
font-size: 18px;
padding: 0 6px;
} }
/* 表格样式优化 */ /* 表格样式优化 */
:deep(.el-table) { :deep(.el-table) {
font-size: 14px; font-size: 14px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
} }
:deep(.el-table th) { :deep(.el-table th) {
background-color: #f8fafc; background-color: #f8fafc;
color: #333; color: #333;
font-weight: 600; font-weight: 600;
font-size: 14px;
padding: 8px 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
:deep(.el-table th .cell) {
} }
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) { :deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background-color: #fafafa; background-color: #f8f9fb;
} }
:deep(.el-table .cell) { :deep(.el-table .cell) {
padding: 8px 12px; padding: 12px 16px;
}
:deep(.el-table tbody tr) {
transition: all 0.3s ease;
}
:deep(.el-table tbody tr:hover) {
background: linear-gradient(90deg, #f0f7ff 0%, #e6f4ff 100%) !important;
transform: scale(1.01);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
} }
/* 分页样式优化 */ /* 分页样式优化 */

View File

@@ -16,7 +16,6 @@ const props = withDefaults(defineProps<Props>(), {
// 从store获取模型列表 // 从store获取模型列表
const modelStore = useModelStore(); const modelStore = useModelStore();
const modelList = computed(() => modelStore.modelList); const modelList = computed(() => modelStore.modelList);
console.log('modelList---', modelList);
// 计算网格布局的列数 // 计算网格布局的列数
const gridTemplateColumns = computed(() => { const gridTemplateColumns = computed(() => {
@@ -75,6 +74,8 @@ const gridTemplateColumns = computed(() => {
<style scoped> <style scoped>
.model-container { .model-container {
margin: 10px 0; margin: 10px 0;
height: 100%;
overflow-y: auto;
} }
.model-header { .model-header {

View File

@@ -48,6 +48,9 @@ const totalTokens = ref(0);
const usageData = ref<any[]>([]); const usageData = ref<any[]>([]);
const modelUsageData = ref<any[]>([]); const modelUsageData = ref<any[]>([]);
// 计算属性:是否有模型数据
const hasModelData = computed(() => modelUsageData.value.length > 0);
// 获取用量数据 // 获取用量数据
async function fetchUsageData() { async function fetchUsageData() {
loading.value = true; loading.value = true;
@@ -156,9 +159,67 @@ function updateLineChart() {
// 更新饼图 - 动态适应数据量 // 更新饼图 - 动态适应数据量
function updatePieChart() { function updatePieChart() {
if (!pieChartInstance || modelUsageData.value.length === 0) if (!pieChartInstance)
return; return;
// 空数据状态
if (modelUsageData.value.length === 0) {
const emptyOption = {
graphic: [
{
type: 'group',
left: 'center',
top: 'center',
children: [
{
type: 'circle',
shape: {
r: 80,
},
style: {
fill: '#f5f7fa',
stroke: '#e9ecef',
lineWidth: 2,
},
},
{
type: 'text',
style: {
text: '📊',
fontSize: 48,
x: -24,
y: -40,
},
},
{
type: 'text',
style: {
text: '暂无数据',
fontSize: 18,
fontWeight: 'bold',
fill: '#909399',
x: -36,
y: 20,
},
},
{
type: 'text',
style: {
text: '还没有模型使用记录',
fontSize: 14,
fill: '#c0c4cc',
x: -70,
y: 50,
},
},
],
},
],
};
pieChartInstance.setOption(emptyOption, true);
return;
}
const data = modelUsageData.value.map(item => ({ const data = modelUsageData.value.map(item => ({
name: item.model, name: item.model,
value: item.tokens, value: item.tokens,
@@ -168,6 +229,7 @@ function updatePieChart() {
const isSmallContainer = pieContainerSize.width.value < 600; const isSmallContainer = pieContainerSize.width.value < 600;
const option = { const option = {
graphic: [], // 清空graphic配置
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{a} <br/>{b}: {c} tokens ({d}%)', formatter: '{a} <br/>{b}: {c} tokens ({d}%)',
@@ -222,14 +284,76 @@ function updatePieChart() {
], ],
}; };
pieChartInstance.setOption(option); pieChartInstance.setOption(option, true);
} }
// 更新柱状图 - 动态适应数据量 // 更新柱状图 - 动态适应数据量
function updateBarChart() { function updateBarChart() {
if (!barChartInstance || modelUsageData.value.length === 0) if (!barChartInstance)
return; return;
// 空数据状态
if (modelUsageData.value.length === 0) {
const emptyOption = {
graphic: [
{
type: 'group',
left: 'center',
top: 'center',
children: [
{
type: 'rect',
shape: {
width: 160,
height: 160,
r: 12,
},
style: {
fill: '#f5f7fa',
stroke: '#e9ecef',
lineWidth: 2,
},
left: -80,
top: -80,
},
{
type: 'text',
style: {
text: '📈',
fontSize: 48,
x: -24,
y: -40,
},
},
{
type: 'text',
style: {
text: '暂无数据',
fontSize: 18,
fontWeight: 'bold',
fill: '#909399',
x: -36,
y: 20,
},
},
{
type: 'text',
style: {
text: '还没有模型使用记录',
fontSize: 14,
fill: '#c0c4cc',
x: -70,
y: 50,
},
},
],
},
],
};
barChartInstance.setOption(emptyOption, true);
return;
}
const models = modelUsageData.value.map(item => item.model); const models = modelUsageData.value.map(item => item.model);
const tokens = modelUsageData.value.map(item => item.tokens); const tokens = modelUsageData.value.map(item => item.tokens);
@@ -237,6 +361,7 @@ function updateBarChart() {
const isSmallContainer = barContainerSize.width.value < 600; const isSmallContainer = barContainerSize.width.value < 600;
const option = { const option = {
graphic: [], // 清空graphic配置
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
@@ -302,7 +427,7 @@ function updateBarChart() {
], ],
}; };
barChartInstance.setOption(option); barChartInstance.setOption(option, true);
} }
// 调整图表大小 // 调整图表大小
@@ -362,36 +487,36 @@ onBeforeUnmount(() => {
<el-card v-loading="loading" class="chart-card"> <el-card v-loading="loading" class="chart-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>近七天每日Token消耗量</span> <span class="card-title">📊 近七天每日Token消耗量</span>
<el-tag type="primary"> <el-tag type="primary" size="large" effect="dark">
近七日总计: {{ totalTokens }} tokens 近七日总计: {{ totalTokens }} tokens
</el-tag> </el-tag>
</div> </div>
</template> </template>
<div class="chart-container"> <div class="chart-container">
<div ref="lineChart" class="chart" style="height: 400px;" /> <div ref="lineChart" class="chart" style="height: 350px;" />
</div> </div>
</el-card> </el-card>
<el-card v-loading="loading" class="chart-card"> <el-card v-loading="loading" class="chart-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>各模型Token消耗占比</span> <span class="card-title">🥧 各模型Token消耗占比</span>
</div> </div>
</template> </template>
<div class="chart-container"> <div class="chart-container">
<div ref="pieChart" class="chart" style="height: 450px;" /> <div ref="pieChart" class="chart" style="height: 400px;" />
</div> </div>
</el-card> </el-card>
<el-card v-loading="loading" class="chart-card"> <el-card v-loading="loading" class="chart-card">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>各模型总Token消耗量</span> <span class="card-title">📈 各模型总Token消耗量</span>
</div> </div>
</template> </template>
<div class="chart-container"> <div class="chart-container">
<div ref="barChart" class="chart" style="height: 500px;" /> <div ref="barChart" class="chart" style="height: 450px;" />
</div> </div>
</el-card> </el-card>
</div> </div>
@@ -401,7 +526,16 @@ onBeforeUnmount(() => {
.usage-statistics { .usage-statistics {
padding: 20px; padding: 20px;
position: relative; position: relative;
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease; transition: all 0.3s ease;
height: 100%;
overflow-y: auto;
}
.usage-statistics:hover {
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.12);
} }
.usage-statistics.fullscreen-mode { .usage-statistics.fullscreen-mode {
@@ -411,60 +545,100 @@ onBeforeUnmount(() => {
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 2000; z-index: 2000;
background: #fff; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px; padding: 30px;
overflow-y: auto; overflow-y: auto;
border-radius: 0;
} }
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e9ecef;
} }
.header h2 { .header h2 {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0; margin: 0;
font-size: 20px; font-size: 24px;
font-weight: 600;
color: #333; color: #333;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
} }
.header .el-icon { .header .el-icon {
margin-right: 8px; margin-right: 10px;
color: #3a4de9; color: #3a4de9;
font-size: 28px;
} }
.chart-card { .chart-card {
margin-bottom: 20px; margin-bottom: 30px;
border-radius: 8px; border-radius: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
overflow: hidden;
background: white;
}
.chart-card:hover {
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-4px);
} }
.card-header { .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 16px;
font-weight: 600;
color: #333;
padding: 8px 0;
}
.card-title {
font-size: 18px;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
.chart-container { .chart-container {
width: 100%; width: 100%;
padding: 10px;
background: linear-gradient(135deg, #fafbfc 0%, #f5f6f8 100%);
border-radius: 12px;
} }
.chart { .chart {
width: 100%; width: 100%;
transition: all 0.3s ease;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.usage-statistics {
padding: 15px;
}
.header { .header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 12px;
margin-bottom: 20px;
}
.header h2 {
font-size: 20px;
} }
.chart-card { .chart-card {
margin-bottom: 15px; margin-bottom: 20px;
} }
.chart { .chart {
@@ -472,7 +646,11 @@ onBeforeUnmount(() => {
} }
.usage-statistics.fullscreen-mode { .usage-statistics.fullscreen-mode {
padding: 10px; padding: 15px;
}
.card-header {
font-size: 14px;
} }
} }
</style> </style>

View File

@@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Camera, Edit, SuccessFilled } from '@element-plus/icons-vue'; import { Camera, Edit, Postcard, Promotion, SuccessFilled, User } from '@element-plus/icons-vue';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { getUserInfo } from '@/api'; import { getUserInfo } from '@/api';
import QrCodeLogin from '@/components/LoginDialog/components/QrCodeLogin/index.vue'; import QrCodeLogin from '@/components/LoginDialog/components/QrCodeLogin/index.vue';
import { useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { getUserProfilePicture, WECHAT_QRCODE_TYPE } from '@/utils/user.ts'; import { getUserProfilePicture, isUserVip, WECHAT_QRCODE_TYPE } from '@/utils/user.ts';
const userStore = useUserStore(); const userStore = useUserStore();
@@ -25,11 +25,17 @@ const userIcon = computed(() => {
const userNick = computed(() => { const userNick = computed(() => {
return user.value.nick || user.value.userName || '未知用户'; return user.value.nick || user.value.userName || '未知用户';
}); });
// 是否绑定了微信 // 是否绑定了微信
const isWechatBound = computed(() => { const isWechatBound = computed(() => {
return userStore.userInfo.isBindFuwuhao || false; return userStore.userInfo.isBindFuwuhao || false;
}); });
// 用户VIP状态
const userVipStatus = computed(() => {
return isUserVip();
});
// 格式化日期 // 格式化日期
function formatDate(dateString: string | null) { function formatDate(dateString: string | null) {
if (!dateString) if (!dateString)
@@ -93,103 +99,164 @@ function handleWechatBind() {
// 微信绑定成功 // 微信绑定成功
function bindWechat() { function bindWechat() {
wechatDialogVisible.value = false; wechatDialogVisible.value = false;
ElMessage.success('微信绑定成功!');
} }
</script> </script>
<template> <template>
<div class="user-profile"> <div class="user-profile">
<el-card class="profile-card"> <!-- 顶部标题 -->
<template #header> <div class="header">
<div class="card-header"> <h2>
<h3>个人信息</h3> <el-icon><User /></el-icon>
<el-button v-if="false" type="primary" size="small" @click="handleEdit"> 个人信息
<el-icon><Edit /></el-icon> </h2>
编辑信息
</el-button>
</div> </div>
</template>
<div class="profile-content"> <!-- 用户卡片 -->
<el-card class="profile-card" shadow="hover">
<!-- 头像和基本信息区域 -->
<div class="user-header-section">
<!-- 头像区域 --> <!-- 头像区域 -->
<div class="avatar-section"> <div class="avatar-section">
<el-avatar :size="100" :src="userIcon" class="user-avatar"> <div class="avatar-wrapper">
<el-avatar :size="120" :src="userIcon" class="user-avatar">
{{ userNick.charAt(0) }} {{ userNick.charAt(0) }}
</el-avatar> </el-avatar>
<div v-if="userVipStatus" class="vip-badge">
<el-icon><Promotion /></el-icon>
VIP
</div>
</div>
<div v-if="false" class="avatar-actions"> <div v-if="false" class="avatar-actions">
<el-button size="small" @click="changeAvatar"> <el-button size="small" type="primary" @click="changeAvatar">
<el-icon><Camera /></el-icon> <el-icon><Camera /></el-icon>
更换头像 更换头像
</el-button> </el-button>
</div> </div>
</div> </div>
<!-- 基本信息 --> <!-- 用户名称和状态 -->
<div class="info-section"> <div class="user-info-quick">
<el-descriptions :column="1" border> <h3 class="user-name">{{ userNick }}</h3>
<el-descriptions-item label="用户名"> <div class="user-tags">
{{ user.userName || '-' }} <el-tag v-if="userVipStatus" type="warning" effect="dark" size="large">
</el-descriptions-item> <el-icon><Promotion /></el-icon>
尊享VIP会员
<el-descriptions-item label="昵称"> </el-tag>
{{ userNick }} <el-tag v-else type="info" size="large">
</el-descriptions-item> 普通用户
</el-tag>
<el-descriptions-item label="性别"> <el-tag :type="getSexTagType(user.sex)" size="large">
<el-tag :type="getSexTagType(user.sex)">
{{ getSexText(user.sex) }} {{ getSexText(user.sex) }}
</el-tag> </el-tag>
</el-descriptions-item> </div>
<div class="user-stats">
<div class="stat-item">
<div class="stat-value">{{ formatDate(user.creationTime)?.split(' ')[0] || '-' }}</div>
<div class="stat-label">注册时间</div>
</div>
</div>
</div>
</div>
<el-descriptions-item label="注册时间"> <el-divider />
{{ formatDate(user.creationTime) }}
</el-descriptions-item>
<el-descriptions-item label="邮箱"> <!-- 详细信息区域 -->
<div class="info-section">
<div class="info-grid">
<!-- 用户名 -->
<div class="info-item">
<div class="info-label">
<el-icon><User /></el-icon>
用户名
</div>
<div class="info-value">{{ user.userName || '-' }}</div>
</div>
<!-- 昵称 -->
<div class="info-item">
<div class="info-label">
<el-icon><Postcard /></el-icon>
昵称
</div>
<div class="info-value">{{ userNick }}</div>
</div>
<!-- 邮箱 -->
<div class="info-item">
<div class="info-label">
<el-icon><Message /></el-icon>
邮箱
</div>
<div class="info-value">
<span v-if="user.email"> <span v-if="user.email">
{{ maskEmail(user.email) }} {{ maskEmail(user.email) }}
<el-tooltip content="已验证" placement="top"> <el-tooltip content="已验证" placement="top">
<el-icon color="#67C23A"><SuccessFilled /></el-icon> <el-icon color="#67C23A" style="margin-left: 5px;"><SuccessFilled /></el-icon>
</el-tooltip> </el-tooltip>
</span> </span>
<span v-else class="unset-text">未设置</span> <span v-else class="unset-text">未设置</span>
</el-descriptions-item> </div>
</div>
<el-descriptions-item label="手机号"> <!-- 手机号 -->
<span v-if="user.phone"> <div class="info-item">
{{ maskPhone(user.phone) }} <div class="info-label">
</span> <el-icon><Phone /></el-icon>
手机号
</div>
<div class="info-value">
<span v-if="user.phone">{{ maskPhone(user.phone) }}</span>
<span v-else class="unset-text">未设置</span> <span v-else class="unset-text">未设置</span>
</el-descriptions-item> </div>
</div>
<el-descriptions-item label="微信绑定"> <!-- 微信绑定 -->
<div class="wechat-binding"> <div class="info-item full-width">
<span v-if="isWechatBound"> <div class="info-label">
<el-icon color="#07C160"><ChatDotRound /></el-icon>
微信绑定
</div>
<div class="info-value wechat-binding">
<span v-if="isWechatBound" class="wechat-status">
<el-icon color="#07C160"><SuccessFilled /></el-icon> <el-icon color="#07C160"><SuccessFilled /></el-icon>
已绑定 已绑定
<!-- <span class="wechat-id">({{ maskWechat(wechatInfo) }})</span> -->
</span> </span>
<span v-else class="unset-text"> <span v-else class="wechat-status unset-text">
未绑定 未绑定
</span> </span>
<el-button <el-button
v-if="!isWechatBound" v-if="!isWechatBound"
class="bind-btn" type="success"
type="primary" size="small"
@click="handleWechatBind" @click="handleWechatBind"
> >
绑定 立即绑定
</el-button> </el-button>
</div> </div>
</el-descriptions-item>
<el-descriptions-item label="个人简介">
<span v-if="user.introduction">
{{ user.introduction }}
</span>
<span v-else class="unset-text">暂无简介</span>
</el-descriptions-item>
</el-descriptions>
</div> </div>
<!-- 个人简介 -->
<div class="info-item full-width">
<div class="info-label">
<el-icon><Document /></el-icon>
个人简介
</div>
<div class="info-value">
<span v-if="user.introduction">{{ user.introduction }}</span>
<span v-else class="unset-text">暂无简介</span>
</div>
</div>
</div>
</div>
<!-- 操作按钮预留 -->
<div v-if="false" class="action-section">
<el-button type="primary" @click="handleEdit">
<el-icon><Edit /></el-icon>
编辑资料
</el-button>
</div> </div>
</el-card> </el-card>
@@ -197,21 +264,27 @@ function bindWechat() {
<el-dialog <el-dialog
v-model="wechatDialogVisible" v-model="wechatDialogVisible"
title="微信绑定" title="微信绑定"
width="400px" width="450px"
:close-on-click-modal="false"
> >
<div class="wechat-dialog"> <div class="wechat-dialog">
<div class="wechat-tip">
<el-alert
type="info"
:closable="false"
show-icon
>
<template #title>
<div>请使用微信扫描下方二维码完成绑定</div>
</template>
</el-alert>
</div>
<QrCodeLogin :type="WECHAT_QRCODE_TYPE.Bind" @bind-wechat="bindWechat()" /> <QrCodeLogin :type="WECHAT_QRCODE_TYPE.Bind" @bind-wechat="bindWechat()" />
</div> </div>
<template #footer> <template #footer>
<el-button @click="wechatDialogVisible = false"> <el-button @click="wechatDialogVisible = false">
取消 取消
</el-button> </el-button>
<el-button
type="primary"
@click="wechatDialogVisible = false"
>
关闭
</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@@ -220,28 +293,41 @@ function bindWechat() {
<style scoped> <style scoped>
.user-profile { .user-profile {
padding: 20px; padding: 20px;
height: 100%;
overflow-y: auto;
} }
.profile-card { .header {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px;
} }
.card-header h3 { .header h2 {
display: flex;
align-items: center;
margin: 0; margin: 0;
font-size: 20px;
color: #333; color: #333;
} }
.profile-content { .header .el-icon {
margin-right: 8px;
color: #409eff;
}
.profile-card {
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
/* 用户头部区域 */
.user-header-section {
display: flex; display: flex;
gap: 40px; gap: 30px;
align-items: flex-start; align-items: center;
padding: 20px 0;
} }
.avatar-section { .avatar-section {
@@ -249,33 +335,127 @@ function bindWechat() {
flex-shrink: 0; flex-shrink: 0;
} }
.avatar-wrapper {
position: relative;
display: inline-block;
}
.user-avatar { .user-avatar {
margin-bottom: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-size: 40px; font-size: 48px;
font-weight: bold; font-weight: bold;
border: 4px solid #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.vip-badge {
position: absolute;
bottom: 0;
right: -10px;
background: linear-gradient(135deg, #f5af19 0%, #f12711 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
display: flex;
align-items: center;
gap: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
} }
.avatar-actions { .avatar-actions {
margin-top: 10px; margin-top: 15px;
} }
.info-section { .user-info-quick {
flex: 1; flex: 1;
} }
:deep(.el-descriptions__body) { .user-name {
background-color: #fafafa; font-size: 28px;
font-weight: 600;
color: #333;
margin: 0 0 15px 0;
} }
:deep(.el-descriptions__label) { .user-tags {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.user-stats {
display: flex;
gap: 30px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 16px;
font-weight: 600; font-weight: 600;
width: 100px; color: #409eff;
margin-bottom: 4px;
}
.stat-label {
font-size: 13px;
color: #909399;
}
/* 信息区域 */
.info-section {
padding: 10px 0;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
transition: all 0.3s;
}
.info-item:hover {
background: #e9ecef;
transform: translateY(-2px);
}
.info-item.full-width {
grid-column: 1 / -1;
}
.info-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #666;
font-weight: 500;
}
.info-value {
font-size: 15px;
color: #333;
font-weight: 500;
} }
.unset-text { .unset-text {
color: #999; color: #999;
font-style: italic; font-style: italic;
font-weight: normal;
} }
.wechat-binding { .wechat-binding {
@@ -284,60 +464,50 @@ function bindWechat() {
justify-content: space-between; justify-content: space-between;
} }
.wechat-id { .wechat-status {
color: #666; display: flex;
font-size: 12px; align-items: center;
margin-left: 5px; gap: 6px;
} }
.bind-btn { /* 操作区域 */
margin-left: 10px; .action-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
text-align: center;
} }
/* 微信绑定对话框 */
.wechat-dialog { .wechat-dialog {
text-align: center; text-align: center;
} }
.qrcode-section {
margin: 20px 0;
}
.qrcode-placeholder {
width: 200px;
height: 200px;
margin: 0 auto;
border: 2px dashed #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #999;
}
.qrcode-placeholder .el-icon {
font-size: 48px;
margin-bottom: 10px;
}
.wechat-tip { .wechat-tip {
color: #666; margin-bottom: 20px;
margin-top: 10px;
}
.wechat-info {
color: #07C160;
font-weight: 500;
} }
/* 响应式 */
@media (max-width: 768px) { @media (max-width: 768px) {
.profile-content { .user-header-section {
flex-direction: column; flex-direction: column;
gap: 20px; text-align: center;
} }
.avatar-section { .user-info-quick {
align-self: center; width: 100%;
}
.user-stats {
justify-content: center;
}
.info-grid {
grid-template-columns: 1fr;
}
.info-item.full-width {
grid-column: 1;
} }
} }
</style> </style>

View File

@@ -1,5 +1,8 @@
<!-- 头像 --> <!-- 头像 -->
<script setup lang="ts"> <script setup lang="ts">
import { ChatLineRound } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Popover from '@/components/Popover/index.vue'; import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
@@ -57,26 +60,42 @@ const popoverList = ref([
]); ]);
const dialogVisible = ref(false); const dialogVisible = ref(false);
const rechargeLogRef = ref();
const activeNav = ref('user');
// ============ 邀请码分享功能 ============
/** 从 URL 获取的邀请码 */
const externalInviteCode = ref<string>('');
const navItems = [ const navItems = [
{ name: 'user', label: '用户信息', icon: 'User' }, { name: 'user', label: '用户信息', icon: 'User' },
// { name: 'role', label: '角色管理', icon: 'Avatar' }, // { name: 'role', label: '角色管理', icon: 'Avatar' },
// { name: 'permission', label: '权限管理', icon: 'Key' }, // { name: 'permission', label: '权限管理', icon: 'Key' },
// { name: 'userInfo', label: '用户信息', icon: 'User' }, // { name: 'userInfo', label: '用户信息', icon: 'User' },
{ name: 'apiKey', label: 'API密钥', icon: 'Key' }, { name: 'apiKey', label: 'API密钥', icon: 'Key' },
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' }, { name: 'rechargeLog', label: '充值记录', icon: 'Document' },
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' }, { name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
{ name: 'premiumService', label: '尊享服务', icon: 'ColdDrink' },
{ name: 'dailyTask', label: '每日任务(限时)', icon: 'Trophy' },
{ name: 'cardFlip', label: '每周邀请(限时)', icon: 'Present' },
// { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' }, // { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' },
]; ];
function openDialog() { function openDialog() {
dialogVisible.value = true; dialogVisible.value = true;
} }
function handleConfirm(activeNav: string) { function handleConfirm(activeNav: string) {
console.log('确认操作,当前导航:', activeNav);
ElMessage.success('操作成功'); ElMessage.success('操作成功');
} }
// 导航切换
function handleNavChange(nav: string) { function handleNavChange(nav: string) {
console.log('导航切换:', nav); activeNav.value = nav;
}
// 联系售后
function handleContactSupport() {
rechargeLogRef.value?.contactCustomerService();
} }
// 点击 // 点击
@@ -169,11 +188,108 @@ function openVipGuide() {
function onProductPackage() { function onProductPackage() {
showProductPackage(); showProductPackage();
} }
// 直接调用
// ============ 监听对话框打开事件,切换到邀请码标签页 ============
watch(dialogVisible, (newVal) => {
if (newVal && externalInviteCode.value) {
// 对话框打开后,切换标签页(已通过 :default-active 绑定,会自动响应)
// console.log('[Avatar] watch: 对话框已打开,切换到 cardFlip 标签页');
nextTick(() => {
activeNav.value = 'cardFlip';
// console.log('[Avatar] watch: 已设置 activeNav 为', activeNav.value);
});
}
// 对话框关闭时,清除邀请码状态和 URL 参数
if (!newVal && externalInviteCode.value) {
// console.log('[Avatar] watch: 对话框关闭,清除邀请码状态');
externalInviteCode.value = '';
// 清除 URL 中的 inviteCode 参数
const url = new URL(window.location.href);
if (url.searchParams.has('inviteCode')) {
url.searchParams.delete('inviteCode');
window.history.replaceState({}, '', url.toString());
// console.log('[Avatar] watch: 已清除 URL 中的 inviteCode 参数');
}
}
});
// ============ 监听 URL 参数,实现邀请码快捷分享 ============
onMounted(() => {
// 获取 URL 查询参数
const urlParams = new URLSearchParams(window.location.search);
const inviteCode = urlParams.get('inviteCode');
if (inviteCode && inviteCode.trim()) {
// console.log('[Avatar] onMounted: 检测到邀请码', inviteCode);
// 保存邀请码
externalInviteCode.value = inviteCode.trim();
// 先设置标签页为 cardFlip
activeNav.value = 'cardFlip';
// console.log('[Avatar] onMounted: 设置 activeNav 为', activeNav.value);
// 延迟打开对话框,确保状态已更新
nextTick(() => {
setTimeout(() => {
// console.log('[Avatar] onMounted: 打开用户中心对话框');
dialogVisible.value = true;
}, 200);
});
// 注意:不立即清除 URL 参数,保留给登录后使用
// URL 参数会在对话框关闭时清除
}
});
</script> </script>
<template> <template>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- <div class="text-1.2xl font-bold text-gray-800 hover:text-blue-600 transition-colors"> -->
<!-- <a -->
<!-- href="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde" -->
<!-- target="_blank" -->
<!-- class="flex items-center gap-2 group" -->
<!-- style="color: #E6A23C;" -->
<!-- title="点击跳转YiXinAI玩法指南专栏" -->
<!-- > -->
<!-- AI使用教程 -->
<!-- </a> -->
<!-- </div> -->
<div class="text-1.2xl font-bold text-gray-800 hover:text-blue-600 transition-colors">
<a
href="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde"
target="_blank"
class="flex items-center gap-2 group"
style="color: #E6A23C;"
title="点击跳转YiXinAI玩法指南专栏"
>
<!-- PC端显示文字 -->
<span class="pc-text">AI使用教程</span>
<!-- 移动端显示图标这里用一个示例SVG实际可以换成你想要的 -->
<svg
class="inline md:hidden w-6 h-6 text-yellow-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 14l9-5-9-5-9 5 9 5z"
/>
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 14l6.16-3.422A12.083 12.083 0 0118 13.5c0 2.579-3.582 4.5-6 4.5s-6-1.921-6-4.5c0-.432.075-.85.198-1.244L12 14z"
/>
</svg>
</a>
</div>
<el-button <el-button
class="buy-btn flex items-center gap-2 px-5 py-2 font-semibold shadow-lg" class="buy-btn flex items-center gap-2 px-5 py-2 font-semibold shadow-lg"
@click="onProductPackage" @click="onProductPackage"
@@ -266,9 +382,20 @@ function onProductPackage() {
v-model="dialogVisible" v-model="dialogVisible"
title="用户中心" title="用户中心"
:nav-items="navItems" :nav-items="navItems"
:default-active="activeNav"
@confirm="handleConfirm" @confirm="handleConfirm"
@nav-change="handleNavChange" @nav-change="handleNavChange"
> >
<template #extra-actions>
<el-tooltip v-if="isUserVip() && activeNav === 'rechargeLog'" content="联系售后" placement="bottom">
<el-button circle plain size="small" @click="handleContactSupport">
<el-icon color="#07c160">
<ChatLineRound />
</el-icon>
</el-button>
</el-tooltip>
</template>
<!-- 用户管理内容 --> <!-- 用户管理内容 -->
<template #user> <template #user>
<user-management /> <user-management />
@@ -277,6 +404,10 @@ function onProductPackage() {
<template #usageStatistics> <template #usageStatistics>
<usage-statistics /> <usage-statistics />
</template> </template>
<!-- 尊享服务 -->
<template #premiumService>
<premium-service />
</template>
<!-- 用量统计 --> <!-- 用量统计 -->
<!-- <template #usageStatistics2> --> <!-- <template #usageStatistics2> -->
<!-- <usage-statistics2 /> --> <!-- <usage-statistics2 /> -->
@@ -295,8 +426,14 @@ function onProductPackage() {
<template #apiKey> <template #apiKey>
<APIKeyManagement /> <APIKeyManagement />
</template> </template>
<template #dailyTask>
<daily-task />
</template>
<template #cardFlip>
<card-flip-activity :external-invite-code="externalInviteCode" />
</template>
<template #rechargeLog> <template #rechargeLog>
<recharge-log /> <recharge-log ref="rechargeLogRef" />
</template> </template>
</nav-dialog> </nav-dialog>
</div> </div>
@@ -354,4 +491,22 @@ function onProductPackage() {
0%, 100% { transform: translateY(0); } 0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); } 50% { transform: translateY(-4px); }
} }
/* 默认 PC 端文字显示,图标隐藏 */
.pc-text {
display: inline;
}
.mobile-icon {
display: none;
}
/* 移动端显示图标,隐藏文字 */
@media (max-width: 768px) {
.pc-text {
display: none;
}
.mobile-icon {
display: inline;
}
}
</style> </style>

View File

@@ -45,9 +45,9 @@ const debouncedSend = useDebounceFn(async () => {
}); });
senderValue.value = ''; // 清空输入框 senderValue.value = ''; // 清空输入框
} }
catch (error) { catch (error: any) {
console.error('发送消息失败:', error); console.error('发送消息失败:', error);
ElMessage.error('发送消息失败,请重试'); ElMessage.error(error);
} }
finally { finally {
isSending.value = false; isSending.value = false;

View File

@@ -46,10 +46,13 @@ const inputValue = ref('');
const senderRef = ref<InstanceType<typeof Sender> | null>(null); const senderRef = ref<InstanceType<typeof Sender> | null>(null);
const bubbleItems = ref<MessageItem[]>([]); const bubbleItems = ref<MessageItem[]>([]);
const bubbleListRef = ref<BubbleListInstance | null>(null); const bubbleListRef = ref<BubbleListInstance | null>(null);
const isSending = ref(false);
const { stream, loading: isLoading, cancel } = useHookFetch({ const { stream, loading: isLoading, cancel } = useHookFetch({
request: send, request: send,
onError: async (error) => { onError: async (error) => {
isLoading.value = false;
if (error.status === 403) { if (error.status === 403) {
const data = await (error.response.json()); const data = await (error.response.json());
// 弹窗提示 // 弹窗提示
@@ -168,12 +171,13 @@ function handleDataChunk(chunk: AnyObject) {
function handleError(err: any) { function handleError(err: any) {
console.error('Fetch error:', err); console.error('Fetch error:', err);
} }
const isSending = ref(false);
async function startSSE(chatContent: string) { async function startSSE(chatContent: string) {
if (isSending.value) if (isSending.value)
return; return;
isSending.value = true;
try { try {
// 清空输入框 // 清空输入框
inputValue.value = ''; inputValue.value = '';
@@ -205,14 +209,20 @@ async function startSSE(chatContent: string) {
} }
else { else {
handleError(err); // 其他错误 handleError(err); // 其他错误
// ElMessage.error('消息发送失败,请重试');
} }
} }
finally { finally {
isSending.value = false; isSending.value = false;
// 停止打字器状态 // 停止打字器状态和加载状态
if (bubbleItems.value.length) { if (bubbleItems.value.length) {
bubbleItems.value[bubbleItems.value.length - 1].typing = false; const latest = bubbleItems.value[bubbleItems.value.length - 1];
latest.typing = false;
latest.loading = false;
if (latest.thinkingStatus === 'thinking') {
latest.thinkingStatus = 'end';
}
} }
} }
} }
@@ -221,8 +231,14 @@ async function startSSE(chatContent: string) {
async function cancelSSE() { async function cancelSSE() {
try { try {
cancel(); // 直接调用,无需参数 cancel(); // 直接调用,无需参数
isSending.value = false;
if (bubbleItems.value.length) { if (bubbleItems.value.length) {
bubbleItems.value[bubbleItems.value.length - 1].typing = false; const latest = bubbleItems.value[bubbleItems.value.length - 1];
latest.typing = false;
latest.loading = false;
if (latest.thinkingStatus === 'thinking') {
latest.thinkingStatus = 'end';
}
} }
} }
catch (err) { catch (err) {
@@ -254,7 +270,6 @@ function addMessage(message: string, isUser: boolean) {
// 展开收起 事件展示 // 展开收起 事件展示
function handleChange(payload: { value: boolean; status: ThinkingStatus }) { function handleChange(payload: { value: boolean; status: ThinkingStatus }) {
console.log('value', payload.value, 'status', payload.status);
} }
function handleDeleteCard(_item: FilesCardProps, index: number) { function handleDeleteCard(_item: FilesCardProps, index: number) {
@@ -279,7 +294,6 @@ watch(
// 复制 // 复制
function copy(item: any) { function copy(item: any) {
console.log('复制', item);
navigator.clipboard.writeText(item.content || '') navigator.clipboard.writeText(item.content || '')
.then(() => ElMessage.success('已复制到剪贴板')) .then(() => ElMessage.success('已复制到剪贴板'))
.catch(() => ElMessage.error('复制失败')); .catch(() => ElMessage.error('复制失败'));
@@ -312,7 +326,7 @@ function copy(item: any) {
<div class="footer-container"> <div class="footer-container">
<div class="footer-time"> <div class="footer-time">
<span v-if="item.creationTime "> {{ item.creationTime }}</span> <span v-if="item.creationTime "> {{ item.creationTime }}</span>
<span style="margin-left: 10px;" v-if="((item.role === 'ai' || item.role === 'assistant') && item?.tokenUsage?.total) " class="footer-token"> <span v-if="((item.role === 'ai' || item.role === 'assistant') && item?.tokenUsage?.total) " style="margin-left: 10px;" class="footer-token">
{{ ((item.role === 'ai' || item.role === 'assistant') && item?.tokenUsage?.total) ? `token:${item?.tokenUsage?.total}` : '' }}</span> {{ ((item.role === 'ai' || item.role === 'assistant') && item?.tokenUsage?.total) ? `token:${item?.tokenUsage?.total}` : '' }}</span>
<el-button icon="DocumentCopy" size="small" circle @click="copy(item)" /> <el-button icon="DocumentCopy" size="small" circle @click="copy(item)" />
</div> </div>

View File

@@ -63,7 +63,6 @@ async function onReLogin() {
} }
function handleThirdPartyLogin(type: any) { function handleThirdPartyLogin(type: any) {
const redirectUri = encodeURIComponent(`${window.location.origin}/chat`); const redirectUri = encodeURIComponent(`${window.location.origin}/chat`);
console.log('cccc', type);
const popup = window.open( const popup = window.open(
`${SSO_SEVER_URL}/login?client_id=${type}&redirect_uri=${redirectUri}`, `${SSO_SEVER_URL}/login?client_id=${type}&redirect_uri=${redirectUri}`,
'SSOLogin', 'SSOLogin',
@@ -170,7 +169,8 @@ function contactCustomerService() {
🎉 恭喜 🎉 恭喜
</h1> </h1>
<p class="text-xl font-semibold mb-6"> <p class="text-xl font-semibold mb-6">
您已成为尊贵的 <span class="text-orange-500">YixinAI VIP</span> 您已充值成功
<!-- 您已成为尊贵的 <span class="text-orange-500">YixinAI VIP</span> -->
</p> </p>
<!-- 订单信息卡片 --> <!-- 订单信息卡片 -->

View File

@@ -10,6 +10,9 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default'] AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default']
APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default'] APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default']
CardFlipActivity: typeof import('./../src/components/userPersonalCenter/components/CardFlipActivity.vue')['default']
CardFlipActivity2: typeof import('./../src/components/userPersonalCenter/components/CardFlipActivity2.vue')['default']
DailyTask: typeof import('./../src/components/userPersonalCenter/components/DailyTask.vue')['default']
DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default'] DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert'] ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
@@ -19,8 +22,6 @@ declare module 'vue' {
ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElEmpty: typeof import('element-plus/es')['ElEmpty']
@@ -34,6 +35,7 @@ declare module 'vue' {
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
@@ -46,6 +48,7 @@ declare module 'vue' {
ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default'] ModelSelect: typeof import('./../src/components/ModelSelect/index.vue')['default']
NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default'] NavDialog: typeof import('./../src/components/userPersonalCenter/NavDialog.vue')['default']
Popover: typeof import('./../src/components/Popover/index.vue')['default'] Popover: typeof import('./../src/components/Popover/index.vue')['default']
PremiumService: typeof import('./../src/components/userPersonalCenter/components/PremiumService.vue')['default']
ProductPackage: typeof import('./../src/components/ProductPackage/index.vue')['default'] ProductPackage: typeof import('./../src/components/ProductPackage/index.vue')['default']
QrCodeLogin: typeof import('./../src/components/LoginDialog/components/QrCodeLogin/index.vue')['default'] QrCodeLogin: typeof import('./../src/components/LoginDialog/components/QrCodeLogin/index.vue')['default']
RechargeLog: typeof import('./../src/components/userPersonalCenter/components/RechargeLog.vue')['default'] RechargeLog: typeof import('./../src/components/userPersonalCenter/components/RechargeLog.vue')['default']

View File

@@ -6,7 +6,6 @@ interface ImportMetaEnv {
readonly VITE_WEB_ENV: string; readonly VITE_WEB_ENV: string;
readonly VITE_WEB_BASE_API: string; readonly VITE_WEB_BASE_API: string;
readonly VITE_API_URL: string; readonly VITE_API_URL: string;
readonly VITE_BUILD_COMPRESS: string;
readonly VITE_SSO_SEVER_URL: string; readonly VITE_SSO_SEVER_URL: string;
readonly VITE_APP_VERSION: string; readonly VITE_APP_VERSION: string;
} }