package user import ( "time" "yunyan/comm" "yunyan/lego/sys/log" "yunyan/lego/sys/mysql" "yunyan/pb" ) // @Summary 使用资源请求 // @Description 上报一次用量(翻译秒数 / 会议秒数 / AI 对话字数)。服务端记原始统计,并按后台换算系数折算成算力做记账。 // @Tags User // @Accept json // @Produce json // @Security BearerAuth // @Param user body pb.UserUsagesReq true "使用资源请求" // @Success 200 {object} comm.HttpResult{data=pb.UserUsagesResp} "成功返回" // @Router /api/home/user_usages [post] // // 2026-09 起本接口**不再扣三个额度桶、不再因余额不足拒绝**——功能准入只看 VIP(见 comm.ResolveVip), // 用量统一折算成算力(见 comm.ApplyComputeUsage):先扣设备赠送、再扣用户余额; // 算力闸门(COMPUTE_GATE)默认关,余额不够也放行、差额记为超额。 // // ⚠️ 客户端只上报**原始量**(秒 / 字符数),不上报算力:换算系数在后台可改,客户端自己算的话 // 改一次系数就要发一次版。客户端拿到的算力是本接口**返回**的。 func (this *apiComp) Usages(session comm.IUserSession, req *pb.UserUsagesReq) (resp *pb.UserUsagesResp, errdata *pb.ErrorData) { var ( user *pb.DBUser statistics *pb.DBUserStatistics userlog *pb.DBUserUseLog kind comm.ComputeUsageKind raw int64 err error ) user, err = this.module.model.findforid(session.GetUserId()) if err != nil { errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()} return } statistics, err = this.module.model.getStatistics(session.GetUserId()) if err != nil && err != mysql.ErrNoDocuments { errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()} return } userlog = &pb.DBUserUseLog{ Uid: session.GetUserId(), Logtype: pb.UserLogType_UserConsume, Ts: time.Now().Unix(), } // ── 原始统计照记(现有字段不动,看板与结算还在用) ── switch req.Usagetype { case pb.UsageType_Trade: kind = comm.ComputeUsageTranslate if v, ok := req.Usages["TRANSLATION_MODE_simultaneous"]; ok && v > 0 { statistics.Trademodel1Num += 1 statistics.Trademodel1Time += v raw += v } if v, ok := req.Usages["TRANSLATION_MODE_faceToFace"]; ok && v > 0 { statistics.Trademodel2Num += 1 statistics.Trademodel2Time += v raw += v } if v, ok := req.Usages["TRANSLATION_MODE_call"]; ok && v > 0 { statistics.Trademodel3Num += 1 statistics.Trademodel3Time += v raw += v } if v, ok := req.Usages["TRANSLATION_MODE_audioVideo"]; ok && v > 0 { statistics.Trademodel4Num += 1 statistics.Trademodel4Time += v raw += v } // 原实现把翻译秒数写进了 Addagentintegral(AI 那一列),这里改回正确的列。 userlog.Addtradesecond = -raw case pb.UsageType_Meet: kind = comm.ComputeUsageMeeting if v, ok := req.Usages["MEETING_ASR_AUDIO_SECONDS"]; ok && v > 0 { statistics.Meetnum += 1 statistics.Meettime += v raw = v userlog.Addmeetsecond = -v } case pb.UsageType_AI: kind = comm.ComputeUsageAIChat statistics.Aichatnum += 1 // 这两列名字里带 token,但存的是**字符数**(Unicode 码点),与大模型自己的 tokenizer 无关。 // 字段早就有、一直没人写;老客户端不传字数时 raw=0 → 本次不扣算力、只记一次。 statistics.Aichatuptoken += req.Upchars statistics.Aichatdowntoken += req.Downchars raw = req.Upchars + req.Downchars userlog.Addagentintegral = -1 default: errdata = &pb.ErrorData{Code: pb.ErrorCode_ReqParameterError, Message: "未知的用量类型"} return } // ── 算力记账 ── res, err := comm.ApplyComputeUsage(session.GetUserId(), kind, raw, userlog, statistics) if err != nil { this.module.Error("Usages: 算力记账失败", log.Field{Key: "uid", Value: session.GetUserId()}, log.Field{Key: "kind", Value: int(kind)}, log.Field{Key: "raw", Value: raw}, log.Field{Key: "err", Value: err.Error()}) errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()} return } if res.Rejected { errdata = &pb.ErrorData{Code: pb.ErrorCode_ComputeNotEnough, Message: "算力不足"} return } if err = this.module.model.updateStatistics(statistics); err != nil { errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()} return } if err = this.module.model.addIntegralLog(userlog); err != nil { errdata = &pb.ErrorData{Code: pb.ErrorCode_DBError, Message: err.Error()} return } // 资源消耗埋点(口径不变) if this.module.analyze != nil { switch req.Usagetype { case pb.UsageType_Trade: this.module.analyze.Report(&comm.StatEvent{Type: comm.StatEventTranslate, ProductId: user.Lastbindproductid, ChannelId: user.Lastbindchannelid, Uid: session.GetUserId(), Second: raw}) case pb.UsageType_Meet: this.module.analyze.Report(&comm.StatEvent{Type: comm.StatEventMeeting, ProductId: user.Lastbindproductid, ChannelId: user.Lastbindchannelid, Uid: session.GetUserId(), Second: raw}) case pb.UsageType_AI: this.module.analyze.Report(&comm.StatEvent{Type: comm.StatEventAiChat, ProductId: user.Lastbindproductid, ChannelId: user.Lastbindchannelid, Uid: session.GetUserId()}) } } // 兼容:老客户端按 usages[usagetype] 读「剩余量」。三桶已停用,这里回算力剩余合计而不是 0—— // 老包若在本地拿剩余量做闸门,回 0 会把它锁死。单位对不上是过渡期的已知代价。 left := res.DeviceLeft + res.UserLeft resp = &pb.UserUsagesResp{ Usages: map[int32]int64{int32(req.Usagetype): left}, Compute: res.Compute, Computedeviceleft: res.DeviceLeft, Computeuserleft: res.UserLeft, Over: res.Over, } return }