refactor: 移除会议用户统计相关代码并更新设备认证逻辑
- 移除 `MeetingUserStats` 相关实体、服务和映射器 - 更新 `AndroidPushGrpcService` 和 `AndroidAuthServiceImpl`,添加 `userId` 和 `tenantId` 参数 - 优化 `AndroidDeviceHomeServiceImpl` 中的积分计算逻辑 - 在 `MeetingPointsController` 中新增 `transfer` 方法,支持从公共账户分配积分给个人账户 - 更新 `MeetingPointsLedgerListItemVO` 中的 `pointsType` 描述 - 优化 `AndroidDeviceBindingServiceImpl` 中的设备登录态验证逻辑dev_na
parent
1813b90c6a
commit
11c3ee1b09
|
|
@ -385,19 +385,7 @@
|
||||||
- 类型:`INTEGER`
|
- 类型:`INTEGER`
|
||||||
- 说明:会议最终有效录音时长(秒),作为会议统计与积分计费统一口径。
|
- 说明:会议最终有效录音时长(秒),作为会议统计与积分计费统一口径。
|
||||||
|
|
||||||
### 6.2 `biz_meeting_user_stats`
|
### 6.2 `biz_meeting_points_accounts`
|
||||||
- 用途:按用户维护会议时长统计与总结触发统计账户。
|
|
||||||
- 关键字段:
|
|
||||||
- `tenant_id`
|
|
||||||
- `user_id`
|
|
||||||
- `total_meeting_duration_seconds`
|
|
||||||
- `total_meeting_duration_minutes`
|
|
||||||
- `total_summary_charge_count`
|
|
||||||
- `total_summary_attempt_count`
|
|
||||||
- 关键索引:
|
|
||||||
- `uk_biz_meeting_user_stats_tenant_user`
|
|
||||||
|
|
||||||
### 6.3 `biz_meeting_points_accounts`
|
|
||||||
- 用途:当前版本按租户维护统一积分余额与累计消耗。
|
- 用途:当前版本按租户维护统一积分余额与累计消耗。
|
||||||
- 关键字段:
|
- 关键字段:
|
||||||
- `tenant_id`
|
- `tenant_id`
|
||||||
|
|
@ -411,7 +399,7 @@
|
||||||
- 关键索引:
|
- 关键索引:
|
||||||
- `uk_biz_meeting_points_accounts_tenant_user`
|
- `uk_biz_meeting_points_accounts_tenant_user`
|
||||||
|
|
||||||
### 6.4 `biz_meeting_summary_charge_records`
|
### 6.3 `biz_meeting_summary_charge_records`
|
||||||
- 用途:每次 SUMMARY 任务保留一条计费快照记录,并按 ASR / LLM 成功节点累计实际扣费。
|
- 用途:每次 SUMMARY 任务保留一条计费快照记录,并按 ASR / LLM 成功节点累计实际扣费。
|
||||||
- 关键字段:
|
- 关键字段:
|
||||||
- `meeting_id`
|
- `meeting_id`
|
||||||
|
|
|
||||||
|
|
@ -512,26 +512,6 @@ ALTER TABLE biz_meetings
|
||||||
|
|
||||||
COMMENT ON COLUMN biz_meetings.effective_audio_duration_seconds IS '会议最终有效录音时长(秒),用于统计与计费口径';
|
COMMENT ON COLUMN biz_meetings.effective_audio_duration_seconds IS '会议最终有效录音时长(秒),用于统计与计费口径';
|
||||||
|
|
||||||
DROP TABLE IF EXISTS biz_meeting_user_stats CASCADE;
|
|
||||||
CREATE TABLE biz_meeting_user_stats (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
|
||||||
status INTEGER NOT NULL DEFAULT 1,
|
|
||||||
user_id BIGINT NOT NULL,
|
|
||||||
total_meeting_duration_seconds BIGINT NOT NULL DEFAULT 0,
|
|
||||||
total_meeting_duration_minutes BIGINT NOT NULL DEFAULT 0,
|
|
||||||
total_summary_charge_count BIGINT NOT NULL DEFAULT 0,
|
|
||||||
total_summary_attempt_count BIGINT NOT NULL DEFAULT 0,
|
|
||||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX uk_biz_meeting_user_stats_tenant_user
|
|
||||||
ON biz_meeting_user_stats (tenant_id, user_id);
|
|
||||||
|
|
||||||
COMMENT ON TABLE biz_meeting_user_stats IS '会议用户时长统计账户表';
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS biz_meeting_points_accounts CASCADE;
|
DROP TABLE IF EXISTS biz_meeting_points_accounts CASCADE;
|
||||||
CREATE TABLE biz_meeting_points_accounts (
|
CREATE TABLE biz_meeting_points_accounts (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public final class SysParamKeys {
|
||||||
public static final String MEETING_CREATE_REALTIME_ENABLED = "meeting.create.realtime_enabled";
|
public static final String MEETING_CREATE_REALTIME_ENABLED = "meeting.create.realtime_enabled";
|
||||||
/** ASR 任务最大并发数。 */
|
/** ASR 任务最大并发数。 */
|
||||||
public static final String MEETING_ASR_MAX_CONCURRENT = "meeting.asr.max_concurrent";
|
public static final String MEETING_ASR_MAX_CONCURRENT = "meeting.asr.max_concurrent";
|
||||||
/** 会议暂停最大时长,单位秒。 */
|
/** 会议暂停最长时长,单位秒。 */
|
||||||
public static final String MEETING_MAX_PAUSE_DURATION = "meeting.max_pause_duration";
|
public static final String MEETING_MAX_PAUSE_DURATION = "meeting.max_pause_duration";
|
||||||
/** 单场会议最大时长,单位分钟。 */
|
/** 单场会议最大时长,单位分钟。 */
|
||||||
public static final String MEETING_MAX_MEETING_DURATION = "meeting.max_meeting_duration";
|
public static final String MEETING_MAX_MEETING_DURATION = "meeting.max_meeting_duration";
|
||||||
|
|
@ -38,10 +38,14 @@ public final class SysParamKeys {
|
||||||
public static final String MEETING_POINTS_ASR_RATIO = "meeting.points.asr_ratio";
|
public static final String MEETING_POINTS_ASR_RATIO = "meeting.points.asr_ratio";
|
||||||
/** 积分拆分时分配给 LLM 的比例。 */
|
/** 积分拆分时分配给 LLM 的比例。 */
|
||||||
public static final String MEETING_POINTS_LLM_RATIO = "meeting.points.llm_ratio";
|
public static final String MEETING_POINTS_LLM_RATIO = "meeting.points.llm_ratio";
|
||||||
/** 新建积分账户时的初始积分余额。 */
|
/** 租户初始化公共积分账户时的初始积分余额。 */
|
||||||
public static final String MEETING_POINTS_INITIAL_BALANCE = "meeting.points.initial_balance";
|
public static final String MEETING_POINTS_INITIAL_BALANCE = "meeting.points.initial_balance";
|
||||||
/** 会议积分账户模式,如公共账户或个人账户。 */
|
/** 会议积分账户模式:PUBLIC / PERSONAL / BOTH。 */
|
||||||
public static final String MEETING_POINTS_ACCOUNT_MODE = "meeting.points.account_mode";
|
public static final String MEETING_POINTS_ACCOUNT_MODE = "meeting.points.account_mode";
|
||||||
|
/** 会议积分扣费优先级:PERSONAL_FIRST / PUBLIC_FIRST。 */
|
||||||
|
public static final String MEETING_POINTS_CHARGE_PRIORITY = "meeting.points.charge_priority";
|
||||||
|
/** 会议积分是否启用余额校验。 */
|
||||||
|
public static final String MEETING_POINTS_ENFORCE_BALANCE = "meeting.points.enforce_balance";
|
||||||
/** 临时授权默认下发数量。 */
|
/** 临时授权默认下发数量。 */
|
||||||
public static final String LICENSE_TEMP_DEFAULT_COUNT = "license.temp.default.count";
|
public static final String LICENSE_TEMP_DEFAULT_COUNT = "license.temp.default.count";
|
||||||
/** 临时授权默认有效期,单位月。 */
|
/** 临时授权默认有效期,单位月。 */
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,6 @@ public class AndroidAuthController {
|
||||||
androidDeviceRegistrationService.requireRegistered(deviceId.trim());
|
androidDeviceRegistrationService.requireRegistered(deviceId.trim());
|
||||||
TokenResponse response = authService.login(request, true);
|
TokenResponse response = authService.login(request, true);
|
||||||
if (response != null && response.getUser() != null && response.getCurrentTenantId() != null && StringUtils.hasText(deviceId)) {
|
if (response != null && response.getUser() != null && response.getCurrentTenantId() != null && StringUtils.hasText(deviceId)) {
|
||||||
androidDeviceBindingService.bindPrivateDevice(
|
|
||||||
deviceId.trim(),
|
|
||||||
response.getCurrentTenantId(),
|
|
||||||
response.getUser().getUserId(),
|
|
||||||
appVersion,
|
|
||||||
platform
|
|
||||||
);
|
|
||||||
androidDeviceBindingService.recordLogin(
|
androidDeviceBindingService.recordLogin(
|
||||||
deviceId.trim(),
|
deviceId.trim(),
|
||||||
response.getCurrentTenantId(),
|
response.getCurrentTenantId(),
|
||||||
|
|
|
||||||
|
|
@ -378,17 +378,21 @@ public class AndroidMeetingController {
|
||||||
public ApiResponse<AndroidMeetingConfigVo> config(HttpServletRequest request) {
|
public ApiResponse<AndroidMeetingConfigVo> config(HttpServletRequest request) {
|
||||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "获取会议配置接口");
|
AndroidRequestLogHelper.logRequest(log, "Android会议", "获取会议配置接口");
|
||||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
LoginUser loginUser = AndroidLoginUserSupport.toLoginUser(authContext);
|
||||||
|
Long tenantId = loginUser != null ? loginUser.getTenantId() : authContext.getTenantId();
|
||||||
|
Long userId = loginUser != null ? loginUser.getUserId() : null;
|
||||||
|
boolean isPlatformAdmin = loginUser != null && Boolean.TRUE.equals(loginUser.getIsPlatformAdmin());
|
||||||
|
boolean isTenantAdmin = loginUser != null && Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||||
AndroidMeetingConfigVo resultVo = new AndroidMeetingConfigVo();
|
AndroidMeetingConfigVo resultVo = new AndroidMeetingConfigVo();
|
||||||
PageResult<List<PromptTemplateVO>> promptTemplateList = promptTemplateService.pageTemplates(
|
PageResult<List<PromptTemplateVO>> promptTemplateList = promptTemplateService.pageTemplates(
|
||||||
1,
|
1,
|
||||||
1000,
|
1000,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
loginUser.getTenantId(),
|
tenantId,
|
||||||
loginUser.getUserId(),
|
userId,
|
||||||
loginUser.getIsPlatformAdmin(),
|
isPlatformAdmin,
|
||||||
loginUser.getIsTenantAdmin()
|
isTenantAdmin
|
||||||
);
|
);
|
||||||
List<PromptTemplateVO> enabledTemplates = promptTemplateList.getRecords() == null
|
List<PromptTemplateVO> enabledTemplates = promptTemplateList.getRecords() == null
|
||||||
? List.of()
|
? List.of()
|
||||||
|
|
@ -396,7 +400,7 @@ public class AndroidMeetingController {
|
||||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||||
.toList();
|
.toList();
|
||||||
resultVo.setTemplateList(enabledTemplates);
|
resultVo.setTemplateList(enabledTemplates);
|
||||||
PageResult<List<AiModelVO>> modelList = aiModelService.pageModels(1, 1000, null, "LLM", loginUser.getTenantId());
|
PageResult<List<AiModelVO>> modelList = aiModelService.pageModels(1, 1000, null, "LLM", tenantId);
|
||||||
List<AiModelVO> enabledModels = modelList.getRecords() == null
|
List<AiModelVO> enabledModels = modelList.getRecords() == null
|
||||||
? List.of()
|
? List.of()
|
||||||
: modelList.getRecords().stream()
|
: modelList.getRecords().stream()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.imeeting.controller.biz;
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
import com.imeeting.dto.biz.MeetingPointsBalanceVO;
|
import com.imeeting.dto.biz.MeetingPointsBalanceVO;
|
||||||
|
import com.imeeting.dto.biz.MeetingPointsTransferRequest;
|
||||||
import com.imeeting.service.biz.MeetingPointsService;
|
import com.imeeting.service.biz.MeetingPointsService;
|
||||||
import com.unisbase.common.ApiResponse;
|
import com.unisbase.common.ApiResponse;
|
||||||
import com.unisbase.security.LoginUser;
|
import com.unisbase.security.LoginUser;
|
||||||
|
|
@ -9,6 +10,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
@ -32,15 +35,37 @@ public class MeetingPointsController {
|
||||||
return ApiResponse.ok(meetingPointsService.getBalanceView(loginUser.getTenantId(), targetUserId));
|
return ApiResponse.ok(meetingPointsService.getBalanceView(loginUser.getTenantId(), targetUserId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "从公共账户分配积分给个人账户")
|
||||||
|
@PostMapping("/transfer")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> transferPublicPoints(@RequestBody MeetingPointsTransferRequest request) {
|
||||||
|
LoginUser loginUser = currentLoginUser();
|
||||||
|
ensureAdmin(loginUser);
|
||||||
|
if (request == null) {
|
||||||
|
throw new RuntimeException("分配请求不能为空");
|
||||||
|
}
|
||||||
|
meetingPointsService.transferPublicPointsToUser(
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
request.getTargetUserId(),
|
||||||
|
request.getPoints(),
|
||||||
|
request.getRemark()
|
||||||
|
);
|
||||||
|
return ApiResponse.ok(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
private Long resolveTargetUserId(LoginUser loginUser, Long requestedUserId) {
|
private Long resolveTargetUserId(LoginUser loginUser, Long requestedUserId) {
|
||||||
if (requestedUserId == null || requestedUserId.equals(loginUser.getUserId())) {
|
if (requestedUserId == null || requestedUserId.equals(loginUser.getUserId())) {
|
||||||
return loginUser.getUserId();
|
return loginUser.getUserId();
|
||||||
}
|
}
|
||||||
|
ensureAdmin(loginUser);
|
||||||
|
return requestedUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureAdmin(LoginUser loginUser) {
|
||||||
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
throw new RuntimeException("无权查看其他用户积分余额");
|
throw new RuntimeException("无权限执行该操作");
|
||||||
}
|
}
|
||||||
return requestedUserId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoginUser currentLoginUser() {
|
private LoginUser currentLoginUser() {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ public class MeetingPointsBalanceVO {
|
||||||
@Schema(description = "目标用户ID")
|
@Schema(description = "目标用户ID")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
@Schema(description = "当前优先扣费账户类型:PUBLIC / PERSONAL")
|
@Schema(description = "当前扣费模式:PUBLIC / PERSONAL / BOTH")
|
||||||
private String preferredAccountMode;
|
private String accountMode;
|
||||||
|
|
||||||
|
@Schema(description = "当前扣费优先级:PERSONAL_FIRST / PUBLIC_FIRST")
|
||||||
|
private String chargePriority;
|
||||||
|
|
||||||
@Schema(description = "公共账户余额")
|
@Schema(description = "公共账户余额")
|
||||||
private Long publicBalance;
|
private Long publicBalance;
|
||||||
|
|
@ -26,4 +29,7 @@ public class MeetingPointsBalanceVO {
|
||||||
|
|
||||||
@Schema(description = "个人账户累计消耗积分")
|
@Schema(description = "个人账户累计消耗积分")
|
||||||
private Long personalTotalPointsUsed;
|
private Long personalTotalPointsUsed;
|
||||||
|
|
||||||
|
@Schema(description = "当前模式下可用总积分")
|
||||||
|
private Long totalAvailableBalance;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "积分流水详情")
|
@Schema(description = "积分流水详情")
|
||||||
|
|
@ -26,13 +27,13 @@ public class MeetingPointsLedgerDetailVO {
|
||||||
@Schema(description = "消耗归属用户名")
|
@Schema(description = "消耗归属用户名")
|
||||||
private String ownerUserName;
|
private String ownerUserName;
|
||||||
|
|
||||||
@Schema(description = "账户类型:PUBLIC / PERSONAL")
|
@Schema(description = "当前流水账户类型:PUBLIC / PERSONAL")
|
||||||
private String chargeAccountType;
|
private String chargeAccountType;
|
||||||
|
|
||||||
@Schema(description = "账户用户ID")
|
@Schema(description = "当前流水账户用户ID")
|
||||||
private Long chargeAccountUserId;
|
private Long chargeAccountUserId;
|
||||||
|
|
||||||
@Schema(description = "消耗类型")
|
@Schema(description = "积分类型")
|
||||||
private String pointsType;
|
private String pointsType;
|
||||||
|
|
||||||
@Schema(description = "消耗积分,展示为正数")
|
@Schema(description = "消耗积分,展示为正数")
|
||||||
|
|
@ -100,4 +101,7 @@ public class MeetingPointsLedgerDetailVO {
|
||||||
|
|
||||||
@Schema(description = "记录创建时间")
|
@Schema(description = "记录创建时间")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "本次总结的扣费分摊明细")
|
||||||
|
private List<MeetingPointsChargeItemVO> chargeItems;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ public class MeetingPointsLedgerListItemVO {
|
||||||
@Schema(description = "账户类型:PUBLIC / PERSONAL")
|
@Schema(description = "账户类型:PUBLIC / PERSONAL")
|
||||||
private String chargeAccountType;
|
private String chargeAccountType;
|
||||||
|
|
||||||
@Schema(description = "消耗类型:ASR / LLM / INIT / RECHARGE")
|
@Schema(description = "消耗类型:ASR / LLM")
|
||||||
private String pointsType;
|
private String pointsType;
|
||||||
|
|
||||||
@Schema(description = "消耗积分,展示为正数")
|
@Schema(description = "消耗积分,展示为正数")
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,27 @@ import lombok.Data;
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "积分管理总览视图")
|
@Schema(description = "积分管理总览视图")
|
||||||
public class MeetingPointsOverviewVO {
|
public class MeetingPointsOverviewVO {
|
||||||
@Schema(description = "当前结算模式:PUBLIC / PERSONAL")
|
@Schema(description = "当前扣费模式:PUBLIC / PERSONAL / BOTH")
|
||||||
private String accountMode;
|
private String accountMode;
|
||||||
|
|
||||||
|
@Schema(description = "当前扣费优先级:PERSONAL_FIRST / PUBLIC_FIRST")
|
||||||
|
private String chargePriority;
|
||||||
|
|
||||||
@Schema(description = "公共账户余额")
|
@Schema(description = "公共账户余额")
|
||||||
private Long publicBalance;
|
private Long publicBalance;
|
||||||
|
|
||||||
@Schema(description = "公共账户累计消耗积分")
|
@Schema(description = "公共账户累计消耗积分")
|
||||||
private Long publicTotalPointsUsed;
|
private Long publicTotalPointsUsed;
|
||||||
|
|
||||||
|
@Schema(description = "个人账户余额汇总")
|
||||||
|
private Long personalBalance;
|
||||||
|
|
||||||
|
@Schema(description = "个人账户累计消耗积分汇总")
|
||||||
|
private Long personalTotalPointsUsed;
|
||||||
|
|
||||||
|
@Schema(description = "当前模式下可用总积分")
|
||||||
|
private Long totalAvailableBalance;
|
||||||
|
|
||||||
@Schema(description = "累计消耗次数")
|
@Schema(description = "累计消耗次数")
|
||||||
private Long totalChargeCount;
|
private Long totalChargeCount;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package com.imeeting.entity.biz;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import com.unisbase.entity.BaseEntity;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@Schema(description = "会议用户时长统计账户实体")
|
|
||||||
@TableName("biz_meeting_user_stats")
|
|
||||||
public class MeetingUserStats extends BaseEntity {
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
|
||||||
@Schema(description = "主键ID")
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Schema(description = "用户ID")
|
|
||||||
private Long userId;
|
|
||||||
|
|
||||||
@Schema(description = "累计会议有效时长(秒)")
|
|
||||||
private Long totalMeetingDurationSeconds;
|
|
||||||
|
|
||||||
@Schema(description = "累计会议有效时长(分钟)")
|
|
||||||
private Long totalMeetingDurationMinutes;
|
|
||||||
|
|
||||||
@Schema(description = "累计成功创建总结任务次数")
|
|
||||||
private Long totalSummaryChargeCount;
|
|
||||||
|
|
||||||
@Schema(description = "累计总结尝试次数,包含失败拦截")
|
|
||||||
private Long totalSummaryAttemptCount;
|
|
||||||
}
|
|
||||||
|
|
@ -114,14 +114,16 @@ public class AndroidPushGrpcService extends PushServiceGrpc.PushServiceImplBase
|
||||||
AndroidAuthContext authContext = androidAuthService.authenticateGrpc(
|
AndroidAuthContext authContext = androidAuthService.authenticateGrpc(
|
||||||
request.getDeviceId(),
|
request.getDeviceId(),
|
||||||
request.getAppVersion(),
|
request.getAppVersion(),
|
||||||
resolvePlatform(request.getPlatform())
|
resolvePlatform(request.getPlatform()),
|
||||||
|
request.getUserId(),
|
||||||
|
request.getTenantId()
|
||||||
);
|
);
|
||||||
|
deviceOnlineManagementService.recordConnected(authContext);
|
||||||
AndroidDeviceSessionState sessionState = androidDeviceSessionService.openSession(authContext, request.getConnectionId());
|
AndroidDeviceSessionState sessionState = androidDeviceSessionService.openSession(authContext, request.getConnectionId());
|
||||||
connectionId = sessionState.getConnectionId();
|
connectionId = sessionState.getConnectionId();
|
||||||
deviceId = sessionState.getDeviceId();
|
deviceId = sessionState.getDeviceId();
|
||||||
appVersion = authContext.getAppVersion();
|
appVersion = authContext.getAppVersion();
|
||||||
platform = authContext.getPlatform();
|
platform = authContext.getPlatform();
|
||||||
deviceOnlineManagementService.recordConnected(authContext);
|
|
||||||
connected = true;
|
connected = true;
|
||||||
String replacedConnectionId = androidGatewayPushService.register(
|
String replacedConnectionId = androidGatewayPushService.register(
|
||||||
connectionId,
|
connectionId,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.imeeting.mapper.biz;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.biz.MeetingUserStats;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface MeetingUserStatsMapper extends BaseMapper<MeetingUserStats> {
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,7 @@ import com.imeeting.dto.android.AndroidAuthContext;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
public interface AndroidAuthService {
|
public interface AndroidAuthService {
|
||||||
AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform);
|
AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform, String userId, String tenantId);
|
||||||
|
|
||||||
AndroidAuthContext authenticateHttp(HttpServletRequest request);
|
AndroidAuthContext authenticateHttp(HttpServletRequest request);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,259 +25,282 @@ import org.springframework.util.StringUtils;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AndroidAuthServiceImpl implements AndroidAuthService {
|
public class AndroidAuthServiceImpl implements AndroidAuthService {
|
||||||
|
|
||||||
private static final String HEADER_DEVICE_ID = "X-Android-Device-Id";
|
private static final String HEADER_DEVICE_ID = "X-Android-Device-Id";
|
||||||
private static final String HEADER_APP_ID = "X-Android-App-Id";
|
private static final String HEADER_APP_ID = "X-Android-App-Id";
|
||||||
private static final String HEADER_APP_VERSION = "X-Android-App-Version";
|
private static final String HEADER_APP_VERSION = "X-Android-App-Version";
|
||||||
private static final String HEADER_PLATFORM = "X-Android-Platform";
|
private static final String HEADER_PLATFORM = "X-Android-Platform";
|
||||||
private static final String HEADER_AUTHORIZATION = "Authorization";
|
private static final String HEADER_AUTHORIZATION = "Authorization";
|
||||||
private static final String BEARER_PREFIX = "Bearer ";
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
|
||||||
private final AndroidGrpcAuthProperties properties;
|
private final AndroidGrpcAuthProperties properties;
|
||||||
private final TokenValidationService tokenValidationService;
|
private final TokenValidationService tokenValidationService;
|
||||||
private final DeviceInfoMapper deviceInfoMapper;
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
private final AndroidDeviceBindingService androidDeviceBindingService;
|
private final AndroidDeviceBindingService androidDeviceBindingService;
|
||||||
private final LicenseService licenseService;
|
private final LicenseService licenseService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform) {
|
public AndroidAuthContext authenticateGrpc(String deviceId, String appVersion, String platform, String userId, String tenantId) {
|
||||||
if (properties.isEnabled() && !properties.isAllowAnonymous()) {
|
if (properties.isEnabled() && !properties.isAllowAnonymous()) {
|
||||||
throw new RuntimeException("Android gRPC push does not allow anonymous access");
|
throw new RuntimeException("Android gRPC push does not allow anonymous access");
|
||||||
}
|
}
|
||||||
LicenseEntity license = licenseService.requireValidBoundLicense(deviceId);
|
LicenseEntity license = licenseService.requireValidBoundLicense(deviceId);
|
||||||
DeviceInfoEntity device = requireRegisteredDevice(deviceId);
|
DeviceInfoEntity device = requireRegisteredDevice(deviceId);
|
||||||
assertDeviceEnabled(device);
|
assertDeviceEnabled(device);
|
||||||
AndroidAuthContext context = buildContext("NONE", true, deviceId, null, appVersion, platform, null, null, null, null);
|
Long requestedUserId = parseOptionalLong(userId, "Android gRPC userId");
|
||||||
context.setUserId(device.getUserId());
|
Long requestedTenantId = parseOptionalLong(tenantId, "Android gRPC tenantId");
|
||||||
context.setTenantId(license.getTenantId());
|
boolean anonymous = requestedUserId == null;
|
||||||
return context;
|
AndroidAuthContext context = buildContext(anonymous ? "NONE" : "GRPC_USER", anonymous, deviceId, null, appVersion, platform, null, null, null, null);
|
||||||
|
Long resolvedTenantId = requestedTenantId != null ? requestedTenantId : license.getTenantId();
|
||||||
|
if (requestedUserId != null) {
|
||||||
|
if (requestedTenantId != null && !requestedTenantId.equals(license.getTenantId())) {
|
||||||
|
throw new RuntimeException("登录租户与授权租户不一致,无法绑定grpc");
|
||||||
|
}
|
||||||
|
context.setUserId(requestedUserId);
|
||||||
|
context.setTenantId(resolvedTenantId);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
context.setUserId(null);
|
||||||
|
context.setTenantId(resolvedTenantId);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request) {
|
||||||
|
return authenticateHttp(request, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered) {
|
||||||
|
return authenticateHttp(request, requireRegistered, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered, boolean allowOptionalToken) {
|
||||||
|
LoginUser loginUser = currentLoginUser();
|
||||||
|
String resolvedToken = resolveHttpToken(request);
|
||||||
|
String deviceId = firstHeader(request, HEADER_DEVICE_ID);
|
||||||
|
String appId = request.getHeader(HEADER_APP_ID);
|
||||||
|
String appVersion = firstHeader(request, HEADER_APP_VERSION);
|
||||||
|
String platform = request.getHeader(HEADER_PLATFORM);
|
||||||
|
|
||||||
|
requireAndroidHttpHeaders(deviceId, appVersion, platform);
|
||||||
|
log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}", deviceId, appVersion, platform);
|
||||||
|
|
||||||
|
DeviceInfoEntity device = requireRegistered ? requireRegisteredDevice(deviceId) : findDevice(deviceId);
|
||||||
|
assertDeviceEnabled(device);
|
||||||
|
LicenseEntity license = requireRegistered ? licenseService.requireValidBoundLicense(deviceId) : null;
|
||||||
|
|
||||||
|
if (loginUser != null) {
|
||||||
|
if (!allowOptionalToken) {
|
||||||
|
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, loginUser.getTenantId(), loginUser.getUserId());
|
||||||
|
}
|
||||||
|
AndroidAuthContext context = buildContext("USER_JWT", false,
|
||||||
|
deviceId,
|
||||||
|
appId,
|
||||||
|
appVersion,
|
||||||
|
platform,
|
||||||
|
resolvedToken,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
loginUser);
|
||||||
|
return applyLicenseContext(context, license, allowOptionalToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
if (StringUtils.hasText(resolvedToken)) {
|
||||||
public AndroidAuthContext authenticateHttp(HttpServletRequest request) {
|
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
||||||
return authenticateHttp(request, true, false);
|
if (requireRegistered && !allowOptionalToken) {
|
||||||
|
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, authResult.getTenantId(), authResult.getUserId());
|
||||||
|
}
|
||||||
|
AndroidAuthContext context = buildContext("USER_JWT", false,
|
||||||
|
deviceId,
|
||||||
|
appId,
|
||||||
|
appVersion,
|
||||||
|
platform,
|
||||||
|
resolvedToken,
|
||||||
|
null,
|
||||||
|
authResult,
|
||||||
|
null);
|
||||||
|
return applyLicenseContext(context, license, allowOptionalToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
if (properties.isAllowAnonymous()) {
|
||||||
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered) {
|
AndroidAuthContext context = buildContext("NONE", true,
|
||||||
return authenticateHttp(request, requireRegistered, false);
|
deviceId,
|
||||||
|
appId,
|
||||||
|
appVersion,
|
||||||
|
platform,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
return applyLicenseContext(context, license, allowOptionalToken);
|
||||||
}
|
}
|
||||||
|
throw new RuntimeException("Missing Android HTTP access token");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
private AndroidAuthContext applyLicenseContext(AndroidAuthContext context, LicenseEntity license, boolean allowOptionalToken) {
|
||||||
public AndroidAuthContext authenticateHttp(HttpServletRequest request, boolean requireRegistered, boolean allowOptionalToken) {
|
if (context == null) {
|
||||||
LoginUser loginUser = currentLoginUser();
|
return null;
|
||||||
String resolvedToken = resolveHttpToken(request);
|
|
||||||
String deviceId = firstHeader(request, HEADER_DEVICE_ID);
|
|
||||||
String appId = request.getHeader(HEADER_APP_ID);
|
|
||||||
String appVersion = firstHeader(request, HEADER_APP_VERSION);
|
|
||||||
String platform = request.getHeader(HEADER_PLATFORM);
|
|
||||||
|
|
||||||
requireAndroidHttpHeaders(deviceId, appVersion, platform);
|
|
||||||
log.info("[安卓接口访问]X-Android-Device-Id={},X-Android-App-Version={},X-Android-Platform={}", deviceId, appVersion, platform);
|
|
||||||
|
|
||||||
DeviceInfoEntity device = requireRegistered ? requireRegisteredDevice(deviceId) : findDevice(deviceId);
|
|
||||||
assertDeviceEnabled(device);
|
|
||||||
LicenseEntity license = requireRegistered ? licenseService.requireValidBoundLicense(deviceId) : null;
|
|
||||||
|
|
||||||
if (loginUser != null) {
|
|
||||||
if (!allowOptionalToken) {
|
|
||||||
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, loginUser.getTenantId(), loginUser.getUserId());
|
|
||||||
}
|
|
||||||
AndroidAuthContext context = buildContext("USER_JWT", false,
|
|
||||||
deviceId,
|
|
||||||
appId,
|
|
||||||
appVersion,
|
|
||||||
platform,
|
|
||||||
resolvedToken,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
loginUser);
|
|
||||||
return applyLicenseContext(context, license, allowOptionalToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.hasText(resolvedToken)) {
|
|
||||||
InternalAuthCheckResponse authResult = validateToken(resolvedToken);
|
|
||||||
if (requireRegistered && !allowOptionalToken) {
|
|
||||||
androidDeviceBindingService.validatePrivateDeviceAccess(deviceId, authResult.getTenantId(), authResult.getUserId());
|
|
||||||
}
|
|
||||||
AndroidAuthContext context = buildContext("USER_JWT", false,
|
|
||||||
deviceId,
|
|
||||||
appId,
|
|
||||||
appVersion,
|
|
||||||
platform,
|
|
||||||
resolvedToken,
|
|
||||||
null,
|
|
||||||
authResult,
|
|
||||||
null);
|
|
||||||
return applyLicenseContext(context, license, allowOptionalToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (properties.isAllowAnonymous()) {
|
|
||||||
AndroidAuthContext context = buildContext("NONE", true,
|
|
||||||
deviceId,
|
|
||||||
appId,
|
|
||||||
appVersion,
|
|
||||||
platform,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null);
|
|
||||||
return applyLicenseContext(context, license, allowOptionalToken);
|
|
||||||
}
|
|
||||||
throw new RuntimeException("Missing Android HTTP access token");
|
|
||||||
}
|
}
|
||||||
|
if (license == null) {
|
||||||
private AndroidAuthContext applyLicenseContext(AndroidAuthContext context, LicenseEntity license, boolean allowOptionalToken) {
|
return context;
|
||||||
if (context == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (license == null) {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
Long currentTenantId = context.getTenantId();
|
|
||||||
context.setTenantId(license.getTenantId());
|
|
||||||
if (allowOptionalToken && context.getUserId() != null && currentTenantId != null && !currentTenantId.equals(license.getTenantId())) {
|
|
||||||
context.setAnonymous(true);
|
|
||||||
context.setAuthMode("NONE");
|
|
||||||
context.setUserId(null);
|
|
||||||
context.setUsername(null);
|
|
||||||
context.setDisplayName(null);
|
|
||||||
context.setPlatformAdmin(null);
|
|
||||||
context.setTenantAdmin(null);
|
|
||||||
context.setPermissions(null);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
Long currentTenantId = context.getTenantId();
|
||||||
private AndroidAuthContext buildContext(String authMode, boolean anonymous, String deviceId,
|
context.setTenantId(license.getTenantId());
|
||||||
String appId, String appVersion, String platform, String accessToken,
|
if (allowOptionalToken && context.getUserId() != null && currentTenantId != null && !currentTenantId.equals(license.getTenantId())) {
|
||||||
String fallbackDeviceId, InternalAuthCheckResponse authResult, LoginUser loginUser) {
|
context.setAnonymous(true);
|
||||||
String resolvedDeviceId = StringUtils.hasText(deviceId) ? deviceId : fallbackDeviceId;
|
context.setAuthMode("NONE");
|
||||||
if (!StringUtils.hasText(resolvedDeviceId)) {
|
context.setUserId(null);
|
||||||
throw new RuntimeException("Missing Android deviceId");
|
context.setUsername(null);
|
||||||
}
|
context.setDisplayName(null);
|
||||||
AndroidAuthContext context = new AndroidAuthContext();
|
context.setPlatformAdmin(null);
|
||||||
context.setAuthMode(authMode);
|
context.setTenantAdmin(null);
|
||||||
context.setAnonymous(anonymous);
|
context.setPermissions(null);
|
||||||
context.setDeviceId(resolvedDeviceId.trim());
|
|
||||||
context.setAppId(StringUtils.hasText(appId) ? appId.trim() : null);
|
|
||||||
context.setAppVersion(StringUtils.hasText(appVersion) ? appVersion.trim() : null);
|
|
||||||
context.setPlatform(StringUtils.hasText(platform) ? platform.trim() : "android");
|
|
||||||
context.setAccessToken(StringUtils.hasText(accessToken) ? accessToken.trim() : null);
|
|
||||||
applyIdentity(context, authResult, loginUser);
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
private void applyIdentity(AndroidAuthContext context, InternalAuthCheckResponse authResult, LoginUser loginUser) {
|
private AndroidAuthContext buildContext(String authMode, boolean anonymous, String deviceId,
|
||||||
if (loginUser != null) {
|
String appId, String appVersion, String platform, String accessToken,
|
||||||
context.setUserId(loginUser.getUserId());
|
String fallbackDeviceId, InternalAuthCheckResponse authResult, LoginUser loginUser) {
|
||||||
context.setTenantId(loginUser.getTenantId());
|
String resolvedDeviceId = StringUtils.hasText(deviceId) ? deviceId : fallbackDeviceId;
|
||||||
context.setUsername(loginUser.getUsername());
|
if (!StringUtils.hasText(resolvedDeviceId)) {
|
||||||
context.setDisplayName(loginUser.getDisplayName());
|
throw new RuntimeException("Missing Android deviceId");
|
||||||
context.setPlatformAdmin(Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()));
|
|
||||||
context.setTenantAdmin(Boolean.TRUE.equals(loginUser.getIsTenantAdmin()));
|
|
||||||
context.setPermissions(loginUser.getPermissions());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (authResult == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.setUserId(authResult.getUserId());
|
|
||||||
context.setTenantId(authResult.getTenantId());
|
|
||||||
context.setUsername(authResult.getUsername());
|
|
||||||
context.setDisplayName(authResult.getUsername());
|
|
||||||
context.setPlatformAdmin(Boolean.TRUE.equals(authResult.getPlatformAdmin()));
|
|
||||||
context.setTenantAdmin(Boolean.TRUE.equals(authResult.getTenantAdmin()));
|
|
||||||
context.setPermissions(authResult.getPermissions());
|
|
||||||
}
|
}
|
||||||
|
AndroidAuthContext context = new AndroidAuthContext();
|
||||||
|
context.setAuthMode(authMode);
|
||||||
|
context.setAnonymous(anonymous);
|
||||||
|
context.setDeviceId(resolvedDeviceId.trim());
|
||||||
|
context.setAppId(StringUtils.hasText(appId) ? appId.trim() : null);
|
||||||
|
context.setAppVersion(StringUtils.hasText(appVersion) ? appVersion.trim() : null);
|
||||||
|
context.setPlatform(StringUtils.hasText(platform) ? platform.trim() : "android");
|
||||||
|
context.setAccessToken(StringUtils.hasText(accessToken) ? accessToken.trim() : null);
|
||||||
|
applyIdentity(context, authResult, loginUser);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
private InternalAuthCheckResponse validateToken(String token) {
|
private void applyIdentity(AndroidAuthContext context, InternalAuthCheckResponse authResult, LoginUser loginUser) {
|
||||||
String resolvedToken = normalizeToken(token);
|
if (loginUser != null) {
|
||||||
if (!StringUtils.hasText(resolvedToken)) {
|
context.setUserId(loginUser.getUserId());
|
||||||
throw new RuntimeException("Missing Android access token");
|
context.setTenantId(loginUser.getTenantId());
|
||||||
}
|
context.setUsername(loginUser.getUsername());
|
||||||
InternalAuthCheckResponse authResult = tokenValidationService.validateAccessToken(resolvedToken);
|
context.setDisplayName(loginUser.getDisplayName());
|
||||||
if (authResult == null || !authResult.isValid()) {
|
context.setPlatformAdmin(Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()));
|
||||||
throw new RuntimeException(authResult == null || !StringUtils.hasText(authResult.getMessage()) ? "Android access token is invalid" : authResult.getMessage());
|
context.setTenantAdmin(Boolean.TRUE.equals(loginUser.getIsTenantAdmin()));
|
||||||
}
|
context.setPermissions(loginUser.getPermissions());
|
||||||
if (authResult.getUserId() == null || authResult.getTenantId() == null) {
|
return;
|
||||||
throw new RuntimeException("Android access token is missing user or tenant context");
|
|
||||||
}
|
|
||||||
return authResult;
|
|
||||||
}
|
}
|
||||||
|
if (authResult == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.setUserId(authResult.getUserId());
|
||||||
|
context.setTenantId(authResult.getTenantId());
|
||||||
|
context.setUsername(authResult.getUsername());
|
||||||
|
context.setDisplayName(authResult.getUsername());
|
||||||
|
context.setPlatformAdmin(Boolean.TRUE.equals(authResult.getPlatformAdmin()));
|
||||||
|
context.setTenantAdmin(Boolean.TRUE.equals(authResult.getTenantAdmin()));
|
||||||
|
context.setPermissions(authResult.getPermissions());
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveHttpToken(HttpServletRequest request) {
|
private InternalAuthCheckResponse validateToken(String token) {
|
||||||
String authorization = request.getHeader(HEADER_AUTHORIZATION);
|
String resolvedToken = normalizeToken(token);
|
||||||
if (!StringUtils.hasText(authorization)) {
|
if (!StringUtils.hasText(resolvedToken)) {
|
||||||
return null;
|
throw new RuntimeException("Missing Android access token");
|
||||||
}
|
|
||||||
if (!authorization.startsWith(BEARER_PREFIX)) {
|
|
||||||
throw new RuntimeException("Android HTTP access token format is invalid");
|
|
||||||
}
|
|
||||||
return authorization.substring(BEARER_PREFIX.length()).trim();
|
|
||||||
}
|
}
|
||||||
|
InternalAuthCheckResponse authResult = tokenValidationService.validateAccessToken(resolvedToken);
|
||||||
|
if (authResult == null || !authResult.isValid()) {
|
||||||
|
throw new RuntimeException(authResult == null || !StringUtils.hasText(authResult.getMessage()) ? "Android access token is invalid" : authResult.getMessage());
|
||||||
|
}
|
||||||
|
if (authResult.getUserId() == null || authResult.getTenantId() == null) {
|
||||||
|
throw new RuntimeException("Android access token is missing user or tenant context");
|
||||||
|
}
|
||||||
|
return authResult;
|
||||||
|
}
|
||||||
|
|
||||||
private String firstHeader(HttpServletRequest request, String... names) {
|
private Long parseOptionalLong(String value, String fieldName) {
|
||||||
for (String name : names) {
|
if (!StringUtils.hasText(value)) {
|
||||||
String value = request.getHeader(name);
|
return null;
|
||||||
if (StringUtils.hasText(value)) {
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
return Long.valueOf(value.trim());
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
throw new RuntimeException(fieldName + " format is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void requireAndroidHttpHeaders(String deviceId, String appVersion, String platform) {
|
private String resolveHttpToken(HttpServletRequest request) {
|
||||||
if (!StringUtils.hasText(deviceId)) {
|
String authorization = request.getHeader(HEADER_AUTHORIZATION);
|
||||||
throw new RuntimeException("Missing Android device_id");
|
if (!StringUtils.hasText(authorization)) {
|
||||||
}
|
return null;
|
||||||
if (!StringUtils.hasText(appVersion)) {
|
|
||||||
throw new RuntimeException("Missing X-Android-App-Version header");
|
|
||||||
}
|
|
||||||
if (!StringUtils.hasText(platform)) {
|
|
||||||
throw new RuntimeException("Missing X-Android-Platform header");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!authorization.startsWith(BEARER_PREFIX)) {
|
||||||
|
throw new RuntimeException("Android HTTP access token format is invalid");
|
||||||
|
}
|
||||||
|
return authorization.substring(BEARER_PREFIX.length()).trim();
|
||||||
|
}
|
||||||
|
|
||||||
private void assertDeviceEnabled(DeviceInfoEntity device) {
|
private String firstHeader(HttpServletRequest request, String... names) {
|
||||||
if (device != null && device.getStatus() != null && device.getStatus() == 0) {
|
for (String name : names) {
|
||||||
throw new BusinessException("403", "设备被禁用");
|
String value = request.getHeader(name);
|
||||||
}
|
if (StringUtils.hasText(value)) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private DeviceInfoEntity requireRegisteredDevice(String deviceId) {
|
private void requireAndroidHttpHeaders(String deviceId, String appVersion, String platform) {
|
||||||
DeviceInfoEntity device = findDevice(deviceId);
|
if (!StringUtils.hasText(deviceId)) {
|
||||||
if (device == null) {
|
throw new RuntimeException("Missing Android device_id");
|
||||||
throw new RuntimeException("设备未注册,请先调用设备注册接口");
|
|
||||||
}
|
|
||||||
return device;
|
|
||||||
}
|
}
|
||||||
|
if (!StringUtils.hasText(appVersion)) {
|
||||||
|
throw new RuntimeException("Missing X-Android-App-Version header");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(platform)) {
|
||||||
|
throw new RuntimeException("Missing X-Android-Platform header");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private DeviceInfoEntity findDevice(String deviceId) {
|
private void assertDeviceEnabled(DeviceInfoEntity device) {
|
||||||
if (!StringUtils.hasText(deviceId)) {
|
if (device != null && device.getStatus() != null && device.getStatus() == 0) {
|
||||||
return null;
|
throw new BusinessException("403", "设备被禁用");
|
||||||
}
|
|
||||||
return deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId.trim());
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeToken(String token) {
|
private DeviceInfoEntity requireRegisteredDevice(String deviceId) {
|
||||||
if (!StringUtils.hasText(token)) {
|
DeviceInfoEntity device = findDevice(deviceId);
|
||||||
return null;
|
if (device == null) {
|
||||||
}
|
throw new RuntimeException("设备未注册,请先调用设备注册接口");
|
||||||
String resolved = token.trim();
|
|
||||||
if (resolved.startsWith(BEARER_PREFIX)) {
|
|
||||||
resolved = resolved.substring(BEARER_PREFIX.length()).trim();
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
}
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
private LoginUser currentLoginUser() {
|
private DeviceInfoEntity findDevice(String deviceId) {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
if (!StringUtils.hasText(deviceId)) {
|
||||||
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser loginUser)) {
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (loginUser.getUserId() == null || loginUser.getTenantId() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return loginUser;
|
|
||||||
}
|
}
|
||||||
|
return deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeToken(String token) {
|
||||||
|
if (!StringUtils.hasText(token)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String resolved = token.trim();
|
||||||
|
if (resolved.startsWith(BEARER_PREFIX)) {
|
||||||
|
resolved = resolved.substring(BEARER_PREFIX.length()).trim();
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginUser currentLoginUser() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser loginUser)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (loginUser.getUserId() == null || loginUser.getTenantId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return loginUser;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package com.imeeting.service.android.impl;
|
package com.imeeting.service.android.impl;
|
||||||
|
|
||||||
import com.imeeting.mapper.DeviceInfoMapper;
|
|
||||||
import com.imeeting.mapper.DeviceLoginLogMapper;
|
|
||||||
import com.imeeting.entity.biz.DeviceInfoEntity;
|
import com.imeeting.entity.biz.DeviceInfoEntity;
|
||||||
import com.imeeting.entity.biz.DeviceLoginLogEntity;
|
import com.imeeting.entity.biz.DeviceLoginLogEntity;
|
||||||
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
|
import com.imeeting.mapper.DeviceLoginLogMapper;
|
||||||
import com.imeeting.service.android.AndroidDeviceBindingService;
|
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -42,11 +42,8 @@ public class AndroidDeviceBindingServiceImpl implements AndroidDeviceBindingServ
|
||||||
throw new RuntimeException("设备登录态无效");
|
throw new RuntimeException("设备登录态无效");
|
||||||
}
|
}
|
||||||
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim());
|
DeviceInfoEntity existing = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceCode.trim());
|
||||||
if (existing == null || existing.getUserId() == null || existing.getTenantId() == null) {
|
if (existing == null) {
|
||||||
throw new RuntimeException("设备未登录,请先完成设备登录");
|
throw new RuntimeException("设备未注册");
|
||||||
}
|
|
||||||
if (!Objects.equals(existing.getUserId(), userId) || !Objects.equals(existing.getTenantId(), tenantId)) {
|
|
||||||
throw new RuntimeException("当前设备已被其他用户占用,请使用当前登录用户或重新登录占用设备");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +57,7 @@ public class AndroidDeviceBindingServiceImpl implements AndroidDeviceBindingServ
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
existing.setUserId(null);
|
existing.setUserId(null);
|
||||||
existing.setTenantId(null);
|
// existing.setTenantId(null);
|
||||||
deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing);
|
deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,7 @@ public class AndroidDeviceHomeServiceImpl implements AndroidDeviceHomeService {
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
MeetingPointsBalanceVO balance = meetingPointsService.getBalanceView(tenantId, anonymous ? null : userId);
|
MeetingPointsBalanceVO balance = meetingPointsService.getBalanceView(tenantId, anonymous ? null : userId);
|
||||||
long totalPoints = defaultLong(balance.getPublicBalance());
|
long totalPoints = defaultLong(balance.getTotalAvailableBalance());
|
||||||
if (!anonymous && userId != null) {
|
|
||||||
totalPoints += defaultLong(balance.getPersonalBalance());
|
|
||||||
}
|
|
||||||
int unitMinutes = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_UNIT_MINUTES, "1"), 1);
|
int unitMinutes = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_UNIT_MINUTES, "1"), 1);
|
||||||
int costPerUnit = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_COST_PER_UNIT, "1"), 1);
|
int costPerUnit = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_COST_PER_UNIT, "1"), 1);
|
||||||
return costPerUnit <= 0 ? 0L : (totalPoints * unitMinutes) / costPerUnit;
|
return costPerUnit <= 0 ? 0L : (totalPoints * unitMinutes) / costPerUnit;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
package com.imeeting.service.biz;
|
package com.imeeting.service.biz;
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.MeetingPointsBalanceVO;
|
||||||
import com.imeeting.entity.biz.AiTask;
|
import com.imeeting.entity.biz.AiTask;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.dto.biz.MeetingPointsBalanceVO;
|
|
||||||
|
|
||||||
public interface MeetingPointsService {
|
public interface MeetingPointsService {
|
||||||
long UNIFIED_ACCOUNT_USER_ID = 0L;
|
long UNIFIED_ACCOUNT_USER_ID = 0L;
|
||||||
|
|
||||||
|
void initializeTenantPointsAccount(Long tenantId);
|
||||||
|
|
||||||
|
void transferPublicPointsToUser(Long tenantId, Long targetUserId, Long points, String remark);
|
||||||
|
|
||||||
void recordAsrSuccessCharge(Meeting meeting, AiTask asrTask);
|
void recordAsrSuccessCharge(Meeting meeting, AiTask asrTask);
|
||||||
|
|
||||||
void recordSummarySuccessCharge(Meeting meeting, AiTask summaryTask);
|
void recordSummarySuccessCharge(Meeting meeting, AiTask summaryTask);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package com.imeeting.service.biz;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
|
||||||
import com.imeeting.entity.biz.MeetingUserStats;
|
|
||||||
|
|
||||||
public interface MeetingUserStatsService extends IService<MeetingUserStats> {
|
|
||||||
}
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.imeeting.dto.biz.DeviceAdminUpdateCommand;
|
||||||
import com.imeeting.dto.biz.DeviceOnlineAdminVO;
|
import com.imeeting.dto.biz.DeviceOnlineAdminVO;
|
||||||
import com.imeeting.entity.biz.DeviceInfoEntity;
|
import com.imeeting.entity.biz.DeviceInfoEntity;
|
||||||
import com.imeeting.mapper.DeviceInfoMapper;
|
import com.imeeting.mapper.DeviceInfoMapper;
|
||||||
|
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||||
import com.imeeting.service.android.AndroidDeviceSessionService;
|
import com.imeeting.service.android.AndroidDeviceSessionService;
|
||||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||||
import com.imeeting.service.biz.DeviceOnlineManagementService;
|
import com.imeeting.service.biz.DeviceOnlineManagementService;
|
||||||
|
|
@ -28,6 +29,7 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
|
||||||
private final DeviceInfoMapper deviceInfoMapper;
|
private final DeviceInfoMapper deviceInfoMapper;
|
||||||
private final AndroidDeviceSessionService androidDeviceSessionService;
|
private final AndroidDeviceSessionService androidDeviceSessionService;
|
||||||
private final AndroidGatewayPushService androidGatewayPushService;
|
private final AndroidGatewayPushService androidGatewayPushService;
|
||||||
|
private final AndroidDeviceBindingService androidDeviceBindingService;
|
||||||
private final LicenseService licenseService;
|
private final LicenseService licenseService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -40,13 +42,28 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (authContext.getUserId() != null) {
|
||||||
|
androidDeviceBindingService.validatePrivateDeviceAccess(
|
||||||
|
deviceCode,
|
||||||
|
authContext.getTenantId(),
|
||||||
|
authContext.getUserId()
|
||||||
|
);
|
||||||
|
androidDeviceBindingService.bindPrivateDevice(
|
||||||
|
deviceCode,
|
||||||
|
authContext.getTenantId(),
|
||||||
|
authContext.getUserId(),
|
||||||
|
authContext.getAppVersion(),
|
||||||
|
authContext.getPlatform()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
existing.setTerminalType(normalizeTerminalType(authContext.getPlatform()));
|
existing.setTerminalType(normalizeTerminalType(authContext.getPlatform()));
|
||||||
existing.setTerminalVersion(normalize(authContext.getAppVersion()));
|
existing.setTerminalVersion(normalize(authContext.getAppVersion()));
|
||||||
existing.setLastOnlineAt(now);
|
existing.setLastOnlineAt(now);
|
||||||
existing.setUserId(authContext.getUserId());
|
existing.setUserId(authContext.getUserId());
|
||||||
existing.setTenantId(authContext.getTenantId());
|
existing.setTenantId(authContext.getTenantId());
|
||||||
deviceInfoMapper.updateById(existing);
|
deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.imeeting.service.biz.impl;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.imeeting.common.SysParamKeys;
|
import com.imeeting.common.SysParamKeys;
|
||||||
|
import com.imeeting.dto.biz.MeetingPointsChargeItemVO;
|
||||||
import com.imeeting.dto.biz.MeetingPointsLedgerDetailVO;
|
import com.imeeting.dto.biz.MeetingPointsLedgerDetailVO;
|
||||||
import com.imeeting.dto.biz.MeetingPointsLedgerListItemVO;
|
import com.imeeting.dto.biz.MeetingPointsLedgerListItemVO;
|
||||||
import com.imeeting.dto.biz.MeetingPointsOverviewVO;
|
import com.imeeting.dto.biz.MeetingPointsOverviewVO;
|
||||||
|
|
@ -37,7 +38,13 @@ import java.util.stream.Collectors;
|
||||||
@Service
|
@Service
|
||||||
public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService {
|
public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService {
|
||||||
private static final String ACCOUNT_MODE_PUBLIC = "PUBLIC";
|
private static final String ACCOUNT_MODE_PUBLIC = "PUBLIC";
|
||||||
|
private static final String ACCOUNT_MODE_PERSONAL = "PERSONAL";
|
||||||
|
private static final String ACCOUNT_MODE_BOTH = "BOTH";
|
||||||
|
private static final String CHARGE_PRIORITY_PUBLIC_FIRST = "PUBLIC_FIRST";
|
||||||
|
private static final String CHARGE_PRIORITY_PERSONAL_FIRST = "PERSONAL_FIRST";
|
||||||
private static final long PUBLIC_ACCOUNT_USER_ID = 0L;
|
private static final long PUBLIC_ACCOUNT_USER_ID = 0L;
|
||||||
|
private static final String POINTS_TYPE_ASR = "ASR";
|
||||||
|
private static final String POINTS_TYPE_LLM = "LLM";
|
||||||
|
|
||||||
private final MeetingPointsAccountService meetingPointsAccountService;
|
private final MeetingPointsAccountService meetingPointsAccountService;
|
||||||
private final MeetingPointsLedgerService meetingPointsLedgerService;
|
private final MeetingPointsLedgerService meetingPointsLedgerService;
|
||||||
|
|
@ -65,33 +72,43 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MeetingPointsOverviewVO getOverview(Long tenantId) {
|
public MeetingPointsOverviewVO getOverview(Long tenantId) {
|
||||||
List<Long> scopedOwnerUserIds = resolveScopedOwnerUserIds(tenantId);
|
String accountMode = resolveAccountMode();
|
||||||
MeetingPointsAccount publicAccount = meetingPointsAccountService.getOne(new LambdaQueryWrapper<MeetingPointsAccount>()
|
String chargePriority = resolveChargePriority();
|
||||||
.eq(MeetingPointsAccount::getTenantId, tenantId)
|
MeetingPointsAccount publicAccount = findAccount(tenantId, PUBLIC_ACCOUNT_USER_ID);
|
||||||
.eq(MeetingPointsAccount::getUserId, PUBLIC_ACCOUNT_USER_ID)
|
long publicBalance = publicAccount == null ? 0L : defaultLong(publicAccount.getCurrentBalance());
|
||||||
.last("LIMIT 1"));
|
long publicTotalUsed = publicAccount == null ? 0L : defaultLong(publicAccount.getTotalPointsUsed());
|
||||||
|
|
||||||
|
long personalBalance = 0L;
|
||||||
|
long personalTotalUsed = 0L;
|
||||||
|
List<MeetingPointsAccount> personalAccounts = meetingPointsAccountService.list(new LambdaQueryWrapper<MeetingPointsAccount>()
|
||||||
|
.eq(MeetingPointsAccount::getTenantId, tenantId)
|
||||||
|
.ne(MeetingPointsAccount::getUserId, PUBLIC_ACCOUNT_USER_ID));
|
||||||
|
for (MeetingPointsAccount account : personalAccounts) {
|
||||||
|
personalBalance += defaultLong(account.getCurrentBalance());
|
||||||
|
personalTotalUsed += defaultLong(account.getTotalPointsUsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> scopedOwnerUserIds = resolveScopedOwnerUserIds(tenantId);
|
||||||
long totalChargeCount = 0L;
|
long totalChargeCount = 0L;
|
||||||
long publicTotalPointsUsed = 0L;
|
if (scopedOwnerUserIds == null) {
|
||||||
List<Long> scopedChargeRecordIds = resolveChargeRecordIdsByOwners(tenantId, scopedOwnerUserIds);
|
totalChargeCount = meetingSummaryChargeRecordService.count(new LambdaQueryWrapper<MeetingSummaryChargeRecord>()
|
||||||
if (scopedChargeRecordIds == null || !scopedChargeRecordIds.isEmpty()) {
|
.eq(MeetingSummaryChargeRecord::getTenantId, tenantId)
|
||||||
LambdaQueryWrapper<MeetingPointsLedger> scopedLedgerWrapper = new LambdaQueryWrapper<MeetingPointsLedger>()
|
.gt(MeetingSummaryChargeRecord::getChargedTotalPoints, 0L));
|
||||||
.eq(MeetingPointsLedger::getTenantId, tenantId)
|
} else if (!scopedOwnerUserIds.isEmpty()) {
|
||||||
.lt(MeetingPointsLedger::getPointsDelta, 0)
|
totalChargeCount = meetingSummaryChargeRecordService.count(new LambdaQueryWrapper<MeetingSummaryChargeRecord>()
|
||||||
.in(scopedChargeRecordIds != null, MeetingPointsLedger::getChargeRecordId, scopedChargeRecordIds);
|
.eq(MeetingSummaryChargeRecord::getTenantId, tenantId)
|
||||||
totalChargeCount = meetingPointsLedgerService.count(scopedLedgerWrapper);
|
.in(MeetingSummaryChargeRecord::getUserId, scopedOwnerUserIds)
|
||||||
publicTotalPointsUsed = meetingPointsLedgerService.list(scopedLedgerWrapper)
|
.gt(MeetingSummaryChargeRecord::getChargedTotalPoints, 0L));
|
||||||
.stream()
|
|
||||||
.map(MeetingPointsLedger::getPointsDelta)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.mapToLong(value -> Math.abs(value))
|
|
||||||
.sum();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MeetingPointsOverviewVO vo = new MeetingPointsOverviewVO();
|
MeetingPointsOverviewVO vo = new MeetingPointsOverviewVO();
|
||||||
vo.setAccountMode(resolveAccountMode());
|
vo.setAccountMode(accountMode);
|
||||||
vo.setPublicBalance(publicAccount == null ? 0L : defaultLong(publicAccount.getCurrentBalance()));
|
vo.setChargePriority(chargePriority);
|
||||||
vo.setPublicTotalPointsUsed(publicTotalPointsUsed);
|
vo.setPublicBalance(publicBalance);
|
||||||
|
vo.setPublicTotalPointsUsed(publicTotalUsed);
|
||||||
|
vo.setPersonalBalance(personalBalance);
|
||||||
|
vo.setPersonalTotalPointsUsed(personalTotalUsed);
|
||||||
|
vo.setTotalAvailableBalance(resolveVisibleTotalBalance(accountMode, publicBalance, personalBalance));
|
||||||
vo.setTotalChargeCount(totalChargeCount);
|
vo.setTotalChargeCount(totalChargeCount);
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +138,7 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
||||||
LambdaQueryWrapper<MeetingPointsLedger> wrapper = new LambdaQueryWrapper<MeetingPointsLedger>()
|
LambdaQueryWrapper<MeetingPointsLedger> wrapper = new LambdaQueryWrapper<MeetingPointsLedger>()
|
||||||
.eq(MeetingPointsLedger::getTenantId, tenantId)
|
.eq(MeetingPointsLedger::getTenantId, tenantId)
|
||||||
.lt(MeetingPointsLedger::getPointsDelta, 0)
|
.lt(MeetingPointsLedger::getPointsDelta, 0)
|
||||||
|
.in(!StringUtils.hasText(pointsType), MeetingPointsLedger::getPointsType, List.of(POINTS_TYPE_ASR, POINTS_TYPE_LLM))
|
||||||
.eq(StringUtils.hasText(pointsType), MeetingPointsLedger::getPointsType, pointsType == null ? null : pointsType.trim().toUpperCase(Locale.ROOT))
|
.eq(StringUtils.hasText(pointsType), MeetingPointsLedger::getPointsType, pointsType == null ? null : pointsType.trim().toUpperCase(Locale.ROOT))
|
||||||
.in(filteredChargeRecordIds != null && !filteredChargeRecordIds.isEmpty(), MeetingPointsLedger::getChargeRecordId, filteredChargeRecordIds)
|
.in(filteredChargeRecordIds != null && !filteredChargeRecordIds.isEmpty(), MeetingPointsLedger::getChargeRecordId, filteredChargeRecordIds)
|
||||||
.orderByDesc(MeetingPointsLedger::getCreatedAt)
|
.orderByDesc(MeetingPointsLedger::getCreatedAt)
|
||||||
|
|
@ -150,7 +168,7 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
||||||
item.setSummaryTaskId(ledger.getSummaryTaskId());
|
item.setSummaryTaskId(ledger.getSummaryTaskId());
|
||||||
item.setOwnerUserId(chargeRecord == null ? null : chargeRecord.getUserId());
|
item.setOwnerUserId(chargeRecord == null ? null : chargeRecord.getUserId());
|
||||||
item.setOwnerUserName(resolveOwnerName(owner, chargeRecord == null ? null : chargeRecord.getUserId()));
|
item.setOwnerUserName(resolveOwnerName(owner, chargeRecord == null ? null : chargeRecord.getUserId()));
|
||||||
item.setChargeAccountType(chargeRecord == null ? null : chargeRecord.getChargeAccountType());
|
item.setChargeAccountType(resolveAccountTypeByLedger(ledger.getUserId()));
|
||||||
item.setPointsType(ledger.getPointsType());
|
item.setPointsType(ledger.getPointsType());
|
||||||
item.setConsumedPoints(Math.abs(defaultLong(ledger.getPointsDelta())));
|
item.setConsumedPoints(Math.abs(defaultLong(ledger.getPointsDelta())));
|
||||||
item.setBalanceBefore(ledger.getBalanceBefore());
|
item.setBalanceBefore(ledger.getBalanceBefore());
|
||||||
|
|
@ -185,8 +203,8 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
||||||
detail.setSummaryTaskId(ledger.getSummaryTaskId());
|
detail.setSummaryTaskId(ledger.getSummaryTaskId());
|
||||||
detail.setOwnerUserId(chargeRecord == null ? null : chargeRecord.getUserId());
|
detail.setOwnerUserId(chargeRecord == null ? null : chargeRecord.getUserId());
|
||||||
detail.setOwnerUserName(resolveOwnerName(owner, chargeRecord == null ? null : chargeRecord.getUserId()));
|
detail.setOwnerUserName(resolveOwnerName(owner, chargeRecord == null ? null : chargeRecord.getUserId()));
|
||||||
detail.setChargeAccountType(chargeRecord == null ? null : chargeRecord.getChargeAccountType());
|
detail.setChargeAccountType(resolveAccountTypeByLedger(ledger.getUserId()));
|
||||||
detail.setChargeAccountUserId(chargeRecord == null ? null : chargeRecord.getChargeAccountUserId());
|
detail.setChargeAccountUserId(ledger.getUserId());
|
||||||
detail.setPointsType(ledger.getPointsType());
|
detail.setPointsType(ledger.getPointsType());
|
||||||
detail.setConsumedPoints(Math.abs(defaultLong(ledger.getPointsDelta())));
|
detail.setConsumedPoints(Math.abs(defaultLong(ledger.getPointsDelta())));
|
||||||
detail.setBalanceBefore(ledger.getBalanceBefore());
|
detail.setBalanceBefore(ledger.getBalanceBefore());
|
||||||
|
|
@ -210,9 +228,46 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
||||||
detail.setAsrChargedAt(chargeRecord == null ? null : chargeRecord.getAsrChargedAt());
|
detail.setAsrChargedAt(chargeRecord == null ? null : chargeRecord.getAsrChargedAt());
|
||||||
detail.setLlmChargedAt(chargeRecord == null ? null : chargeRecord.getLlmChargedAt());
|
detail.setLlmChargedAt(chargeRecord == null ? null : chargeRecord.getLlmChargedAt());
|
||||||
detail.setCreatedAt(ledger.getCreatedAt());
|
detail.setCreatedAt(ledger.getCreatedAt());
|
||||||
|
detail.setChargeItems(buildChargeItems(tenantId, ledger.getChargeRecordId()));
|
||||||
return detail;
|
return detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MeetingPointsAccount findAccount(Long tenantId, Long userId) {
|
||||||
|
if (tenantId == null || userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return meetingPointsAccountService.getOne(new LambdaQueryWrapper<MeetingPointsAccount>()
|
||||||
|
.eq(MeetingPointsAccount::getTenantId, tenantId)
|
||||||
|
.eq(MeetingPointsAccount::getUserId, userId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MeetingPointsChargeItemVO> buildChargeItems(Long tenantId, Long chargeRecordId) {
|
||||||
|
if (chargeRecordId == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<MeetingPointsLedger> ledgers = meetingPointsLedgerService.list(new LambdaQueryWrapper<MeetingPointsLedger>()
|
||||||
|
.eq(MeetingPointsLedger::getTenantId, tenantId)
|
||||||
|
.eq(MeetingPointsLedger::getChargeRecordId, chargeRecordId)
|
||||||
|
.lt(MeetingPointsLedger::getPointsDelta, 0)
|
||||||
|
.orderByAsc(MeetingPointsLedger::getId));
|
||||||
|
List<MeetingPointsChargeItemVO> items = new ArrayList<>();
|
||||||
|
int order = 1;
|
||||||
|
for (MeetingPointsLedger ledger : ledgers) {
|
||||||
|
MeetingPointsChargeItemVO item = new MeetingPointsChargeItemVO();
|
||||||
|
item.setId(ledger.getId());
|
||||||
|
item.setChargeStage(ledger.getPointsType());
|
||||||
|
item.setAccountType(resolveAccountTypeByLedger(ledger.getUserId()));
|
||||||
|
item.setAccountUserId(ledger.getUserId());
|
||||||
|
item.setPriorityOrder(order++);
|
||||||
|
item.setChargedPoints(Math.abs(defaultLong(ledger.getPointsDelta())));
|
||||||
|
item.setBalanceBefore(ledger.getBalanceBefore());
|
||||||
|
item.setBalanceAfter(ledger.getBalanceAfter());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
private List<Long> resolveMatchedOwnerIds(Long tenantId, String username, List<Long> scopedOwnerUserIds) {
|
private List<Long> resolveMatchedOwnerIds(Long tenantId, String username, List<Long> scopedOwnerUserIds) {
|
||||||
if (scopedOwnerUserIds != null && scopedOwnerUserIds.isEmpty()) {
|
if (scopedOwnerUserIds != null && scopedOwnerUserIds.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
@ -337,7 +392,34 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
||||||
if (!StringUtils.hasText(configured)) {
|
if (!StringUtils.hasText(configured)) {
|
||||||
return ACCOUNT_MODE_PUBLIC;
|
return ACCOUNT_MODE_PUBLIC;
|
||||||
}
|
}
|
||||||
return configured.trim().toUpperCase(Locale.ROOT);
|
String normalized = configured.trim().toUpperCase(Locale.ROOT);
|
||||||
|
if (ACCOUNT_MODE_PERSONAL.equals(normalized) || ACCOUNT_MODE_BOTH.equals(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return ACCOUNT_MODE_PUBLIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveChargePriority() {
|
||||||
|
String configured = sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_CHARGE_PRIORITY, CHARGE_PRIORITY_PUBLIC_FIRST);
|
||||||
|
if (!StringUtils.hasText(configured)) {
|
||||||
|
return CHARGE_PRIORITY_PUBLIC_FIRST;
|
||||||
|
}
|
||||||
|
String normalized = configured.trim().toUpperCase(Locale.ROOT);
|
||||||
|
return CHARGE_PRIORITY_PUBLIC_FIRST.equals(normalized) ? CHARGE_PRIORITY_PUBLIC_FIRST : CHARGE_PRIORITY_PERSONAL_FIRST;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveAccountTypeByLedger(Long accountUserId) {
|
||||||
|
return accountUserId != null && accountUserId == PUBLIC_ACCOUNT_USER_ID ? ACCOUNT_MODE_PUBLIC : ACCOUNT_MODE_PERSONAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long resolveVisibleTotalBalance(String accountMode, long publicBalance, long personalBalance) {
|
||||||
|
if (ACCOUNT_MODE_PUBLIC.equals(accountMode)) {
|
||||||
|
return publicBalance;
|
||||||
|
}
|
||||||
|
if (ACCOUNT_MODE_PERSONAL.equals(accountMode)) {
|
||||||
|
return personalBalance;
|
||||||
|
}
|
||||||
|
return publicBalance + personalBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long defaultLong(Long value) {
|
private long defaultLong(Long value) {
|
||||||
|
|
|
||||||
|
|
@ -8,34 +8,27 @@ import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.entity.biz.MeetingPointsAccount;
|
import com.imeeting.entity.biz.MeetingPointsAccount;
|
||||||
import com.imeeting.entity.biz.MeetingPointsLedger;
|
import com.imeeting.entity.biz.MeetingPointsLedger;
|
||||||
import com.imeeting.entity.biz.MeetingSummaryChargeRecord;
|
import com.imeeting.entity.biz.MeetingSummaryChargeRecord;
|
||||||
import com.imeeting.entity.biz.MeetingTranscript;
|
|
||||||
import com.imeeting.entity.biz.MeetingUserStats;
|
|
||||||
import com.imeeting.mapper.biz.AiTaskMapper;
|
import com.imeeting.mapper.biz.AiTaskMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingPointsAccountMapper;
|
import com.imeeting.mapper.biz.MeetingPointsAccountMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingSummaryChargeRecordMapper;
|
import com.imeeting.mapper.biz.MeetingSummaryChargeRecordMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
|
||||||
import com.imeeting.service.biz.MeetingPointsAccountService;
|
import com.imeeting.service.biz.MeetingPointsAccountService;
|
||||||
import com.imeeting.service.biz.MeetingPointsLedgerService;
|
import com.imeeting.service.biz.MeetingPointsLedgerService;
|
||||||
import com.imeeting.service.biz.MeetingPointsService;
|
import com.imeeting.service.biz.MeetingPointsService;
|
||||||
import com.imeeting.service.biz.MeetingSummaryChargeRecordService;
|
import com.imeeting.service.biz.MeetingSummaryChargeRecordService;
|
||||||
import com.imeeting.service.biz.MeetingUserStatsService;
|
|
||||||
import com.unisbase.service.SysParamService;
|
import com.unisbase.service.SysParamService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.sound.sampled.AudioInputStream;
|
|
||||||
import javax.sound.sampled.AudioSystem;
|
|
||||||
import java.io.File;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
|
@ -45,25 +38,71 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
private static final String TRIGGER_RESUMMARY = "RESUMMARY";
|
private static final String TRIGGER_RESUMMARY = "RESUMMARY";
|
||||||
private static final String ACCOUNT_MODE_PUBLIC = "PUBLIC";
|
private static final String ACCOUNT_MODE_PUBLIC = "PUBLIC";
|
||||||
private static final String ACCOUNT_MODE_PERSONAL = "PERSONAL";
|
private static final String ACCOUNT_MODE_PERSONAL = "PERSONAL";
|
||||||
|
private static final String ACCOUNT_MODE_BOTH = "BOTH";
|
||||||
|
private static final String CHARGE_PRIORITY_PERSONAL_FIRST = "PERSONAL_FIRST";
|
||||||
|
private static final String CHARGE_PRIORITY_PUBLIC_FIRST = "PUBLIC_FIRST";
|
||||||
private static final String STATUS_PENDING = "PENDING";
|
private static final String STATUS_PENDING = "PENDING";
|
||||||
private static final String STATUS_ASR_CHARGED = "ASR_CHARGED";
|
private static final String STATUS_ASR_CHARGED = "ASR_CHARGED";
|
||||||
private static final String STATUS_COMPLETED = "COMPLETED";
|
private static final String STATUS_COMPLETED = "COMPLETED";
|
||||||
private static final String STATUS_FAILED = "FAILED";
|
private static final String STATUS_FAILED = "FAILED";
|
||||||
private static final String STATUS_DISABLED = "DISABLED";
|
private static final String STATUS_DISABLED = "DISABLED";
|
||||||
|
private static final String POINTS_TYPE_ASR = "ASR";
|
||||||
|
private static final String POINTS_TYPE_LLM = "LLM";
|
||||||
|
private static final String POINTS_TYPE_INIT = "INIT";
|
||||||
|
private static final String POINTS_TYPE_TRANSFER_OUT = "TRANSFER_OUT";
|
||||||
|
private static final String POINTS_TYPE_TRANSFER_IN = "TRANSFER_IN";
|
||||||
|
|
||||||
private final MeetingSummaryChargeRecordService chargeRecordService;
|
private final MeetingSummaryChargeRecordService chargeRecordService;
|
||||||
private final MeetingPointsAccountService pointsAccountService;
|
private final MeetingPointsAccountService pointsAccountService;
|
||||||
private final MeetingPointsLedgerService pointsLedgerService;
|
private final MeetingPointsLedgerService pointsLedgerService;
|
||||||
private final MeetingUserStatsService meetingUserStatsService;
|
|
||||||
private final MeetingTranscriptMapper transcriptMapper;
|
|
||||||
private final MeetingMapper meetingMapper;
|
private final MeetingMapper meetingMapper;
|
||||||
private final AiTaskMapper aiTaskMapper;
|
private final AiTaskMapper aiTaskMapper;
|
||||||
private final MeetingPointsAccountMapper meetingPointsAccountMapper;
|
private final MeetingPointsAccountMapper meetingPointsAccountMapper;
|
||||||
private final MeetingSummaryChargeRecordMapper meetingSummaryChargeRecordMapper;
|
private final MeetingSummaryChargeRecordMapper meetingSummaryChargeRecordMapper;
|
||||||
private final SysParamService sysParamService;
|
private final SysParamService sysParamService;
|
||||||
|
|
||||||
@Value("${unisbase.app.upload-path}")
|
@Override
|
||||||
private String uploadPath;
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void initializeTenantPointsAccount(Long tenantId) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long initialBalance = nonNegativeLong(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_INITIAL_BALANCE, "0"), 0L);
|
||||||
|
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, initialBalance, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void transferPublicPointsToUser(Long tenantId, Long targetUserId, Long points, String remark) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new RuntimeException("缺少租户信息");
|
||||||
|
}
|
||||||
|
if (targetUserId == null || targetUserId <= 0L) {
|
||||||
|
throw new RuntimeException("目标用户不能为空");
|
||||||
|
}
|
||||||
|
long safePoints = points == null ? 0L : points;
|
||||||
|
if (safePoints <= 0L) {
|
||||||
|
throw new RuntimeException("分配积分必须大于0");
|
||||||
|
}
|
||||||
|
|
||||||
|
MeetingPointsAccount publicAccount = getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false);
|
||||||
|
long publicBefore = defaultLong(publicAccount.getCurrentBalance());
|
||||||
|
if (publicBefore < safePoints) {
|
||||||
|
throw new RuntimeException("公共账户积分不足");
|
||||||
|
}
|
||||||
|
|
||||||
|
MeetingPointsAccount personalAccount = getOrCreateAccountForMutation(tenantId, targetUserId, 0L, false);
|
||||||
|
long personalBefore = defaultLong(personalAccount.getCurrentBalance());
|
||||||
|
|
||||||
|
publicAccount.setCurrentBalance(publicBefore - safePoints);
|
||||||
|
personalAccount.setCurrentBalance(personalBefore + safePoints);
|
||||||
|
pointsAccountService.updateById(publicAccount);
|
||||||
|
pointsAccountService.updateById(personalAccount);
|
||||||
|
|
||||||
|
String normalizedRemark = StringUtils.hasText(remark) ? remark.trim() : "管理员从公共账户分配积分";
|
||||||
|
saveTransferLedger(tenantId, UNIFIED_ACCOUNT_USER_ID, POINTS_TYPE_TRANSFER_OUT, -safePoints, publicBefore, publicBefore - safePoints, normalizedRemark);
|
||||||
|
saveTransferLedger(tenantId, targetUserId, POINTS_TYPE_TRANSFER_IN, safePoints, personalBefore, personalBefore + safePoints, normalizedRemark);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
|
@ -87,7 +126,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
|
|
||||||
ensureMeetingDurationStats(meeting, durationSeconds);
|
ensureMeetingDurationStats(meeting, durationSeconds);
|
||||||
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(meeting, summaryTask, chargeTriggerType, durationSeconds);
|
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(meeting, summaryTask, chargeTriggerType, durationSeconds);
|
||||||
if (defaultLong(record.getChargedAsrPoints()) > 0) {
|
if (defaultLong(record.getChargedAsrPoints()) > 0L) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isPointsEnabled()) {
|
if (!isPointsEnabled()) {
|
||||||
|
|
@ -102,26 +141,16 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MeetingPointsAccount account = getOrCreateAccount(meeting.getTenantId(), record.getChargeAccountUserId());
|
ChargeExecutionResult result = executeCharge(meeting, summaryTask, record, POINTS_TYPE_ASR, chargeAmount);
|
||||||
long balanceBefore = defaultLong(account.getCurrentBalance());
|
record.setBalanceBefore(record.getBalanceBefore() == null ? result.totalBalanceBefore() : record.getBalanceBefore());
|
||||||
long balanceAfter = balanceBefore - chargeAmount;
|
record.setBalanceAfter(result.totalBalanceAfter());
|
||||||
|
record.setChargedTotalPoints(defaultLong(record.getChargedTotalPoints()) + result.chargedPoints());
|
||||||
account.setCurrentBalance(balanceAfter);
|
record.setChargedAsrPoints(result.chargedPoints());
|
||||||
account.setTotalPointsUsed(defaultLong(account.getTotalPointsUsed()) + chargeAmount);
|
|
||||||
account.setTotalAsrPointsUsed(defaultLong(account.getTotalAsrPointsUsed()) + chargeAmount);
|
|
||||||
pointsAccountService.updateById(account);
|
|
||||||
|
|
||||||
record.setBalanceBefore(record.getBalanceBefore() == null ? balanceBefore : record.getBalanceBefore());
|
|
||||||
record.setBalanceAfter(balanceAfter);
|
|
||||||
record.setChargedTotalPoints(defaultLong(record.getChargedTotalPoints()) + chargeAmount);
|
|
||||||
record.setChargedAsrPoints(chargeAmount);
|
|
||||||
record.setAsrChargedAt(LocalDateTime.now());
|
record.setAsrChargedAt(LocalDateTime.now());
|
||||||
record.setChargedAt(LocalDateTime.now());
|
record.setChargedAt(LocalDateTime.now());
|
||||||
record.setPointsDelta(-defaultLong(record.getChargedTotalPoints()));
|
record.setPointsDelta(-defaultLong(record.getChargedTotalPoints()));
|
||||||
record.setSummaryStatus(STATUS_ASR_CHARGED);
|
record.setSummaryStatus(STATUS_ASR_CHARGED);
|
||||||
saveOrUpdateRecord(record);
|
saveOrUpdateRecord(record);
|
||||||
|
|
||||||
saveLedger(meeting, summaryTask, record, "ASR", -chargeAmount, balanceBefore, balanceAfter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -139,20 +168,17 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
record.setSummaryStatus(STATUS_COMPLETED);
|
record.setSummaryStatus(STATUS_COMPLETED);
|
||||||
record.setLlmChargedAt(LocalDateTime.now());
|
record.setLlmChargedAt(LocalDateTime.now());
|
||||||
saveOrUpdateRecord(record);
|
saveOrUpdateRecord(record);
|
||||||
incrementSummaryChargeCount(meeting.getTenantId(), meeting.getCreatorId());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureMeetingDurationStats(meeting, durationSeconds);
|
ensureMeetingDurationStats(meeting, durationSeconds);
|
||||||
if (defaultLong(record.getChargedLlmPoints()) > 0) {
|
if (defaultLong(record.getChargedLlmPoints()) > 0L) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPointsEnabled()) {
|
if (!isPointsEnabled()) {
|
||||||
record.setSummaryStatus(STATUS_DISABLED);
|
record.setSummaryStatus(STATUS_DISABLED);
|
||||||
record.setLlmChargedAt(LocalDateTime.now());
|
record.setLlmChargedAt(LocalDateTime.now());
|
||||||
saveOrUpdateRecord(record);
|
saveOrUpdateRecord(record);
|
||||||
incrementSummaryChargeCount(meeting.getTenantId(), meeting.getCreatorId());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,33 +187,21 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
record.setSummaryStatus(STATUS_COMPLETED);
|
record.setSummaryStatus(STATUS_COMPLETED);
|
||||||
record.setLlmChargedAt(LocalDateTime.now());
|
record.setLlmChargedAt(LocalDateTime.now());
|
||||||
saveOrUpdateRecord(record);
|
saveOrUpdateRecord(record);
|
||||||
incrementSummaryChargeCount(meeting.getTenantId(), meeting.getCreatorId());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MeetingPointsAccount account = getOrCreateAccount(meeting.getTenantId(), record.getChargeAccountUserId());
|
ChargeExecutionResult result = executeCharge(meeting, summaryTask, record, POINTS_TYPE_LLM, chargeAmount);
|
||||||
long balanceBefore = defaultLong(account.getCurrentBalance());
|
|
||||||
long balanceAfter = balanceBefore - chargeAmount;
|
|
||||||
|
|
||||||
account.setCurrentBalance(balanceAfter);
|
|
||||||
account.setTotalPointsUsed(defaultLong(account.getTotalPointsUsed()) + chargeAmount);
|
|
||||||
account.setTotalLlmPointsUsed(defaultLong(account.getTotalLlmPointsUsed()) + chargeAmount);
|
|
||||||
pointsAccountService.updateById(account);
|
|
||||||
|
|
||||||
if (record.getBalanceBefore() == null) {
|
if (record.getBalanceBefore() == null) {
|
||||||
record.setBalanceBefore(balanceBefore);
|
record.setBalanceBefore(result.totalBalanceBefore());
|
||||||
}
|
}
|
||||||
record.setBalanceAfter(balanceAfter);
|
record.setBalanceAfter(result.totalBalanceAfter());
|
||||||
record.setChargedTotalPoints(defaultLong(record.getChargedTotalPoints()) + chargeAmount);
|
record.setChargedTotalPoints(defaultLong(record.getChargedTotalPoints()) + result.chargedPoints());
|
||||||
record.setChargedLlmPoints(chargeAmount);
|
record.setChargedLlmPoints(result.chargedPoints());
|
||||||
record.setLlmChargedAt(LocalDateTime.now());
|
record.setLlmChargedAt(LocalDateTime.now());
|
||||||
record.setChargedAt(LocalDateTime.now());
|
record.setChargedAt(LocalDateTime.now());
|
||||||
record.setPointsDelta(-defaultLong(record.getChargedTotalPoints()));
|
record.setPointsDelta(-defaultLong(record.getChargedTotalPoints()));
|
||||||
record.setSummaryStatus(STATUS_COMPLETED);
|
record.setSummaryStatus(STATUS_COMPLETED);
|
||||||
saveOrUpdateRecord(record);
|
saveOrUpdateRecord(record);
|
||||||
|
|
||||||
saveLedger(meeting, summaryTask, record, "LLM", -chargeAmount, balanceBefore, balanceAfter);
|
|
||||||
incrementSummaryChargeCount(meeting.getTenantId(), meeting.getCreatorId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -211,22 +225,36 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String resolveLatestBlockedReason(Long summaryTaskId) {
|
public String resolveLatestBlockedReason(Long summaryTaskId) {
|
||||||
return null;
|
if (summaryTaskId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MeetingSummaryChargeRecord record = chargeRecordService.getOne(new LambdaQueryWrapper<MeetingSummaryChargeRecord>()
|
||||||
|
.eq(MeetingSummaryChargeRecord::getSummaryTaskId, summaryTaskId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
return record == null ? null : record.getBlockedReason();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MeetingPointsBalanceVO getBalanceView(Long tenantId, Long userId) {
|
public MeetingPointsBalanceVO getBalanceView(Long tenantId, Long userId) {
|
||||||
MeetingPointsAccount publicAccount = getOrCreateAccount(tenantId, UNIFIED_ACCOUNT_USER_ID);
|
MeetingPointsAccount publicAccount = findAccount(tenantId, UNIFIED_ACCOUNT_USER_ID);
|
||||||
MeetingPointsAccount personalAccount = getOrCreateAccount(tenantId, userId);
|
MeetingPointsAccount personalAccount = userId == null ? null : findAccount(tenantId, userId);
|
||||||
|
long publicBalance = publicAccount == null ? 0L : defaultLong(publicAccount.getCurrentBalance());
|
||||||
|
long publicTotalUsed = publicAccount == null ? 0L : defaultLong(publicAccount.getTotalPointsUsed());
|
||||||
|
long personalBalance = personalAccount == null ? 0L : defaultLong(personalAccount.getCurrentBalance());
|
||||||
|
long personalTotalUsed = personalAccount == null ? 0L : defaultLong(personalAccount.getTotalPointsUsed());
|
||||||
|
String accountMode = resolveAccountMode();
|
||||||
|
String chargePriority = resolveChargePriority();
|
||||||
|
|
||||||
MeetingPointsBalanceVO vo = new MeetingPointsBalanceVO();
|
MeetingPointsBalanceVO vo = new MeetingPointsBalanceVO();
|
||||||
vo.setTenantId(tenantId);
|
vo.setTenantId(tenantId);
|
||||||
vo.setUserId(userId);
|
vo.setUserId(userId);
|
||||||
vo.setPreferredAccountMode(resolveAccountMode());
|
vo.setAccountMode(accountMode);
|
||||||
vo.setPublicBalance(defaultLong(publicAccount.getCurrentBalance()));
|
vo.setChargePriority(chargePriority);
|
||||||
vo.setPublicTotalPointsUsed(defaultLong(publicAccount.getTotalPointsUsed()));
|
vo.setPublicBalance(publicBalance);
|
||||||
vo.setPersonalBalance(defaultLong(personalAccount.getCurrentBalance()));
|
vo.setPublicTotalPointsUsed(publicTotalUsed);
|
||||||
vo.setPersonalTotalPointsUsed(defaultLong(personalAccount.getTotalPointsUsed()));
|
vo.setPersonalBalance(personalBalance);
|
||||||
|
vo.setPersonalTotalPointsUsed(personalTotalUsed);
|
||||||
|
vo.setTotalAvailableBalance(resolveVisibleTotalBalance(accountMode, publicBalance, personalBalance));
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,15 +294,15 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
incrementSummaryAttemptCount(meeting.getTenantId(), meeting.getCreatorId());
|
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyChargeSnapshot(MeetingSummaryChargeRecord record, Meeting meeting, String chargeTriggerType, int durationSeconds) {
|
private void applyChargeSnapshot(MeetingSummaryChargeRecord record, Meeting meeting, String chargeTriggerType, int durationSeconds) {
|
||||||
ChargeSnapshot snapshot = buildChargeSnapshot(durationSeconds);
|
ChargeSnapshot snapshot = buildChargeSnapshot(durationSeconds);
|
||||||
ChargeAccountSnapshot accountSnapshot = resolveChargeAccountSnapshot(meeting);
|
String accountMode = resolveAccountMode();
|
||||||
record.setChargeAccountType(accountSnapshot.accountType());
|
Long ownerUserId = meeting.getCreatorId() == null ? UNIFIED_ACCOUNT_USER_ID : meeting.getCreatorId();
|
||||||
record.setChargeAccountUserId(accountSnapshot.accountUserId());
|
record.setChargeAccountType(accountMode);
|
||||||
|
record.setChargeAccountUserId(ACCOUNT_MODE_PERSONAL.equals(accountMode) ? ownerUserId : UNIFIED_ACCOUNT_USER_ID);
|
||||||
record.setAudioDurationSeconds(durationSeconds);
|
record.setAudioDurationSeconds(durationSeconds);
|
||||||
record.setChargedMinutes(snapshot.chargedMinutes());
|
record.setChargedMinutes(snapshot.chargedMinutes());
|
||||||
record.setBillingUnits(snapshot.billingUnits());
|
record.setBillingUnits(snapshot.billingUnits());
|
||||||
|
|
@ -293,6 +321,176 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChargeExecutionResult executeCharge(Meeting meeting, AiTask summaryTask, MeetingSummaryChargeRecord record,
|
||||||
|
String pointsType, long chargeAmount) {
|
||||||
|
List<ChargeTarget> chargeTargets = resolveChargeTargets(meeting.getTenantId(), record.getUserId());
|
||||||
|
long totalBalanceBefore = 0L;
|
||||||
|
for (ChargeTarget target : chargeTargets) {
|
||||||
|
totalBalanceBefore += defaultLong(target.account().getCurrentBalance());
|
||||||
|
}
|
||||||
|
if (isBalanceEnforced() && totalBalanceBefore < chargeAmount) {
|
||||||
|
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||||
|
saveOrUpdateRecord(record);
|
||||||
|
throw new RuntimeException("积分余额不足");
|
||||||
|
}
|
||||||
|
|
||||||
|
long remaining = chargeAmount;
|
||||||
|
for (int i = 0; i < chargeTargets.size(); i++) {
|
||||||
|
ChargeTarget target = chargeTargets.get(i);
|
||||||
|
MeetingPointsAccount account = target.account();
|
||||||
|
long currentBalance = defaultLong(account.getCurrentBalance());
|
||||||
|
boolean lastTarget = i == chargeTargets.size() - 1;
|
||||||
|
long deducted = calculateDeductedPoints(currentBalance, remaining, lastTarget);
|
||||||
|
if (deducted <= 0L) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long balanceAfter = currentBalance - deducted;
|
||||||
|
account.setCurrentBalance(balanceAfter);
|
||||||
|
account.setTotalPointsUsed(defaultLong(account.getTotalPointsUsed()) + deducted);
|
||||||
|
if (POINTS_TYPE_ASR.equals(pointsType)) {
|
||||||
|
account.setTotalAsrPointsUsed(defaultLong(account.getTotalAsrPointsUsed()) + deducted);
|
||||||
|
} else if (POINTS_TYPE_LLM.equals(pointsType)) {
|
||||||
|
account.setTotalLlmPointsUsed(defaultLong(account.getTotalLlmPointsUsed()) + deducted);
|
||||||
|
}
|
||||||
|
pointsAccountService.updateById(account);
|
||||||
|
|
||||||
|
saveChargeLedger(meeting, summaryTask, record, target.accountUserId(), pointsType, -deducted, currentBalance, balanceAfter);
|
||||||
|
remaining -= deducted;
|
||||||
|
if (remaining <= 0L) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > 0L) {
|
||||||
|
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||||
|
saveOrUpdateRecord(record);
|
||||||
|
throw new RuntimeException("积分扣费失败,未能完成完整扣减");
|
||||||
|
}
|
||||||
|
return new ChargeExecutionResult(chargeAmount, totalBalanceBefore, totalBalanceBefore - chargeAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long calculateDeductedPoints(long currentBalance, long remaining, boolean lastTarget) {
|
||||||
|
if (remaining <= 0L) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
if (isBalanceEnforced()) {
|
||||||
|
return Math.min(Math.max(currentBalance, 0L), remaining);
|
||||||
|
}
|
||||||
|
if (lastTarget) {
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(currentBalance, 0L), remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveChargeLedger(Meeting meeting, AiTask summaryTask, MeetingSummaryChargeRecord record, Long accountUserId,
|
||||||
|
String pointsType, long pointsDelta, long balanceBefore, long balanceAfter) {
|
||||||
|
if (pointsDelta == 0L) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MeetingPointsLedger ledger = new MeetingPointsLedger();
|
||||||
|
ledger.setTenantId(meeting.getTenantId());
|
||||||
|
ledger.setStatus(1);
|
||||||
|
ledger.setUserId(accountUserId);
|
||||||
|
ledger.setMeetingId(meeting.getId());
|
||||||
|
ledger.setSummaryTaskId(summaryTask.getId());
|
||||||
|
ledger.setChargeRecordId(record.getId());
|
||||||
|
ledger.setPointsDelta(pointsDelta);
|
||||||
|
ledger.setPointsType(pointsType);
|
||||||
|
ledger.setBalanceBefore(balanceBefore);
|
||||||
|
ledger.setBalanceAfter(balanceAfter);
|
||||||
|
ledger.setRemark(TRIGGER_RESUMMARY.equals(record.getChargeTriggerType())
|
||||||
|
? "重新总结成功后扣减"
|
||||||
|
: "总结任务成功后扣减");
|
||||||
|
pointsLedgerService.save(ledger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveTransferLedger(Long tenantId, Long accountUserId, String pointsType, long pointsDelta,
|
||||||
|
long balanceBefore, long balanceAfter, String remark) {
|
||||||
|
MeetingPointsLedger ledger = new MeetingPointsLedger();
|
||||||
|
ledger.setTenantId(tenantId);
|
||||||
|
ledger.setStatus(1);
|
||||||
|
ledger.setUserId(accountUserId);
|
||||||
|
ledger.setPointsDelta(pointsDelta);
|
||||||
|
ledger.setPointsType(pointsType);
|
||||||
|
ledger.setBalanceBefore(balanceBefore);
|
||||||
|
ledger.setBalanceAfter(balanceAfter);
|
||||||
|
ledger.setRemark(remark);
|
||||||
|
pointsLedgerService.save(ledger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MeetingPointsAccount findAccount(Long tenantId, Long userId) {
|
||||||
|
if (tenantId == null || userId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return pointsAccountService.getOne(new LambdaQueryWrapper<MeetingPointsAccount>()
|
||||||
|
.eq(MeetingPointsAccount::getTenantId, tenantId)
|
||||||
|
.eq(MeetingPointsAccount::getUserId, userId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private MeetingPointsAccount getOrCreateAccountForMutation(Long tenantId, Long userId, long initialBalance, boolean createInitLedger) {
|
||||||
|
MeetingPointsAccount account = meetingPointsAccountMapper.selectForUpdate(tenantId, userId);
|
||||||
|
if (account != null) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
account = new MeetingPointsAccount();
|
||||||
|
account.setTenantId(tenantId);
|
||||||
|
account.setStatus(1);
|
||||||
|
account.setUserId(userId);
|
||||||
|
account.setCurrentBalance(initialBalance);
|
||||||
|
account.setTotalPointsUsed(0L);
|
||||||
|
account.setTotalAsrPointsUsed(0L);
|
||||||
|
account.setTotalLlmPointsUsed(0L);
|
||||||
|
try {
|
||||||
|
pointsAccountService.save(account);
|
||||||
|
} catch (DuplicateKeyException ex) {
|
||||||
|
account = meetingPointsAccountMapper.selectForUpdate(tenantId, userId);
|
||||||
|
if (account == null) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
if (createInitLedger && initialBalance > 0L) {
|
||||||
|
saveTransferLedger(tenantId, userId, POINTS_TYPE_INIT, initialBalance, 0L, initialBalance, "公共积分账户初始化发放");
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChargeTarget> resolveChargeTargets(Long tenantId, Long ownerUserId) {
|
||||||
|
Long personalUserId = ownerUserId == null ? UNIFIED_ACCOUNT_USER_ID : ownerUserId;
|
||||||
|
String accountMode = resolveAccountMode();
|
||||||
|
String chargePriority = resolveChargePriority();
|
||||||
|
List<ChargeTarget> targets = new ArrayList<>();
|
||||||
|
if (ACCOUNT_MODE_PUBLIC.equals(accountMode)) {
|
||||||
|
targets.add(new ChargeTarget(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID,
|
||||||
|
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false)));
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
if (ACCOUNT_MODE_PERSONAL.equals(accountMode)) {
|
||||||
|
targets.add(new ChargeTarget(ACCOUNT_MODE_PERSONAL, personalUserId,
|
||||||
|
getOrCreateAccountForMutation(tenantId, personalUserId, 0L, false)));
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
if (personalUserId.equals(UNIFIED_ACCOUNT_USER_ID)) {
|
||||||
|
targets.add(new ChargeTarget(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID,
|
||||||
|
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false)));
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
if (CHARGE_PRIORITY_PUBLIC_FIRST.equals(chargePriority)) {
|
||||||
|
targets.add(new ChargeTarget(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID,
|
||||||
|
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false)));
|
||||||
|
targets.add(new ChargeTarget(ACCOUNT_MODE_PERSONAL, personalUserId,
|
||||||
|
getOrCreateAccountForMutation(tenantId, personalUserId, 0L, false)));
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
targets.add(new ChargeTarget(ACCOUNT_MODE_PERSONAL, personalUserId,
|
||||||
|
getOrCreateAccountForMutation(tenantId, personalUserId, 0L, false)));
|
||||||
|
targets.add(new ChargeTarget(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID,
|
||||||
|
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, 0L, false)));
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
private ChargeSnapshot buildChargeSnapshot(int durationSeconds) {
|
private ChargeSnapshot buildChargeSnapshot(int durationSeconds) {
|
||||||
int chargedMinutes = toChargedMinutes(durationSeconds);
|
int chargedMinutes = toChargedMinutes(durationSeconds);
|
||||||
int unitMinutes = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_UNIT_MINUTES, "1"), 1);
|
int unitMinutes = positiveInt(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_UNIT_MINUTES, "1"), 1);
|
||||||
|
|
@ -316,113 +514,6 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
meeting.setEffectiveAudioDurationSeconds(durationSeconds);
|
meeting.setEffectiveAudioDurationSeconds(durationSeconds);
|
||||||
meetingMapper.updateById(meeting);
|
meetingMapper.updateById(meeting);
|
||||||
}
|
}
|
||||||
long deltaSeconds = (long) durationSeconds - (previousDuration == null ? 0L : previousDuration.longValue());
|
|
||||||
if (deltaSeconds == 0L) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MeetingUserStats stats = getOrCreateUserStats(meeting.getTenantId(), meeting.getCreatorId());
|
|
||||||
stats.setTotalMeetingDurationSeconds(defaultLong(stats.getTotalMeetingDurationSeconds()) + deltaSeconds);
|
|
||||||
stats.setTotalMeetingDurationMinutes(toCeilMinutes(defaultLong(stats.getTotalMeetingDurationSeconds())));
|
|
||||||
meetingUserStatsService.updateById(stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void incrementSummaryAttemptCount(Long tenantId, Long userId) {
|
|
||||||
if (userId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MeetingUserStats stats = getOrCreateUserStats(tenantId, userId);
|
|
||||||
stats.setTotalSummaryAttemptCount(defaultLong(stats.getTotalSummaryAttemptCount()) + 1L);
|
|
||||||
meetingUserStatsService.updateById(stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void incrementSummaryChargeCount(Long tenantId, Long userId) {
|
|
||||||
if (userId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MeetingUserStats stats = getOrCreateUserStats(tenantId, userId);
|
|
||||||
stats.setTotalSummaryChargeCount(defaultLong(stats.getTotalSummaryChargeCount()) + 1L);
|
|
||||||
meetingUserStatsService.updateById(stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MeetingUserStats getOrCreateUserStats(Long tenantId, Long userId) {
|
|
||||||
MeetingUserStats stats = meetingUserStatsService.getOne(new LambdaQueryWrapper<MeetingUserStats>()
|
|
||||||
.eq(MeetingUserStats::getTenantId, tenantId)
|
|
||||||
.eq(MeetingUserStats::getUserId, userId)
|
|
||||||
.last("LIMIT 1"));
|
|
||||||
if (stats != null) {
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
stats = new MeetingUserStats();
|
|
||||||
stats.setTenantId(tenantId);
|
|
||||||
stats.setStatus(1);
|
|
||||||
stats.setUserId(userId);
|
|
||||||
stats.setTotalMeetingDurationSeconds(0L);
|
|
||||||
stats.setTotalMeetingDurationMinutes(0L);
|
|
||||||
stats.setTotalSummaryChargeCount(0L);
|
|
||||||
stats.setTotalSummaryAttemptCount(0L);
|
|
||||||
meetingUserStatsService.save(stats);
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MeetingPointsAccount getOrCreateAccount(Long tenantId, Long userId) {
|
|
||||||
Long effectiveUserId = userId == null ? UNIFIED_ACCOUNT_USER_ID : userId;
|
|
||||||
MeetingPointsAccount account = meetingPointsAccountMapper.selectForUpdate(tenantId, effectiveUserId);
|
|
||||||
if (account != null) {
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
account = new MeetingPointsAccount();
|
|
||||||
account.setTenantId(tenantId);
|
|
||||||
account.setStatus(1);
|
|
||||||
account.setUserId(effectiveUserId);
|
|
||||||
long initialBalance = nonNegativeLong(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_INITIAL_BALANCE, "0"), 0L);
|
|
||||||
account.setCurrentBalance(initialBalance);
|
|
||||||
account.setTotalPointsUsed(0L);
|
|
||||||
account.setTotalAsrPointsUsed(0L);
|
|
||||||
account.setTotalLlmPointsUsed(0L);
|
|
||||||
try {
|
|
||||||
pointsAccountService.save(account);
|
|
||||||
} catch (DuplicateKeyException ex) {
|
|
||||||
account = meetingPointsAccountMapper.selectForUpdate(tenantId, effectiveUserId);
|
|
||||||
if (account == null) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialBalance > 0L) {
|
|
||||||
MeetingPointsLedger initLedger = new MeetingPointsLedger();
|
|
||||||
initLedger.setTenantId(tenantId);
|
|
||||||
initLedger.setStatus(1);
|
|
||||||
initLedger.setUserId(effectiveUserId);
|
|
||||||
initLedger.setPointsDelta(initialBalance);
|
|
||||||
initLedger.setPointsType("INIT");
|
|
||||||
initLedger.setBalanceBefore(0L);
|
|
||||||
initLedger.setBalanceAfter(initialBalance);
|
|
||||||
initLedger.setRemark(UNIFIED_ACCOUNT_USER_ID == effectiveUserId ? "公共积分账户初始化发放" : "个人积分账户初始化发放");
|
|
||||||
pointsLedgerService.save(initLedger);
|
|
||||||
}
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveLedger(Meeting meeting, AiTask summaryTask, MeetingSummaryChargeRecord record,
|
|
||||||
String pointsType, long pointsDelta, long balanceBefore, long balanceAfter) {
|
|
||||||
if (pointsDelta == 0L) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MeetingPointsLedger ledger = new MeetingPointsLedger();
|
|
||||||
ledger.setTenantId(meeting.getTenantId());
|
|
||||||
ledger.setStatus(1);
|
|
||||||
ledger.setUserId(record.getChargeAccountUserId());
|
|
||||||
ledger.setMeetingId(meeting.getId());
|
|
||||||
ledger.setSummaryTaskId(summaryTask.getId());
|
|
||||||
ledger.setChargeRecordId(record.getId());
|
|
||||||
ledger.setPointsDelta(pointsDelta);
|
|
||||||
ledger.setPointsType(pointsType);
|
|
||||||
ledger.setBalanceBefore(balanceBefore);
|
|
||||||
ledger.setBalanceAfter(balanceAfter);
|
|
||||||
ledger.setRemark(TRIGGER_RESUMMARY.equals(record.getChargeTriggerType())
|
|
||||||
? "重新总结成功后扣减" : "任务成功后按阶段扣减");
|
|
||||||
pointsLedgerService.save(ledger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveOrUpdateRecord(MeetingSummaryChargeRecord record) {
|
private void saveOrUpdateRecord(MeetingSummaryChargeRecord record) {
|
||||||
|
|
@ -465,38 +556,48 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
return Boolean.parseBoolean(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ENABLED, "false"));
|
return Boolean.parseBoolean(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ENABLED, "false"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isBalanceEnforced() {
|
||||||
|
return Boolean.parseBoolean(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ENFORCE_BALANCE, "false"));
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveAccountMode() {
|
private String resolveAccountMode() {
|
||||||
String configured = sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ACCOUNT_MODE, ACCOUNT_MODE_PUBLIC);
|
String configured = sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ACCOUNT_MODE, ACCOUNT_MODE_PUBLIC);
|
||||||
if (configured == null || configured.isBlank()) {
|
if (!StringUtils.hasText(configured)) {
|
||||||
return ACCOUNT_MODE_PUBLIC;
|
return ACCOUNT_MODE_PUBLIC;
|
||||||
}
|
}
|
||||||
String normalized = configured.trim().toUpperCase();
|
String normalized = configured.trim().toUpperCase();
|
||||||
if (ACCOUNT_MODE_PERSONAL.equals(normalized)) {
|
if (ACCOUNT_MODE_PERSONAL.equals(normalized) || ACCOUNT_MODE_BOTH.equals(normalized)) {
|
||||||
return ACCOUNT_MODE_PERSONAL;
|
return normalized;
|
||||||
}
|
}
|
||||||
return ACCOUNT_MODE_PUBLIC;
|
return ACCOUNT_MODE_PUBLIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChargeAccountSnapshot resolveChargeAccountSnapshot(Meeting meeting) {
|
private String resolveChargePriority() {
|
||||||
String accountMode = resolveAccountMode();
|
String configured = sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_CHARGE_PRIORITY, CHARGE_PRIORITY_PERSONAL_FIRST);
|
||||||
if (ACCOUNT_MODE_PERSONAL.equals(accountMode)) {
|
if (!StringUtils.hasText(configured)) {
|
||||||
Long ownerUserId = meeting.getCreatorId() == null ? UNIFIED_ACCOUNT_USER_ID : meeting.getCreatorId();
|
return CHARGE_PRIORITY_PERSONAL_FIRST;
|
||||||
return new ChargeAccountSnapshot(ACCOUNT_MODE_PERSONAL, ownerUserId);
|
|
||||||
}
|
}
|
||||||
return new ChargeAccountSnapshot(ACCOUNT_MODE_PUBLIC, UNIFIED_ACCOUNT_USER_ID);
|
String normalized = configured.trim().toUpperCase();
|
||||||
|
if (CHARGE_PRIORITY_PUBLIC_FIRST.equals(normalized)) {
|
||||||
|
return CHARGE_PRIORITY_PUBLIC_FIRST;
|
||||||
|
}
|
||||||
|
return CHARGE_PRIORITY_PERSONAL_FIRST;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long resolveVisibleTotalBalance(String accountMode, long publicBalance, long personalBalance) {
|
||||||
|
if (ACCOUNT_MODE_PUBLIC.equals(accountMode)) {
|
||||||
|
return publicBalance;
|
||||||
|
}
|
||||||
|
if (ACCOUNT_MODE_PERSONAL.equals(accountMode)) {
|
||||||
|
return personalBalance;
|
||||||
|
}
|
||||||
|
return publicBalance + personalBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int toChargedMinutes(int durationSeconds) {
|
private int toChargedMinutes(int durationSeconds) {
|
||||||
return (int) Math.ceil(durationSeconds / 60.0d);
|
return (int) Math.ceil(durationSeconds / 60.0d);
|
||||||
}
|
}
|
||||||
|
|
||||||
private long toCeilMinutes(long durationSeconds) {
|
|
||||||
if (durationSeconds <= 0L) {
|
|
||||||
return 0L;
|
|
||||||
}
|
|
||||||
return (long) Math.ceil(durationSeconds / 60.0d);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int positiveInt(String rawValue, int defaultValue) {
|
private int positiveInt(String rawValue, int defaultValue) {
|
||||||
try {
|
try {
|
||||||
int value = Integer.parseInt(String.valueOf(rawValue).trim());
|
int value = Integer.parseInt(String.valueOf(rawValue).trim());
|
||||||
|
|
@ -535,7 +636,10 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
||||||
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ChargeAccountSnapshot(String accountType, Long accountUserId) {
|
private record ChargeTarget(String accountType, Long accountUserId, MeetingPointsAccount account) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ChargeExecutionResult(long chargedPoints, long totalBalanceBefore, long totalBalanceAfter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ChargeSnapshot(
|
private record ChargeSnapshot(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package com.imeeting.service.biz.impl;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
||||||
import com.imeeting.entity.biz.MeetingUserStats;
|
|
||||||
import com.imeeting.mapper.biz.MeetingUserStatsMapper;
|
|
||||||
import com.imeeting.service.biz.MeetingUserStatsService;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class MeetingUserStatsServiceImpl extends ServiceImpl<MeetingUserStatsMapper, MeetingUserStats> implements MeetingUserStatsService {
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
package com.imeeting.service.biz.impl;
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
|
import com.imeeting.service.biz.LicenseService;
|
||||||
|
import com.imeeting.service.biz.MeetingPointsService;
|
||||||
import com.unisbase.dto.CreateTenantDTO;
|
import com.unisbase.dto.CreateTenantDTO;
|
||||||
import com.unisbase.dto.PageResult;
|
import com.unisbase.dto.PageResult;
|
||||||
import com.unisbase.dto.SysTenantDTO;
|
import com.unisbase.dto.SysTenantDTO;
|
||||||
import com.unisbase.service.SysTenantService;
|
import com.unisbase.service.SysTenantService;
|
||||||
import com.unisbase.service.TenantManagementService;
|
import com.unisbase.service.TenantManagementService;
|
||||||
import com.unisbase.service.TenantModeService;
|
import com.unisbase.service.TenantModeService;
|
||||||
import com.imeeting.service.biz.LicenseService;
|
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.context.annotation.Primary;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
@ -20,13 +21,16 @@ public class TenantManagementServicePrimaryImpl implements TenantManagementServi
|
||||||
private final SysTenantService sysTenantService;
|
private final SysTenantService sysTenantService;
|
||||||
private final TenantModeService tenantModeService;
|
private final TenantModeService tenantModeService;
|
||||||
private final LicenseService licenseService;
|
private final LicenseService licenseService;
|
||||||
|
private final MeetingPointsService meetingPointsService;
|
||||||
|
|
||||||
public TenantManagementServicePrimaryImpl(SysTenantService sysTenantService,
|
public TenantManagementServicePrimaryImpl(SysTenantService sysTenantService,
|
||||||
TenantModeService tenantModeService,
|
TenantModeService tenantModeService,
|
||||||
LicenseService licenseService) {
|
LicenseService licenseService,
|
||||||
|
MeetingPointsService meetingPointsService) {
|
||||||
this.sysTenantService = sysTenantService;
|
this.sysTenantService = sysTenantService;
|
||||||
this.tenantModeService = tenantModeService;
|
this.tenantModeService = tenantModeService;
|
||||||
this.licenseService = licenseService;
|
this.licenseService = licenseService;
|
||||||
|
this.meetingPointsService = meetingPointsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -52,6 +56,7 @@ public class TenantManagementServicePrimaryImpl implements TenantManagementServi
|
||||||
tenantModeService.assertTenantLifecycleAllowed();
|
tenantModeService.assertTenantLifecycleAllowed();
|
||||||
Long tenantId = sysTenantService.createTenantWithAdmin(tenant);
|
Long tenantId = sysTenantService.createTenantWithAdmin(tenant);
|
||||||
licenseService.initializeTemporaryLicenses(tenantId);
|
licenseService.initializeTemporaryLicenses(tenantId);
|
||||||
|
meetingPointsService.initializeTenantPointsAccount(tenantId);
|
||||||
return tenantId;
|
return tenantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ unisbase:
|
||||||
- biz_prompt_templates
|
- biz_prompt_templates
|
||||||
- biz_meeting_transcript_chapter_versions
|
- biz_meeting_transcript_chapter_versions
|
||||||
- biz_meeting_transcript_chapters
|
- biz_meeting_transcript_chapters
|
||||||
|
- biz_android_push_message
|
||||||
- biz_client_downloads
|
- biz_client_downloads
|
||||||
- biz_external_apps
|
- biz_external_apps
|
||||||
security:
|
security:
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,26 @@ import http from "../http";
|
||||||
|
|
||||||
export interface MeetingPointsOverviewVO {
|
export interface MeetingPointsOverviewVO {
|
||||||
accountMode: string;
|
accountMode: string;
|
||||||
|
chargePriority: string;
|
||||||
publicBalance: number;
|
publicBalance: number;
|
||||||
publicTotalPointsUsed: number;
|
publicTotalPointsUsed: number;
|
||||||
|
personalBalance: number;
|
||||||
|
personalTotalPointsUsed: number;
|
||||||
|
totalAvailableBalance: number;
|
||||||
totalChargeCount: number;
|
totalChargeCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MeetingPointsChargeItemVO {
|
||||||
|
id: number;
|
||||||
|
chargeStage: string;
|
||||||
|
accountType: string;
|
||||||
|
accountUserId: number;
|
||||||
|
priorityOrder: number;
|
||||||
|
chargedPoints: number;
|
||||||
|
balanceBefore: number;
|
||||||
|
balanceAfter: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MeetingPointsLedgerListItemVO {
|
export interface MeetingPointsLedgerListItemVO {
|
||||||
id: number;
|
id: number;
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
|
|
@ -16,7 +31,7 @@ export interface MeetingPointsLedgerListItemVO {
|
||||||
ownerUserId?: number;
|
ownerUserId?: number;
|
||||||
ownerUserName?: string;
|
ownerUserName?: string;
|
||||||
chargeAccountType?: string;
|
chargeAccountType?: string;
|
||||||
pointsType: "ASR" | "LLM" | "INIT" | "RECHARGE";
|
pointsType: "ASR" | "LLM";
|
||||||
consumedPoints: number;
|
consumedPoints: number;
|
||||||
balanceBefore?: number;
|
balanceBefore?: number;
|
||||||
balanceAfter?: number;
|
balanceAfter?: number;
|
||||||
|
|
@ -56,6 +71,7 @@ export interface MeetingPointsLedgerDetailVO {
|
||||||
asrChargedAt?: string;
|
asrChargedAt?: string;
|
||||||
llmChargedAt?: string;
|
llmChargedAt?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
|
chargeItems?: MeetingPointsChargeItemVO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMeetingPointsOverview() {
|
export async function getMeetingPointsOverview() {
|
||||||
|
|
@ -77,3 +93,12 @@ export async function getMeetingPointsLedgerDetail(ledgerId: number) {
|
||||||
const resp = await http.get(`/api/biz/meeting-points/management/ledgers/${ledgerId}`);
|
const resp = await http.get(`/api/biz/meeting-points/management/ledgers/${ledgerId}`);
|
||||||
return resp.data.data as MeetingPointsLedgerDetailVO;
|
return resp.data.data as MeetingPointsLedgerDetailVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function transferMeetingPoints(payload: {
|
||||||
|
targetUserId: number;
|
||||||
|
points: number;
|
||||||
|
remark?: string;
|
||||||
|
}) {
|
||||||
|
const resp = await http.post("/api/biz/meeting-points/transfer", payload);
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,23 @@
|
||||||
import { Alert, Button, Card, Col, Descriptions, Input, Modal, Row, Select, Space, Statistic, Tag, Typography, message } from "antd";
|
import { EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
||||||
import { EyeOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
import { listUsers } from "@/api";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Descriptions,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Statistic,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from "antd";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import PageContainer from "@/components/shared/PageContainer";
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||||
|
|
@ -8,10 +26,13 @@ import {
|
||||||
getMeetingPointsLedgerDetail,
|
getMeetingPointsLedgerDetail,
|
||||||
getMeetingPointsLedgerPage,
|
getMeetingPointsLedgerPage,
|
||||||
getMeetingPointsOverview,
|
getMeetingPointsOverview,
|
||||||
|
transferMeetingPoints,
|
||||||
|
type MeetingPointsChargeItemVO,
|
||||||
type MeetingPointsLedgerDetailVO,
|
type MeetingPointsLedgerDetailVO,
|
||||||
type MeetingPointsLedgerListItemVO,
|
type MeetingPointsLedgerListItemVO,
|
||||||
type MeetingPointsOverviewVO,
|
type MeetingPointsOverviewVO,
|
||||||
} from "@/api/business/meetingPoints";
|
} from "@/api/business/meetingPoints";
|
||||||
|
import type { SysUser } from "@/types";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -22,45 +43,39 @@ const POINTS_TYPE_OPTIONS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
function getAccountModeLabel(mode?: string) {
|
function getAccountModeLabel(mode?: string) {
|
||||||
return mode === "PERSONAL" ? "个人账户" : "公共账户";
|
if (mode === "PERSONAL") return "个人账户";
|
||||||
|
if (mode === "BOTH") return "公共和个人共存";
|
||||||
|
return "公共账户";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChargePriorityLabel(priority?: string) {
|
||||||
|
return priority === "PUBLIC_FIRST" ? "公共优先" : "个人优先";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccountTypeLabel(type?: string) {
|
||||||
|
return type === "PERSONAL" ? "个人账户" : "公共账户";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPointsTypeLabel(value?: string) {
|
function getPointsTypeLabel(value?: string) {
|
||||||
if (value === "ASR") {
|
if (value === "ASR") return "转录";
|
||||||
return "转录";
|
if (value === "LLM") return "总结";
|
||||||
}
|
if (value === "TRANSFER_OUT") return "转出";
|
||||||
if (value === "LLM") {
|
if (value === "TRANSFER_IN") return "转入";
|
||||||
return "总结";
|
if (value === "INIT") return "初始化";
|
||||||
}
|
|
||||||
if (value === "INIT") {
|
|
||||||
return "初始化";
|
|
||||||
}
|
|
||||||
if (value === "RECHARGE") {
|
|
||||||
return "充值";
|
|
||||||
}
|
|
||||||
return value || "-";
|
return value || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPointsTypeColor(value?: string) {
|
function getPointsTypeColor(value?: string) {
|
||||||
if (value === "ASR") {
|
if (value === "ASR") return "blue";
|
||||||
return "blue";
|
if (value === "LLM") return "purple";
|
||||||
}
|
if (value === "TRANSFER_IN") return "green";
|
||||||
if (value === "LLM") {
|
if (value === "TRANSFER_OUT") return "orange";
|
||||||
return "purple";
|
|
||||||
}
|
|
||||||
if (value === "RECHARGE") {
|
|
||||||
return "green";
|
|
||||||
}
|
|
||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChargeTriggerLabel(value?: string) {
|
function getChargeTriggerLabel(value?: string) {
|
||||||
if (value === "RESUMMARY") {
|
if (value === "RESUMMARY") return "重新总结";
|
||||||
return "重新总结";
|
if (value === "AUTO_SUMMARY") return "自动总结";
|
||||||
}
|
|
||||||
if (value === "AUTO_SUMMARY") {
|
|
||||||
return "自动总结";
|
|
||||||
}
|
|
||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,22 +87,31 @@ export default function MeetingPointsManagement() {
|
||||||
const [overview, setOverview] = useState<MeetingPointsOverviewVO | null>(null);
|
const [overview, setOverview] = useState<MeetingPointsOverviewVO | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [detailLoading, setDetailLoading] = useState(false);
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
|
const [transferLoading, setTransferLoading] = useState(false);
|
||||||
const [records, setRecords] = useState<MeetingPointsLedgerListItemVO[]>([]);
|
const [records, setRecords] = useState<MeetingPointsLedgerListItemVO[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [detailOpen, setDetailOpen] = useState(false);
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
|
const [transferOpen, setTransferOpen] = useState(false);
|
||||||
const [detail, setDetail] = useState<MeetingPointsLedgerDetailVO | null>(null);
|
const [detail, setDetail] = useState<MeetingPointsLedgerDetailVO | null>(null);
|
||||||
|
const [users, setUsers] = useState<SysUser[]>([]);
|
||||||
const [params, setParams] = useState({
|
const [params, setParams] = useState({
|
||||||
current: 1,
|
current: 1,
|
||||||
size: 20,
|
size: 20,
|
||||||
username: "",
|
username: "",
|
||||||
pointsType: "",
|
pointsType: "",
|
||||||
});
|
});
|
||||||
|
const [transferForm] = Form.useForm();
|
||||||
|
|
||||||
const loadOverview = async () => {
|
const loadOverview = async () => {
|
||||||
const data = await getMeetingPointsOverview();
|
const data = await getMeetingPointsOverview();
|
||||||
setOverview(data);
|
setOverview(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
const data = await listUsers();
|
||||||
|
setUsers(data || []);
|
||||||
|
};
|
||||||
|
|
||||||
const loadPage = async (nextParams = params) => {
|
const loadPage = async (nextParams = params) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -100,8 +124,7 @@ export default function MeetingPointsManagement() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadOverview();
|
void Promise.all([loadOverview(), loadPage(), loadUsers()]);
|
||||||
void loadPage();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
|
|
@ -122,7 +145,7 @@ export default function MeetingPointsManagement() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
await Promise.all([loadOverview(), loadPage()]);
|
await Promise.all([loadOverview(), loadPage(), loadUsers()]);
|
||||||
message.success("已刷新积分数据");
|
message.success("已刷新积分数据");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -137,15 +160,36 @@ export default function MeetingPointsManagement() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTransferSubmit = async () => {
|
||||||
|
const values = await transferForm.validateFields();
|
||||||
|
setTransferLoading(true);
|
||||||
|
try {
|
||||||
|
await transferMeetingPoints(values);
|
||||||
|
message.success("积分分配成功");
|
||||||
|
setTransferOpen(false);
|
||||||
|
transferForm.resetFields();
|
||||||
|
await Promise.all([loadOverview(), loadPage()]);
|
||||||
|
} finally {
|
||||||
|
setTransferLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
title: "用户名",
|
title: "用户",
|
||||||
dataIndex: "ownerUserName",
|
dataIndex: "ownerUserName",
|
||||||
key: "ownerUserName",
|
key: "ownerUserName",
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (value: string) => <Text strong>{value || "-"}</Text>,
|
render: (value: string) => <Text strong>{value || "-"}</Text>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "扣费账户",
|
||||||
|
dataIndex: "chargeAccountType",
|
||||||
|
key: "chargeAccountType",
|
||||||
|
width: 120,
|
||||||
|
render: (value: string) => <Tag>{getAccountTypeLabel(value)}</Tag>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "消耗类型",
|
title: "消耗类型",
|
||||||
dataIndex: "pointsType",
|
dataIndex: "pointsType",
|
||||||
|
|
@ -196,13 +240,55 @@ export default function MeetingPointsManagement() {
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chargeItemColumns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: "阶段",
|
||||||
|
dataIndex: "chargeStage",
|
||||||
|
key: "chargeStage",
|
||||||
|
render: (value: string) => <Tag color={getPointsTypeColor(value)}>{getPointsTypeLabel(value)}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "扣费账户",
|
||||||
|
dataIndex: "accountType",
|
||||||
|
key: "accountType",
|
||||||
|
render: (value: string) => getAccountTypeLabel(value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "账户用户ID",
|
||||||
|
dataIndex: "accountUserId",
|
||||||
|
key: "accountUserId",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "扣费积分",
|
||||||
|
dataIndex: "chargedPoints",
|
||||||
|
key: "chargedPoints",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "扣费前余额",
|
||||||
|
dataIndex: "balanceBefore",
|
||||||
|
key: "balanceBefore",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "扣费后余额",
|
||||||
|
dataIndex: "balanceAfter",
|
||||||
|
key: "balanceAfter",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
title="积分管理"
|
title="积分管理"
|
||||||
subtitle="当前页面展示余额与消耗流水"
|
subtitle="查看公共账户、个人账户和总结扣费流水"
|
||||||
headerExtra={
|
headerExtra={
|
||||||
<Space>
|
<Space>
|
||||||
{/*<Tag color="processing">当前结算模式:{getAccountModeLabel(overview?.accountMode)}</Tag>*/}
|
<Tag color="processing">当前模式:{getAccountModeLabel(overview?.accountMode)}</Tag>
|
||||||
|
<Tag color="blue">优先级:{getChargePriorityLabel(overview?.chargePriority)}</Tag>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={() => setTransferOpen(true)}>
|
||||||
|
分配积分
|
||||||
|
</Button>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -231,21 +317,20 @@ export default function MeetingPointsManagement() {
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||||
{/*<Col xs={24} md={6}>*/}
|
|
||||||
{/* <Card>*/}
|
|
||||||
{/* <Statistic title="当前结算账户" value={getAccountModeLabel(overview?.accountMode)} />*/}
|
|
||||||
{/* </Card>*/}
|
|
||||||
{/*</Col>*/}
|
|
||||||
<Col xs={24} md={6}>
|
<Col xs={24} md={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic title="剩余总积分" value={overview?.publicBalance ?? 0} />
|
<Statistic title="当前模式可用总积分" value={overview?.totalAvailableBalance ?? 0} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={6}>
|
<Col xs={24} md={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic title="累计消耗总积分" value={overview?.publicTotalPointsUsed ?? 0} />
|
<Statistic title="公共账户余额" value={overview?.publicBalance ?? 0} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="个人账户余额汇总" value={overview?.personalBalance ?? 0} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={6}>
|
<Col xs={24} md={6}>
|
||||||
|
|
@ -255,15 +340,32 @@ export default function MeetingPointsManagement() {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: 0, display: "flex", flexDirection: "column", minHeight: 0 } }}>
|
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="公共账户累计消耗积分" value={overview?.publicTotalPointsUsed ?? 0} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Card>
|
||||||
|
<Statistic title="个人账户累计消耗积分汇总" value={overview?.personalTotalPointsUsed ?? 0} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className="app-page__content-card"
|
||||||
|
style={{ flex: 1, minHeight: 0 }}
|
||||||
|
styles={{ body: { padding: 0, display: "flex", flexDirection: "column", minHeight: 0 } }}
|
||||||
|
>
|
||||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||||
<ListTable
|
<ListTable
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
columns={columns as any}
|
columns={columns as never}
|
||||||
dataSource={records}
|
dataSource={records}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
totalCount={total}
|
totalCount={total}
|
||||||
scroll={{ y: "calc(100vh - 430px)", x: 1100 }}
|
scroll={{ y: "calc(100vh - 470px)", x: 1200 }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -280,7 +382,7 @@ export default function MeetingPointsManagement() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="消耗详情"
|
title="积分流水详情"
|
||||||
open={detailOpen}
|
open={detailOpen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setDetailOpen(false);
|
setDetailOpen(false);
|
||||||
|
|
@ -297,36 +399,72 @@ export default function MeetingPointsManagement() {
|
||||||
关闭
|
关闭
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
width={720}
|
width={900}
|
||||||
confirmLoading={detailLoading}
|
confirmLoading={detailLoading}
|
||||||
>
|
>
|
||||||
{detail && (
|
{detail && (
|
||||||
<Descriptions bordered column={1} size="small">
|
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||||
<Descriptions.Item label="用户名">{detail.ownerUserName || "-"}</Descriptions.Item>
|
<Descriptions bordered column={2} size="small">
|
||||||
{/*<Descriptions.Item label="扣费账户">{getAccountModeLabel(detail.chargeAccountType)}</Descriptions.Item>*/}
|
<Descriptions.Item label="用户">{detail.ownerUserName || "-"}</Descriptions.Item>
|
||||||
<Descriptions.Item label="消耗类型">{getPointsTypeLabel(detail.pointsType)}</Descriptions.Item>
|
<Descriptions.Item label="当前流水账户">{getAccountTypeLabel(detail.chargeAccountType)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="消耗积分">{detail.consumedPoints ?? 0}</Descriptions.Item>
|
<Descriptions.Item label="消耗类型">{getPointsTypeLabel(detail.pointsType)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="消耗时间">{formatDateTime(detail.createdAt)}</Descriptions.Item>
|
<Descriptions.Item label="消耗积分">{detail.consumedPoints ?? 0}</Descriptions.Item>
|
||||||
<Descriptions.Item label="会议标题">{detail.meetingTitle || "-"}</Descriptions.Item>
|
<Descriptions.Item label="消耗时间">{formatDateTime(detail.createdAt)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="触发类型">{getChargeTriggerLabel(detail.chargeTriggerType)}</Descriptions.Item>
|
<Descriptions.Item label="会议标题">{detail.meetingTitle || "-"}</Descriptions.Item>
|
||||||
<Descriptions.Item label="录音时长(秒)">{detail.audioDurationSeconds ?? "-"}</Descriptions.Item>
|
<Descriptions.Item label="触发类型">{getChargeTriggerLabel(detail.chargeTriggerType)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="计费分钟数">{detail.chargedMinutes ?? "-"}</Descriptions.Item>
|
<Descriptions.Item label="录音时长(秒)">{detail.audioDurationSeconds ?? "-"}</Descriptions.Item>
|
||||||
{/*<Descriptions.Item label="计费单位数">{detail.billingUnits ?? "-"}</Descriptions.Item>*/}
|
<Descriptions.Item label="计费分钟数">{detail.chargedMinutes ?? "-"}</Descriptions.Item>
|
||||||
{/*<Descriptions.Item label="单位分钟数">{detail.unitMinutesSnapshot ?? "-"}</Descriptions.Item>*/}
|
<Descriptions.Item label="应计总积分">{detail.totalPoints ?? 0}</Descriptions.Item>
|
||||||
{/*<Descriptions.Item label="单位价格">{detail.costPerUnitSnapshot ?? "-"}</Descriptions.Item>*/}
|
<Descriptions.Item label="已扣总积分">{detail.chargedTotalPoints ?? 0}</Descriptions.Item>
|
||||||
{/*<Descriptions.Item label="应计总积分">{detail.totalPoints ?? 0}</Descriptions.Item>*/}
|
<Descriptions.Item label="扣费前余额">{detail.balanceBefore ?? "-"}</Descriptions.Item>
|
||||||
{/*<Descriptions.Item label="已扣总积分">{detail.chargedTotalPoints ?? 0}</Descriptions.Item>*/}
|
<Descriptions.Item label="扣费后余额">{detail.balanceAfter ?? "-"}</Descriptions.Item>
|
||||||
{/*<Descriptions.Item label="应计 ASR 积分">{detail.asrPoints ?? 0}</Descriptions.Item>*/}
|
</Descriptions>
|
||||||
{/*<Descriptions.Item label="已扣 ASR 积分">{detail.chargedAsrPoints ?? 0}</Descriptions.Item>*/}
|
|
||||||
{/*<Descriptions.Item label="应计 LLM 积分">{detail.llmPoints ?? 0}</Descriptions.Item>*/}
|
<Card title="本次总结扣费明细" size="small">
|
||||||
{/*<Descriptions.Item label="已扣 LLM 积分">{detail.chargedLlmPoints ?? 0}</Descriptions.Item>*/}
|
<Table<MeetingPointsChargeItemVO>
|
||||||
<Descriptions.Item label="扣费前余额">{detail.balanceBefore ?? "-"}</Descriptions.Item>
|
rowKey="id"
|
||||||
<Descriptions.Item label="扣费后余额">{detail.balanceAfter ?? "-"}</Descriptions.Item>
|
size="small"
|
||||||
{/*<Descriptions.Item label="记录状态">{detail.summaryStatus || "-"}</Descriptions.Item>*/}
|
pagination={false}
|
||||||
{/*<Descriptions.Item label="失败原因">{detail.failureReason || "-"}</Descriptions.Item>*/}
|
columns={chargeItemColumns as never}
|
||||||
</Descriptions>
|
dataSource={detail.chargeItems || []}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="从公共账户分配积分"
|
||||||
|
open={transferOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setTransferOpen(false);
|
||||||
|
transferForm.resetFields();
|
||||||
|
}}
|
||||||
|
onOk={() => void handleTransferSubmit()}
|
||||||
|
confirmLoading={transferLoading}
|
||||||
|
>
|
||||||
|
<Form form={transferForm} layout="vertical">
|
||||||
|
<Form.Item name="targetUserId" label="目标用户" rules={[{ required: true, message: "请选择目标用户" }]}>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
placeholder="请选择用户"
|
||||||
|
options={users
|
||||||
|
.filter((user) => user.userId && user.userId > 0)
|
||||||
|
.map((user) => ({
|
||||||
|
label: `${user.displayName || user.username} (#${user.userId})`,
|
||||||
|
value: user.userId,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="points" label="分配积分" rules={[{ required: true, message: "请输入分配积分" }]}>
|
||||||
|
<InputNumber min={1} precision={0} style={{ width: "100%" }} placeholder="请输入正整数积分" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="remark" label="备注">
|
||||||
|
<Input placeholder="可选,默认记为管理员从公共账户分配积分" maxLength={200} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue