You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

92 lines
3.8 KiB

package echomeet
import (
"strings"
"time"
"yunyan/comm"
"yunyan/lego/sys/mysql"
"yunyan/pb"
)
// 语音纪要的算力退还(2026-09-25)。
//
// 开始任务(api_starttask)按会议时长扣算力:先扣设备赠送、再扣用户余额,闸门关时不够也放行、
// 差额记超额。之后转写可能失败,失败路径有八处(同步/异步提交、短音频处理、轮询、两个回调、卡住兜底……),
// 以前只有短音频那一处在「退」,而且退进了早已停用的旧会议额度 meetintegral——算力其实没回来。
//
// 现在统一走这里:
// - 扣费明细在开始任务时记到记录上(computeuser / computedevices,见 recordCharge);
// - 任何一处判转写失败,都调 refundRecordCompute 按明细原路退回;
// - 退之前先在库里**原子地认领**明细(带条件把两列清零,影响 1 行才算认领到),
// 多条失败路径同时触发也只会退一次;
// - 超额部分从来没扣过,不在明细里,不会被退成白送的算力。
// recordCharge 把本次实际扣费的明细记到会议记录上(调用方随后 saverecord)。
func recordCharge(rec *pb.DBEchoMeetRecord, res comm.ComputeResult) {
rec.Computeuser = res.User
rec.Computedevices = comm.EncodeDeviceCharges(res.Cuts)
}
// refundRecordCompute 转写失败时按记录上的明细退还算力。**必须在 saverecord 之前调**:
// 它会把 rec 上的明细清零,随后那次保存才不会把已退的明细又写回去。
func (this *modelComp) refundRecordCompute(rec *pb.DBEchoMeetRecord, reason string) {
if rec == nil || (rec.Computeuser == 0 && rec.Computedevices == "") {
return
}
userPart, devices := rec.Computeuser, comm.DecodeDeviceCharges(rec.Computedevices)
// 原子认领:只有把非空明细清零的那一次才去退,别的并发路径影响 0 行、直接跳过。
res := mysql.Table(comm.TableEchomeetRecord).
Where("id = ? AND (computeuser <> 0 OR computedevices <> '')", rec.Id).
Updates(map[string]interface{}{"computeuser": 0, "computedevices": ""})
rec.Computeuser, rec.Computedevices = 0, ""
if res.Error != nil {
this.module.Errorf("算力退还 id:%d 认领明细失败 err:%v(本次不退,避免重复退)", rec.Id, res.Error)
return
}
if res.RowsAffected == 0 {
this.module.Infof("算力退还 id:%d 明细已被其它路径认领,跳过", rec.Id)
return
}
this.refundCharge(rec.Uid, userPart, devices, int64(rec.Seconds), reason, rec.Id)
}
// refundCharge 按明细退还并写一条 ComputeRefund 流水。开始任务中途失败(还没落到记录上)时直接用它。
func (this *modelComp) refundCharge(uid string, userPart int64, devices []comm.DeviceCharge, seconds int64, reason string, recId uint64) {
refunded, err := comm.RefundComputeUsage(uid, userPart, devices)
if err != nil {
this.module.Errorf("算力退还 id:%d uid:%s 失败 err:%v(已退 %d)", recId, uid, err, refunded)
}
if refunded == 0 {
return
}
var devicePart int64
codes := make([]string, 0, len(devices))
for _, d := range devices {
devicePart += d.N
codes = append(codes, d.Code)
}
log := &pb.DBUserUseLog{
Uid: uid,
Logtype: pb.UserLogType_ComputeRefund,
Ts: time.Now().Unix(),
Addmeetsecond: seconds,
Extra: reason,
Compute: refunded,
Computedevice: devicePart,
Computeuser: userPart,
Devicemac: truncate(strings.Join(codes, ","), 50), // 列宽 size:50
}
if lerr := this.addUseLog(log); lerr != nil {
this.module.Warnf("算力退还 id:%d 写流水失败 err:%v", recId, lerr)
}
this.module.Infof("算力退还 id:%d uid:%s 退回 %d(设备 %d / 账户 %d)原因:%s",
recId, uid, refunded, devicePart, userPart, reason)
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}