feat: 添加租户积分校验设置和前端页面优化
- 在 `MeetingPointsServiceImpl` 中新增租户积分校验逻辑,支持无限余额模式 - 更新 `MeetingPointsManagement` 页面,移除不必要的组件和样式,优化统计卡片和表格布局 - 新增 `TenantMeetingPointsSettings` 页面,用于管理租户积分校验设置dev_na
parent
e330edc965
commit
6e4d10427a
|
|
@ -44,8 +44,6 @@ public final class SysParamKeys {
|
|||
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";
|
||||
/** 临时授权默认有效期,单位月。 */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.TenantMeetingPointsSettingVO;
|
||||
import com.imeeting.dto.biz.UpdateTenantMeetingPointsBalanceCheckCommand;
|
||||
import com.imeeting.service.biz.TenantMeetingPointsManagementService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "租户积分余额校验")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/tenant-meeting-points/settings")
|
||||
public class TenantMeetingPointsSettingController {
|
||||
private final TenantMeetingPointsManagementService tenantMeetingPointsManagementService;
|
||||
|
||||
public TenantMeetingPointsSettingController(TenantMeetingPointsManagementService tenantMeetingPointsManagementService) {
|
||||
this.tenantMeetingPointsManagementService = tenantMeetingPointsManagementService;
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询租户积分余额校验配置")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<TenantMeetingPointsSettingVO>>> pageSettings(
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size,
|
||||
@RequestParam(required = false) String tenantName,
|
||||
@RequestParam(required = false) String tenantCode,
|
||||
@RequestParam(required = false) Boolean balanceCheckEnabled) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
ensurePlatformAdmin(loginUser);
|
||||
return ApiResponse.ok(tenantMeetingPointsManagementService.pageSettings(current, size, tenantName, tenantCode, balanceCheckEnabled));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取当前租户积分余额校验配置")
|
||||
@GetMapping("/current")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<TenantMeetingPointsSettingVO> getCurrentSetting() {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
ensureAdmin(loginUser);
|
||||
return ApiResponse.ok(tenantMeetingPointsManagementService.getCurrentTenantSetting(loginUser.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新租户积分余额校验开关")
|
||||
@PutMapping("/{tenantId}/balance-check")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<TenantMeetingPointsSettingVO> updateBalanceCheck(@PathVariable Long tenantId,
|
||||
@Valid @RequestBody UpdateTenantMeetingPointsBalanceCheckCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
ensureCanManageTenant(loginUser, tenantId);
|
||||
if (command == null || command.getBalanceCheckEnabled() == null) {
|
||||
throw new RuntimeException("余额校验开关不能为空");
|
||||
}
|
||||
return ApiResponse.ok(tenantMeetingPointsManagementService.updateBalanceCheck(
|
||||
tenantId,
|
||||
command.getBalanceCheckEnabled(),
|
||||
command.getRemark(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getDisplayName()
|
||||
));
|
||||
}
|
||||
|
||||
private void ensureCanManageTenant(LoginUser loginUser, Long tenantId) {
|
||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||
return;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||
throw new RuntimeException("无权限执行该操作");
|
||||
}
|
||||
if (tenantId == null || !tenantId.equals(loginUser.getTenantId())) {
|
||||
throw new RuntimeException("租户管理员只能操作当前租户");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureAdmin(LoginUser loginUser) {
|
||||
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||
throw new RuntimeException("无权限执行该操作");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePlatformAdmin(LoginUser loginUser) {
|
||||
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||
throw new RuntimeException("仅平台管理员可查看租户列表");
|
||||
}
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,9 @@ public class MeetingPointsBalanceVO {
|
|||
@Schema(description = "当前扣费优先级:PERSONAL_FIRST / PUBLIC_FIRST")
|
||||
private String chargePriority;
|
||||
|
||||
@Schema(description = "是否启用余额校验")
|
||||
private Boolean balanceCheckEnabled;
|
||||
|
||||
@Schema(description = "公共账户余额")
|
||||
private Long publicBalance;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ public class MeetingPointsOverviewVO {
|
|||
@Schema(description = "当前扣费优先级:PERSONAL_FIRST / PUBLIC_FIRST")
|
||||
private String chargePriority;
|
||||
|
||||
@Schema(description = "是否启用余额校验")
|
||||
private Boolean balanceCheckEnabled;
|
||||
|
||||
@Schema(description = "公共账户余额")
|
||||
private Long publicBalance;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "个人积分账户概览")
|
||||
public class MeetingPointsPersonalAccountVO {
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "展示名称")
|
||||
private String displayName;
|
||||
|
||||
@Schema(description = "当前积分余额")
|
||||
private Long currentBalance;
|
||||
|
||||
@Schema(description = "累计消耗积分")
|
||||
private Long totalPointsUsed;
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(description = "租户积分余额校验配置视图")
|
||||
public class TenantMeetingPointsSettingVO {
|
||||
@Schema(description = "租户ID")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "租户编码")
|
||||
private String tenantCode;
|
||||
|
||||
@Schema(description = "租户名称")
|
||||
private String tenantName;
|
||||
|
||||
@Schema(description = "是否启用余额校验")
|
||||
private Boolean balanceCheckEnabled;
|
||||
|
||||
@Schema(description = "是否处于无限余额模式")
|
||||
private Boolean unlimitedBalanceMode;
|
||||
|
||||
@Schema(description = "公共账户账面余额")
|
||||
private Long publicBalance;
|
||||
|
||||
@Schema(description = "公共账户累计消耗积分")
|
||||
private Long publicTotalPointsUsed;
|
||||
|
||||
@Schema(description = "最近一次切换时间")
|
||||
private LocalDateTime lastSwitchAt;
|
||||
|
||||
@Schema(description = "最近一次切换操作人名称")
|
||||
private String lastSwitchByName;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "更新租户积分余额校验开关请求")
|
||||
public class UpdateTenantMeetingPointsBalanceCheckCommand {
|
||||
@NotNull(message = "余额校验开关不能为空")
|
||||
@Schema(description = "是否启用余额校验:true-启用,false-关闭", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean balanceCheckEnabled;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -41,6 +41,9 @@ public class MeetingPointsLedger extends BaseEntity {
|
|||
@Schema(description = "变动后余额")
|
||||
private Long balanceAfter;
|
||||
|
||||
@Schema(description = "余额校验快照:1-校验余额,0-无限余额模式")
|
||||
private Integer balanceCheckEnabledSnapshot;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@ public class MeetingSummaryChargeRecord extends BaseEntity {
|
|||
@Schema(description = "积分模式是否开启")
|
||||
private Integer pointsModeEnabled;
|
||||
|
||||
@Schema(description = "余额校验快照:1-校验余额,0-无限余额模式")
|
||||
private Integer balanceCheckEnabledSnapshot;
|
||||
|
||||
@Schema(description = "阻塞原因")
|
||||
private String blockedReason;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
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;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "租户积分余额校验配置实体")
|
||||
@TableName("biz_meeting_points_tenant_settings")
|
||||
public class TenantMeetingPointsSetting extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "是否启用余额校验:1-启用,0-关闭")
|
||||
private Integer balanceCheckEnabled;
|
||||
|
||||
@Schema(description = "最近一次切换时间")
|
||||
private LocalDateTime lastSwitchAt;
|
||||
|
||||
@Schema(description = "最近一次切换操作人ID")
|
||||
private Long lastSwitchBy;
|
||||
|
||||
@Schema(description = "最近一次切换操作人名称")
|
||||
private String lastSwitchByName;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.imeeting.mapper.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.imeeting.entity.biz.TenantMeetingPointsSetting;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface TenantMeetingPointsSettingMapper extends BaseMapper<TenantMeetingPointsSetting> {
|
||||
@Select("""
|
||||
SELECT *
|
||||
FROM biz_meeting_points_tenant_settings
|
||||
WHERE tenant_id = #{tenantId}
|
||||
AND is_deleted = 0
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
""")
|
||||
TenantMeetingPointsSetting selectForUpdate(@Param("tenantId") Long tenantId);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.imeeting.service.biz;
|
||||
|
||||
import com.imeeting.dto.biz.TenantMeetingPointsSettingVO;
|
||||
import com.unisbase.dto.PageResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TenantMeetingPointsManagementService {
|
||||
PageResult<List<TenantMeetingPointsSettingVO>> pageSettings(Integer current,
|
||||
Integer size,
|
||||
String tenantName,
|
||||
String tenantCode,
|
||||
Boolean balanceCheckEnabled);
|
||||
|
||||
TenantMeetingPointsSettingVO getCurrentTenantSetting(Long tenantId);
|
||||
|
||||
TenantMeetingPointsSettingVO updateBalanceCheck(Long tenantId,
|
||||
boolean balanceCheckEnabled,
|
||||
String remark,
|
||||
Long operatorUserId,
|
||||
String operatorName);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.imeeting.service.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.imeeting.entity.biz.TenantMeetingPointsSetting;
|
||||
|
||||
public interface TenantMeetingPointsSettingService extends IService<TenantMeetingPointsSetting> {
|
||||
void initializeTenantSetting(Long tenantId);
|
||||
|
||||
boolean isBalanceCheckEnabled(Long tenantId);
|
||||
|
||||
TenantMeetingPointsSetting getByTenantId(Long tenantId);
|
||||
|
||||
TenantMeetingPointsSetting getByTenantIdForUpdate(Long tenantId);
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import com.imeeting.service.biz.MeetingPointsLedgerService;
|
|||
import com.imeeting.service.biz.MeetingPointsQueryService;
|
||||
import com.imeeting.service.biz.MeetingSummaryChargeRecordService;
|
||||
import com.imeeting.service.biz.MeetingService;
|
||||
import com.imeeting.service.biz.TenantMeetingPointsSettingService;
|
||||
import com.unisbase.dto.DataScopeRuleDTO;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.entity.SysUser;
|
||||
|
|
@ -54,6 +55,7 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
|||
private final SysUserMapper sysUserMapper;
|
||||
private final DataScopeService dataScopeService;
|
||||
private final SysParamService sysParamService;
|
||||
private final TenantMeetingPointsSettingService tenantMeetingPointsSettingService;
|
||||
|
||||
public MeetingPointsQueryServiceImpl(MeetingPointsAccountService meetingPointsAccountService,
|
||||
MeetingPointsLedgerService meetingPointsLedgerService,
|
||||
|
|
@ -61,7 +63,8 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
|||
MeetingService meetingService,
|
||||
SysUserMapper sysUserMapper,
|
||||
DataScopeService dataScopeService,
|
||||
SysParamService sysParamService) {
|
||||
SysParamService sysParamService,
|
||||
TenantMeetingPointsSettingService tenantMeetingPointsSettingService) {
|
||||
this.meetingPointsAccountService = meetingPointsAccountService;
|
||||
this.meetingPointsLedgerService = meetingPointsLedgerService;
|
||||
this.meetingSummaryChargeRecordService = meetingSummaryChargeRecordService;
|
||||
|
|
@ -69,12 +72,14 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
|||
this.sysUserMapper = sysUserMapper;
|
||||
this.dataScopeService = dataScopeService;
|
||||
this.sysParamService = sysParamService;
|
||||
this.tenantMeetingPointsSettingService = tenantMeetingPointsSettingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MeetingPointsOverviewVO getOverview(Long tenantId, Long userId, boolean isAdmin) {
|
||||
String accountMode = resolveAccountMode();
|
||||
String chargePriority = resolveChargePriority();
|
||||
boolean balanceCheckEnabled = tenantMeetingPointsSettingService.isBalanceCheckEnabled(tenantId);
|
||||
MeetingPointsAccount publicAccount = findAccount(tenantId, PUBLIC_ACCOUNT_USER_ID);
|
||||
long publicBalance = publicAccount == null ? 0L : defaultLong(publicAccount.getCurrentBalance());
|
||||
long publicTotalUsed = publicAccount == null ? 0L : defaultLong(publicAccount.getTotalPointsUsed());
|
||||
|
|
@ -111,6 +116,7 @@ public class MeetingPointsQueryServiceImpl implements MeetingPointsQueryService
|
|||
MeetingPointsOverviewVO vo = new MeetingPointsOverviewVO();
|
||||
vo.setAccountMode(accountMode);
|
||||
vo.setChargePriority(chargePriority);
|
||||
vo.setBalanceCheckEnabled(balanceCheckEnabled);
|
||||
vo.setPublicBalance(publicBalance);
|
||||
vo.setPublicTotalPointsUsed(publicTotalUsed);
|
||||
vo.setPersonalBalance(personalBalance);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.imeeting.service.biz.MeetingPointsAccountService;
|
|||
import com.imeeting.service.biz.MeetingPointsLedgerService;
|
||||
import com.imeeting.service.biz.MeetingPointsService;
|
||||
import com.imeeting.service.biz.MeetingSummaryChargeRecordService;
|
||||
import com.imeeting.service.biz.TenantMeetingPointsSettingService;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -28,7 +29,9 @@ import java.math.BigDecimal;
|
|||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
|
|
@ -42,6 +45,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
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_BLOCKED = "BLOCKED";
|
||||
private static final String STATUS_ASR_CHARGED = "ASR_CHARGED";
|
||||
private static final String STATUS_COMPLETED = "COMPLETED";
|
||||
private static final String STATUS_FAILED = "FAILED";
|
||||
|
|
@ -51,6 +55,8 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
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 static final String BLOCKED_REASON_INSUFFICIENT_POINTS = "INSUFFICIENT_POINTS";
|
||||
private static final String TASK_CONFIG_BALANCE_CHECK_ENABLED_SNAPSHOT = "balanceCheckEnabledSnapshot";
|
||||
|
||||
private final MeetingSummaryChargeRecordService chargeRecordService;
|
||||
private final MeetingPointsAccountService pointsAccountService;
|
||||
|
|
@ -60,6 +66,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
private final MeetingPointsAccountMapper meetingPointsAccountMapper;
|
||||
private final MeetingSummaryChargeRecordMapper meetingSummaryChargeRecordMapper;
|
||||
private final SysParamService sysParamService;
|
||||
private final TenantMeetingPointsSettingService tenantMeetingPointsSettingService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
|
|
@ -67,7 +74,10 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
if (tenantId == null) {
|
||||
return;
|
||||
}
|
||||
long initialBalance = nonNegativeLong(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_INITIAL_BALANCE, "0"), 0L);
|
||||
long initialBalance = nonNegativeLong(
|
||||
sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_INITIAL_BALANCE, "0"),
|
||||
0L
|
||||
);
|
||||
getOrCreateAccountForMutation(tenantId, UNIFIED_ACCOUNT_USER_ID, initialBalance, true);
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +87,9 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
if (tenantId == null) {
|
||||
throw new RuntimeException("缺少租户信息");
|
||||
}
|
||||
if (!isBalanceCheckEnabled(tenantId)) {
|
||||
throw new RuntimeException("无限余额模式下不允许分配积分");
|
||||
}
|
||||
if (targetUserId == null || targetUserId <= 0L) {
|
||||
throw new RuntimeException("目标用户不能为空");
|
||||
}
|
||||
|
|
@ -128,7 +141,14 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
}
|
||||
|
||||
ensureMeetingDurationStats(meeting, durationSeconds);
|
||||
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(meeting, summaryTask, chargeTriggerType, durationSeconds);
|
||||
boolean balanceCheckEnabledSnapshot = resolveTaskBalanceCheckSnapshot(asrTask, meeting.getTenantId());
|
||||
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(
|
||||
meeting,
|
||||
summaryTask,
|
||||
chargeTriggerType,
|
||||
durationSeconds,
|
||||
balanceCheckEnabledSnapshot
|
||||
);
|
||||
if (defaultLong(record.getChargedAsrPoints()) > 0L) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -165,7 +185,14 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
String chargeTriggerType = resolveChargeTriggerType(summaryTask);
|
||||
Integer durationSeconds = resolveEffectiveAudioDurationSeconds(meeting);
|
||||
int safeDurationSeconds = durationSeconds == null || durationSeconds <= 0 ? 0 : durationSeconds;
|
||||
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(meeting, summaryTask, chargeTriggerType, safeDurationSeconds);
|
||||
boolean balanceCheckEnabledSnapshot = resolveTaskBalanceCheckSnapshot(summaryTask, meeting.getTenantId());
|
||||
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(
|
||||
meeting,
|
||||
summaryTask,
|
||||
chargeTriggerType,
|
||||
safeDurationSeconds,
|
||||
balanceCheckEnabledSnapshot
|
||||
);
|
||||
if (durationSeconds == null || durationSeconds <= 0) {
|
||||
record.setFailureReason("无法解析有效录音时长,未记录积分扣减");
|
||||
record.setSummaryStatus(STATUS_COMPLETED);
|
||||
|
|
@ -253,6 +280,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
vo.setUserId(userId);
|
||||
vo.setAccountMode(accountMode);
|
||||
vo.setChargePriority(chargePriority);
|
||||
vo.setBalanceCheckEnabled(isBalanceCheckEnabled(tenantId));
|
||||
vo.setPublicBalance(publicBalance);
|
||||
vo.setPublicTotalPointsUsed(publicTotalUsed);
|
||||
vo.setPersonalBalance(personalBalance);
|
||||
|
|
@ -264,20 +292,34 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void assertSufficientPointsBeforeAsrSubmit(Meeting meeting, AiTask asrTask) {
|
||||
if (!shouldEnforceBalance() || meeting == null || asrTask == null || meeting.getId() == null) {
|
||||
if (meeting == null || asrTask == null || meeting.getId() == null) {
|
||||
return;
|
||||
}
|
||||
boolean balanceCheckEnabledSnapshot = resolveOrPersistTaskBalanceCheckSnapshot(asrTask, meeting.getTenantId());
|
||||
if (!shouldEnforceBalance(balanceCheckEnabledSnapshot)) {
|
||||
return;
|
||||
}
|
||||
Integer durationSeconds = resolveEffectiveAudioDurationSeconds(meeting);
|
||||
if (durationSeconds == null || durationSeconds <= 0) {
|
||||
throw new RuntimeException("无法解析录音时长,不能校验积分余额");
|
||||
}
|
||||
ensureSufficientPoints(meeting, null, buildChargeSnapshot(durationSeconds).asrPoints(), "ASR_SUBMIT");
|
||||
ensureSufficientPoints(
|
||||
meeting,
|
||||
null,
|
||||
buildChargeSnapshot(durationSeconds).asrPoints(),
|
||||
"ASR_SUBMIT",
|
||||
balanceCheckEnabledSnapshot
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void assertSufficientPointsBeforeSummarySubmit(Meeting meeting, AiTask summaryTask) {
|
||||
if (!shouldEnforceBalance() || meeting == null || summaryTask == null || meeting.getId() == null) {
|
||||
if (meeting == null || summaryTask == null || meeting.getId() == null) {
|
||||
return;
|
||||
}
|
||||
boolean balanceCheckEnabledSnapshot = resolveOrPersistTaskBalanceCheckSnapshot(summaryTask, meeting.getTenantId());
|
||||
if (!shouldEnforceBalance(balanceCheckEnabledSnapshot)) {
|
||||
return;
|
||||
}
|
||||
String chargeTriggerType = resolveChargeTriggerType(summaryTask);
|
||||
|
|
@ -285,18 +327,33 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
if (durationSeconds == null || durationSeconds <= 0) {
|
||||
throw new RuntimeException("无法解析录音时长,不能校验积分余额");
|
||||
}
|
||||
ensureSufficientPoints(meeting, summaryTask, chargeTriggerType, durationSeconds, "SUMMARY_SUBMIT");
|
||||
ensureSufficientPoints(
|
||||
meeting,
|
||||
summaryTask,
|
||||
chargeTriggerType,
|
||||
durationSeconds,
|
||||
"SUMMARY_SUBMIT",
|
||||
balanceCheckEnabledSnapshot
|
||||
);
|
||||
}
|
||||
|
||||
private MeetingSummaryChargeRecord getOrCreateChargeRecord(Meeting meeting, AiTask summaryTask, String chargeTriggerType, int durationSeconds) {
|
||||
private MeetingSummaryChargeRecord getOrCreateChargeRecord(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
String chargeTriggerType,
|
||||
int durationSeconds,
|
||||
boolean balanceCheckEnabledSnapshot) {
|
||||
MeetingSummaryChargeRecord record = meetingSummaryChargeRecordMapper.selectForUpdateBySummaryTaskId(summaryTask.getId());
|
||||
if (record != null) {
|
||||
if (record.getBalanceCheckEnabledSnapshot() == null) {
|
||||
record.setBalanceCheckEnabledSnapshot(toSnapshotFlag(balanceCheckEnabledSnapshot));
|
||||
}
|
||||
if ((record.getAudioDurationSeconds() == null || record.getAudioDurationSeconds() <= 0) && durationSeconds > 0) {
|
||||
applyChargeSnapshot(record, meeting, chargeTriggerType, durationSeconds);
|
||||
saveOrUpdateRecord(record);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
record = new MeetingSummaryChargeRecord();
|
||||
record.setTenantId(meeting.getTenantId());
|
||||
record.setStatus(1);
|
||||
|
|
@ -305,6 +362,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
record.setUserId(meeting.getCreatorId());
|
||||
record.setChargeTriggerType(chargeTriggerType);
|
||||
record.setPointsModeEnabled(isPointsEnabled() ? 1 : 0);
|
||||
record.setBalanceCheckEnabledSnapshot(toSnapshotFlag(balanceCheckEnabledSnapshot));
|
||||
record.setChargedTotalPoints(0L);
|
||||
record.setChargedAsrPoints(0L);
|
||||
record.setChargedLlmPoints(0L);
|
||||
|
|
@ -331,8 +389,19 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
AiTask summaryTask,
|
||||
String chargeTriggerType,
|
||||
int durationSeconds,
|
||||
String submitStage) {
|
||||
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(meeting, summaryTask, chargeTriggerType, durationSeconds);
|
||||
String submitStage,
|
||||
boolean balanceCheckEnabledSnapshot) {
|
||||
MeetingSummaryChargeRecord record = getOrCreateChargeRecord(
|
||||
meeting,
|
||||
summaryTask,
|
||||
chargeTriggerType,
|
||||
durationSeconds,
|
||||
balanceCheckEnabledSnapshot
|
||||
);
|
||||
if (!shouldEnforceBalance(isBalanceCheckEnabled(record))) {
|
||||
clearBlockedReason(record);
|
||||
return;
|
||||
}
|
||||
long requiredPoints = defaultLong(record.getTotalPoints()) - defaultLong(record.getChargedTotalPoints());
|
||||
if (requiredPoints <= 0L) {
|
||||
clearBlockedReason(record);
|
||||
|
|
@ -340,12 +409,12 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
}
|
||||
long availableBalance = resolveAvailableBalanceForCheck(meeting.getTenantId(), record.getUserId());
|
||||
if (availableBalance < requiredPoints) {
|
||||
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||
record.setBlockedReason(BLOCKED_REASON_INSUFFICIENT_POINTS);
|
||||
record.setFailureReason("INSUFFICIENT_POINTS at " + submitStage + ", required="
|
||||
+ requiredPoints + ", available=" + availableBalance);
|
||||
record.setBalanceBefore(availableBalance);
|
||||
record.setBalanceAfter(availableBalance);
|
||||
record.setSummaryStatus("BLOCKED");
|
||||
record.setSummaryStatus(STATUS_BLOCKED);
|
||||
saveOrUpdateRecord(record);
|
||||
throw new RuntimeException("积分余额不足");
|
||||
}
|
||||
|
|
@ -355,7 +424,12 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
private void ensureSufficientPoints(Meeting meeting,
|
||||
MeetingSummaryChargeRecord record,
|
||||
long requiredPoints,
|
||||
String submitStage) {
|
||||
String submitStage,
|
||||
boolean balanceCheckEnabledSnapshot) {
|
||||
if (!shouldEnforceBalance(balanceCheckEnabledSnapshot)) {
|
||||
clearBlockedReason(record);
|
||||
return;
|
||||
}
|
||||
if (requiredPoints <= 0L) {
|
||||
clearBlockedReason(record);
|
||||
return;
|
||||
|
|
@ -364,12 +438,12 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
long availableBalance = resolveAvailableBalanceForCheck(meeting.getTenantId(), ownerUserId);
|
||||
if (availableBalance < requiredPoints) {
|
||||
if (record != null) {
|
||||
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||
record.setBlockedReason(BLOCKED_REASON_INSUFFICIENT_POINTS);
|
||||
record.setFailureReason("INSUFFICIENT_POINTS at " + submitStage + ", required="
|
||||
+ requiredPoints + ", available=" + availableBalance);
|
||||
record.setBalanceBefore(availableBalance);
|
||||
record.setBalanceAfter(availableBalance);
|
||||
record.setSummaryStatus("BLOCKED");
|
||||
record.setSummaryStatus(STATUS_BLOCKED);
|
||||
saveOrUpdateRecord(record);
|
||||
}
|
||||
throw new RuntimeException("积分余额不足");
|
||||
|
|
@ -381,7 +455,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
if (record == null || record.getId() == null) {
|
||||
return;
|
||||
}
|
||||
if (record.getBlockedReason() != null || "BLOCKED".equals(record.getSummaryStatus())) {
|
||||
if (record.getBlockedReason() != null || STATUS_BLOCKED.equals(record.getSummaryStatus())) {
|
||||
record.setBlockedReason(null);
|
||||
record.setFailureReason(null);
|
||||
record.setSummaryStatus(isPointsEnabled() ? STATUS_PENDING : STATUS_DISABLED);
|
||||
|
|
@ -406,45 +480,59 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
record.setTotalPoints(snapshot.llmPoints());
|
||||
record.setAsrPoints(0L);
|
||||
record.setLlmPoints(snapshot.llmPoints());
|
||||
} else {
|
||||
record.setTotalPoints(snapshot.totalPoints());
|
||||
record.setAsrPoints(snapshot.asrPoints());
|
||||
record.setLlmPoints(snapshot.llmPoints());
|
||||
return;
|
||||
}
|
||||
record.setTotalPoints(snapshot.totalPoints());
|
||||
record.setAsrPoints(snapshot.asrPoints());
|
||||
record.setLlmPoints(snapshot.llmPoints());
|
||||
}
|
||||
|
||||
private ChargeExecutionResult executeCharge(Meeting meeting, AiTask summaryTask, MeetingSummaryChargeRecord record,
|
||||
String pointsType, long chargeAmount) {
|
||||
private ChargeExecutionResult executeCharge(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
MeetingSummaryChargeRecord record,
|
||||
String pointsType,
|
||||
long chargeAmount) {
|
||||
List<ChargeTarget> chargeTargets = resolveChargeTargets(meeting.getTenantId(), record.getUserId());
|
||||
if (chargeTargets.isEmpty()) {
|
||||
record.setBlockedReason(BLOCKED_REASON_INSUFFICIENT_POINTS);
|
||||
saveOrUpdateRecord(record);
|
||||
throw new RuntimeException("积分账户不存在或不可用");
|
||||
}
|
||||
|
||||
long totalBalanceBefore = 0L;
|
||||
for (ChargeTarget target : chargeTargets) {
|
||||
totalBalanceBefore += defaultLong(target.account().getCurrentBalance());
|
||||
}
|
||||
if (shouldEnforceBalance() && totalBalanceBefore < chargeAmount) {
|
||||
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||
|
||||
boolean balanceCheckEnabledSnapshot = isBalanceCheckEnabled(record);
|
||||
if (shouldEnforceBalance(balanceCheckEnabledSnapshot) && totalBalanceBefore < chargeAmount) {
|
||||
record.setBlockedReason(BLOCKED_REASON_INSUFFICIENT_POINTS);
|
||||
saveOrUpdateRecord(record);
|
||||
throw new RuntimeException("积分余额不足");
|
||||
}
|
||||
|
||||
long remaining = chargeAmount;
|
||||
for (int i = 0; i < chargeTargets.size(); i++) {
|
||||
ChargeTarget target = chargeTargets.get(i);
|
||||
if (!balanceCheckEnabledSnapshot) {
|
||||
ChargeTarget target = chargeTargets.get(0);
|
||||
MeetingPointsAccount account = target.account();
|
||||
long currentBalance = defaultLong(account.getCurrentBalance());
|
||||
boolean lastTarget = i == chargeTargets.size() - 1;
|
||||
long deducted = calculateDeductedPoints(currentBalance, remaining, lastTarget);
|
||||
increaseConsumedPoints(account, pointsType, chargeAmount);
|
||||
pointsAccountService.updateById(account);
|
||||
saveChargeLedger(meeting, summaryTask, record, target.accountUserId(), pointsType, -chargeAmount, currentBalance, currentBalance);
|
||||
return new ChargeExecutionResult(chargeAmount, totalBalanceBefore, totalBalanceBefore);
|
||||
}
|
||||
|
||||
long remaining = chargeAmount;
|
||||
for (ChargeTarget target : chargeTargets) {
|
||||
MeetingPointsAccount account = target.account();
|
||||
long currentBalance = defaultLong(account.getCurrentBalance());
|
||||
long deducted = Math.min(Math.max(currentBalance, 0L), remaining);
|
||||
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);
|
||||
}
|
||||
increaseConsumedPoints(account, pointsType, deducted);
|
||||
pointsAccountService.updateById(account);
|
||||
|
||||
saveChargeLedger(meeting, summaryTask, record, target.accountUserId(), pointsType, -deducted, currentBalance, balanceAfter);
|
||||
|
|
@ -455,28 +543,21 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
}
|
||||
|
||||
if (remaining > 0L) {
|
||||
record.setBlockedReason("INSUFFICIENT_POINTS");
|
||||
record.setBlockedReason(BLOCKED_REASON_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 (shouldEnforceBalance()) {
|
||||
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) {
|
||||
private void saveChargeLedger(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
MeetingSummaryChargeRecord record,
|
||||
Long accountUserId,
|
||||
String pointsType,
|
||||
long pointsDelta,
|
||||
long balanceBefore,
|
||||
long balanceAfter) {
|
||||
if (pointsDelta == 0L) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -491,14 +572,18 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
ledger.setPointsType(pointsType);
|
||||
ledger.setBalanceBefore(balanceBefore);
|
||||
ledger.setBalanceAfter(balanceAfter);
|
||||
ledger.setRemark(TRIGGER_RESUMMARY.equals(record.getChargeTriggerType())
|
||||
? "重新总结成功后扣减"
|
||||
: "总结任务成功后扣减");
|
||||
ledger.setBalanceCheckEnabledSnapshot(record.getBalanceCheckEnabledSnapshot());
|
||||
ledger.setRemark(buildChargeLedgerRemark(record));
|
||||
pointsLedgerService.save(ledger);
|
||||
}
|
||||
|
||||
private void saveTransferLedger(Long tenantId, Long accountUserId, String pointsType, long pointsDelta,
|
||||
long balanceBefore, long balanceAfter, String remark) {
|
||||
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);
|
||||
|
|
@ -507,6 +592,7 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
ledger.setPointsType(pointsType);
|
||||
ledger.setBalanceBefore(balanceBefore);
|
||||
ledger.setBalanceAfter(balanceAfter);
|
||||
ledger.setBalanceCheckEnabledSnapshot(toSnapshotFlag(isBalanceCheckEnabled(tenantId)));
|
||||
ledger.setRemark(remark);
|
||||
pointsLedgerService.save(ledger);
|
||||
}
|
||||
|
|
@ -573,6 +659,15 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
return account;
|
||||
}
|
||||
|
||||
private void increaseConsumedPoints(MeetingPointsAccount account, String pointsType, long points) {
|
||||
account.setTotalPointsUsed(defaultLong(account.getTotalPointsUsed()) + points);
|
||||
if (POINTS_TYPE_ASR.equals(pointsType)) {
|
||||
account.setTotalAsrPointsUsed(defaultLong(account.getTotalAsrPointsUsed()) + points);
|
||||
} else if (POINTS_TYPE_LLM.equals(pointsType)) {
|
||||
account.setTotalLlmPointsUsed(defaultLong(account.getTotalLlmPointsUsed()) + points);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ChargeTarget> resolveChargeTargets(Long tenantId, Long ownerUserId) {
|
||||
Long personalUserId = ownerUserId == null ? UNIFIED_ACCOUNT_USER_ID : ownerUserId;
|
||||
String accountMode = resolveAccountMode();
|
||||
|
|
@ -672,12 +767,68 @@ public class MeetingPointsServiceImpl implements MeetingPointsService {
|
|||
return Boolean.parseBoolean(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ENABLED, "false"));
|
||||
}
|
||||
|
||||
private boolean isBalanceEnforced() {
|
||||
return Boolean.parseBoolean(sysParamService.getCachedParamValue(SysParamKeys.MEETING_POINTS_ENFORCE_BALANCE, "true"));
|
||||
private boolean isBalanceCheckEnabled(Long tenantId) {
|
||||
return tenantMeetingPointsSettingService.isBalanceCheckEnabled(tenantId);
|
||||
}
|
||||
|
||||
private boolean shouldEnforceBalance() {
|
||||
return isPointsEnabled() && isBalanceEnforced();
|
||||
private boolean isBalanceCheckEnabled(MeetingSummaryChargeRecord record) {
|
||||
return record == null || !Integer.valueOf(0).equals(record.getBalanceCheckEnabledSnapshot());
|
||||
}
|
||||
|
||||
private boolean shouldEnforceBalance(boolean balanceCheckEnabledSnapshot) {
|
||||
return isPointsEnabled() && balanceCheckEnabledSnapshot;
|
||||
}
|
||||
|
||||
private boolean resolveOrPersistTaskBalanceCheckSnapshot(AiTask task, Long tenantId) {
|
||||
boolean snapshot = resolveTaskBalanceCheckSnapshot(task, tenantId);
|
||||
Map<String, Object> taskConfig = task.getTaskConfig() == null ? new HashMap<>() : new HashMap<>(task.getTaskConfig());
|
||||
if (!taskConfig.containsKey(TASK_CONFIG_BALANCE_CHECK_ENABLED_SNAPSHOT)) {
|
||||
taskConfig.put(TASK_CONFIG_BALANCE_CHECK_ENABLED_SNAPSHOT, snapshot);
|
||||
task.setTaskConfig(taskConfig);
|
||||
if (task.getId() != null) {
|
||||
aiTaskMapper.updateById(task);
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private boolean resolveTaskBalanceCheckSnapshot(AiTask task, Long tenantId) {
|
||||
if (task != null && task.getTaskConfig() != null && task.getTaskConfig().containsKey(TASK_CONFIG_BALANCE_CHECK_ENABLED_SNAPSHOT)) {
|
||||
return parseBooleanFlag(task.getTaskConfig().get(TASK_CONFIG_BALANCE_CHECK_ENABLED_SNAPSHOT), true);
|
||||
}
|
||||
return isBalanceCheckEnabled(tenantId);
|
||||
}
|
||||
|
||||
private boolean parseBooleanFlag(Object rawValue, boolean defaultValue) {
|
||||
if (rawValue == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (rawValue instanceof Boolean booleanValue) {
|
||||
return booleanValue;
|
||||
}
|
||||
String normalized = String.valueOf(rawValue).trim().toLowerCase();
|
||||
if ("1".equals(normalized) || "true".equals(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if ("0".equals(normalized) || "false".equals(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private int toSnapshotFlag(boolean balanceCheckEnabledSnapshot) {
|
||||
return balanceCheckEnabledSnapshot ? 1 : 0;
|
||||
}
|
||||
|
||||
private String buildChargeLedgerRemark(MeetingSummaryChargeRecord record) {
|
||||
if (!isBalanceCheckEnabled(record)) {
|
||||
return TRIGGER_RESUMMARY.equals(record.getChargeTriggerType())
|
||||
? "重新总结成功后记录消耗,未扣减余额"
|
||||
: "总结任务成功后记录消耗,未扣减余额";
|
||||
}
|
||||
return TRIGGER_RESUMMARY.equals(record.getChargeTriggerType())
|
||||
? "重新总结成功后扣减积分"
|
||||
: "总结任务成功后扣减积分";
|
||||
}
|
||||
|
||||
private String resolveAccountMode() {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package com.imeeting.service.biz.impl;
|
|||
|
||||
import com.imeeting.service.biz.LicenseService;
|
||||
import com.imeeting.service.biz.MeetingPointsService;
|
||||
import com.imeeting.service.biz.TenantMeetingPointsSettingService;
|
||||
import com.unisbase.config.properties.UnisBaseProperties;
|
||||
import com.unisbase.dto.CreateTenantDTO;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.dto.SysTenantDTO;
|
||||
import com.unisbase.service.SysTenantService;
|
||||
import com.unisbase.service.TenantManagementService;
|
||||
import com.unisbase.service.TenantModeService;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
|
@ -17,26 +18,30 @@ import java.util.List;
|
|||
@Service
|
||||
@Primary
|
||||
public class TenantManagementServicePrimaryImpl implements TenantManagementService {
|
||||
private static final long DEFAULT_TENANT_ID = 1L;
|
||||
|
||||
private final SysTenantService sysTenantService;
|
||||
private final TenantModeService tenantModeService;
|
||||
private final UnisBaseProperties unisBaseProperties;
|
||||
private final LicenseService licenseService;
|
||||
private final MeetingPointsService meetingPointsService;
|
||||
private final TenantMeetingPointsSettingService tenantMeetingPointsSettingService;
|
||||
|
||||
public TenantManagementServicePrimaryImpl(SysTenantService sysTenantService,
|
||||
TenantModeService tenantModeService,
|
||||
UnisBaseProperties unisBaseProperties,
|
||||
LicenseService licenseService,
|
||||
MeetingPointsService meetingPointsService) {
|
||||
MeetingPointsService meetingPointsService,
|
||||
TenantMeetingPointsSettingService tenantMeetingPointsSettingService) {
|
||||
this.sysTenantService = sysTenantService;
|
||||
this.tenantModeService = tenantModeService;
|
||||
this.unisBaseProperties = unisBaseProperties;
|
||||
this.licenseService = licenseService;
|
||||
this.meetingPointsService = meetingPointsService;
|
||||
this.tenantMeetingPointsSettingService = tenantMeetingPointsSettingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<List<SysTenantDTO>> listTenants(Integer current, Integer size, String name, String code) {
|
||||
if (tenantModeService.isSingleTenantMode()) {
|
||||
SysTenantDTO defaultTenant = sysTenantService.findById(tenantModeService.getDefaultTenantId());
|
||||
if (isSingleTenantMode()) {
|
||||
SysTenantDTO defaultTenant = sysTenantService.findById(getDefaultTenantId());
|
||||
PageResult<List<SysTenantDTO>> result = new PageResult<>();
|
||||
result.setRecords(defaultTenant == null ? List.of() : List.of(defaultTenant));
|
||||
result.setTotal(defaultTenant == null ? 0 : 1);
|
||||
|
|
@ -53,16 +58,17 @@ public class TenantManagementServicePrimaryImpl implements TenantManagementServi
|
|||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createTenant(CreateTenantDTO tenant) {
|
||||
tenantModeService.assertTenantLifecycleAllowed();
|
||||
assertTenantLifecycleAllowed();
|
||||
Long tenantId = sysTenantService.createTenantWithAdmin(tenant);
|
||||
licenseService.initializeTemporaryLicenses(tenantId);
|
||||
meetingPointsService.initializeTenantPointsAccount(tenantId);
|
||||
tenantMeetingPointsSettingService.initializeTenantSetting(tenantId);
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateTenant(Long tenantId, SysTenantDTO tenant) {
|
||||
tenantModeService.assertDefaultTenantCanBeUpdated(tenantId, tenant == null ? null : tenant.getStatus());
|
||||
assertDefaultTenantCanBeUpdated(tenantId, tenant == null ? null : tenant.getStatus());
|
||||
if (tenant == null) {
|
||||
throw new IllegalArgumentException("租户信息不能为空");
|
||||
}
|
||||
|
|
@ -72,7 +78,45 @@ public class TenantManagementServicePrimaryImpl implements TenantManagementServi
|
|||
|
||||
@Override
|
||||
public boolean deleteTenant(Long tenantId) {
|
||||
tenantModeService.assertTenantLifecycleAllowed();
|
||||
assertTenantLifecycleAllowed();
|
||||
return sysTenantService.deleteById(tenantId);
|
||||
}
|
||||
|
||||
private boolean isSingleTenantMode() {
|
||||
if (unisBaseProperties == null || unisBaseProperties.getTenant() == null) {
|
||||
return false;
|
||||
}
|
||||
String mode = unisBaseProperties.getTenant().getMode();
|
||||
if (mode != null && !mode.isBlank()) {
|
||||
return "single".equalsIgnoreCase(mode.trim());
|
||||
}
|
||||
return !unisBaseProperties.getTenant().isEnabled();
|
||||
}
|
||||
|
||||
private Long getDefaultTenantId() {
|
||||
if (unisBaseProperties == null || unisBaseProperties.getTenant() == null) {
|
||||
return DEFAULT_TENANT_ID;
|
||||
}
|
||||
Long configured = unisBaseProperties.getTenant().getDefaultTenantId();
|
||||
return configured == null || configured <= 0 ? DEFAULT_TENANT_ID : configured;
|
||||
}
|
||||
|
||||
private void assertTenantLifecycleAllowed() {
|
||||
if (isSingleTenantMode()) {
|
||||
throw new IllegalArgumentException("当前为单租户模式,不支持租户生命周期操作");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertDefaultTenantCanBeUpdated(Long tenantId, Integer status) {
|
||||
if (!isSingleTenantMode()) {
|
||||
return;
|
||||
}
|
||||
Long defaultTenantId = getDefaultTenantId();
|
||||
if (!defaultTenantId.equals(tenantId)) {
|
||||
throw new IllegalArgumentException("当前为单租户模式,只允许维护默认租户");
|
||||
}
|
||||
if (status != null && status != 1) {
|
||||
throw new IllegalArgumentException("当前为单租户模式,不允许禁用默认租户");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.dto.biz.TenantMeetingPointsSettingVO;
|
||||
import com.imeeting.entity.biz.MeetingPointsAccount;
|
||||
import com.imeeting.entity.biz.TenantMeetingPointsSetting;
|
||||
import com.imeeting.service.biz.MeetingPointsAccountService;
|
||||
import com.imeeting.service.biz.TenantMeetingPointsManagementService;
|
||||
import com.imeeting.service.biz.TenantMeetingPointsSettingService;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.dto.SysTenantDTO;
|
||||
import com.unisbase.service.SysTenantService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class TenantMeetingPointsManagementServiceImpl implements TenantMeetingPointsManagementService {
|
||||
private static final long PUBLIC_ACCOUNT_USER_ID = 0L;
|
||||
|
||||
private final SysTenantService sysTenantService;
|
||||
private final TenantMeetingPointsSettingService tenantMeetingPointsSettingService;
|
||||
private final MeetingPointsAccountService meetingPointsAccountService;
|
||||
|
||||
public TenantMeetingPointsManagementServiceImpl(SysTenantService sysTenantService,
|
||||
TenantMeetingPointsSettingService tenantMeetingPointsSettingService,
|
||||
MeetingPointsAccountService meetingPointsAccountService) {
|
||||
this.sysTenantService = sysTenantService;
|
||||
this.tenantMeetingPointsSettingService = tenantMeetingPointsSettingService;
|
||||
this.meetingPointsAccountService = meetingPointsAccountService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<List<TenantMeetingPointsSettingVO>> pageSettings(Integer current,
|
||||
Integer size,
|
||||
String tenantName,
|
||||
String tenantCode,
|
||||
Boolean balanceCheckEnabled) {
|
||||
PageResult<List<SysTenantDTO>> tenantPage = sysTenantService.page(current, size, tenantName, tenantCode);
|
||||
List<SysTenantDTO> tenants = tenantPage == null || tenantPage.getRecords() == null
|
||||
? Collections.emptyList()
|
||||
: tenantPage.getRecords();
|
||||
List<Long> tenantIds = tenants.stream()
|
||||
.map(SysTenantDTO::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
Map<Long, TenantMeetingPointsSetting> settingMap = loadSettingMap(tenantIds);
|
||||
Map<Long, MeetingPointsAccount> publicAccountMap = loadPublicAccountMap(tenantIds);
|
||||
|
||||
List<TenantMeetingPointsSettingVO> records = tenants.stream()
|
||||
.map(tenant -> toSettingVO(tenant, settingMap.get(tenant.getId()), publicAccountMap.get(tenant.getId())))
|
||||
.filter(item -> balanceCheckEnabled == null || Objects.equals(item.getBalanceCheckEnabled(), balanceCheckEnabled))
|
||||
.toList();
|
||||
|
||||
PageResult<List<TenantMeetingPointsSettingVO>> result = new PageResult<>();
|
||||
result.setTotal(balanceCheckEnabled == null ? tenantPage.getTotal() : records.size());
|
||||
result.setRecords(records);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantMeetingPointsSettingVO getCurrentTenantSetting(Long tenantId) {
|
||||
if (tenantId == null) {
|
||||
throw new RuntimeException("租户不能为空");
|
||||
}
|
||||
SysTenantDTO tenant = sysTenantService.findById(tenantId);
|
||||
if (tenant == null) {
|
||||
throw new RuntimeException("租户不存在");
|
||||
}
|
||||
TenantMeetingPointsSetting setting = tenantMeetingPointsSettingService.getByTenantId(tenantId);
|
||||
MeetingPointsAccount publicAccount = findPublicAccount(tenantId);
|
||||
return toSettingVO(tenant, setting, publicAccount);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public TenantMeetingPointsSettingVO updateBalanceCheck(Long tenantId,
|
||||
boolean balanceCheckEnabled,
|
||||
String remark,
|
||||
Long operatorUserId,
|
||||
String operatorName) {
|
||||
if (tenantId == null) {
|
||||
throw new RuntimeException("租户不能为空");
|
||||
}
|
||||
SysTenantDTO tenant = sysTenantService.findById(tenantId);
|
||||
if (tenant == null) {
|
||||
throw new RuntimeException("租户不存在");
|
||||
}
|
||||
|
||||
TenantMeetingPointsSetting setting = tenantMeetingPointsSettingService.getByTenantIdForUpdate(tenantId);
|
||||
if (setting == null) {
|
||||
tenantMeetingPointsSettingService.initializeTenantSetting(tenantId);
|
||||
setting = tenantMeetingPointsSettingService.getByTenantIdForUpdate(tenantId);
|
||||
}
|
||||
if (setting == null) {
|
||||
throw new RuntimeException("租户积分配置初始化失败");
|
||||
}
|
||||
|
||||
setting.setStatus(1);
|
||||
setting.setBalanceCheckEnabled(balanceCheckEnabled ? 1 : 0);
|
||||
setting.setLastSwitchAt(LocalDateTime.now());
|
||||
setting.setLastSwitchBy(operatorUserId);
|
||||
setting.setLastSwitchByName(StringUtils.hasText(operatorName) ? operatorName.trim() : null);
|
||||
setting.setRemark(StringUtils.hasText(remark) ? truncate(remark.trim(), 500) : null);
|
||||
tenantMeetingPointsSettingService.updateById(setting);
|
||||
|
||||
MeetingPointsAccount publicAccount = findPublicAccount(tenantId);
|
||||
return toSettingVO(tenant, setting, publicAccount);
|
||||
}
|
||||
|
||||
private Map<Long, TenantMeetingPointsSetting> loadSettingMap(List<Long> tenantIds) {
|
||||
if (tenantIds == null || tenantIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return tenantMeetingPointsSettingService.list(new LambdaQueryWrapper<TenantMeetingPointsSetting>()
|
||||
.in(TenantMeetingPointsSetting::getTenantId, tenantIds))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(TenantMeetingPointsSetting::getTenantId, item -> item, (left, right) -> left, HashMap::new));
|
||||
}
|
||||
|
||||
private Map<Long, MeetingPointsAccount> loadPublicAccountMap(List<Long> tenantIds) {
|
||||
if (tenantIds == null || tenantIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return meetingPointsAccountService.list(new LambdaQueryWrapper<MeetingPointsAccount>()
|
||||
.in(MeetingPointsAccount::getTenantId, tenantIds)
|
||||
.eq(MeetingPointsAccount::getUserId, PUBLIC_ACCOUNT_USER_ID))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(MeetingPointsAccount::getTenantId, item -> item, (left, right) -> left, HashMap::new));
|
||||
}
|
||||
|
||||
private MeetingPointsAccount findPublicAccount(Long tenantId) {
|
||||
return meetingPointsAccountService.getOne(new LambdaQueryWrapper<MeetingPointsAccount>()
|
||||
.eq(MeetingPointsAccount::getTenantId, tenantId)
|
||||
.eq(MeetingPointsAccount::getUserId, PUBLIC_ACCOUNT_USER_ID)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private TenantMeetingPointsSettingVO toSettingVO(SysTenantDTO tenant,
|
||||
TenantMeetingPointsSetting setting,
|
||||
MeetingPointsAccount publicAccount) {
|
||||
TenantMeetingPointsSettingVO vo = new TenantMeetingPointsSettingVO();
|
||||
vo.setTenantId(tenant == null ? null : tenant.getId());
|
||||
vo.setTenantCode(tenant == null ? null : tenant.getTenantCode());
|
||||
vo.setTenantName(tenant == null ? null : tenant.getTenantName());
|
||||
boolean balanceCheckEnabled = setting == null || !Integer.valueOf(0).equals(setting.getBalanceCheckEnabled());
|
||||
vo.setBalanceCheckEnabled(balanceCheckEnabled);
|
||||
vo.setUnlimitedBalanceMode(!balanceCheckEnabled);
|
||||
vo.setPublicBalance(publicAccount == null ? 0L : defaultLong(publicAccount.getCurrentBalance()));
|
||||
vo.setPublicTotalPointsUsed(publicAccount == null ? 0L : defaultLong(publicAccount.getTotalPointsUsed()));
|
||||
vo.setLastSwitchAt(setting == null ? null : setting.getLastSwitchAt());
|
||||
vo.setLastSwitchByName(setting == null ? null : setting.getLastSwitchByName());
|
||||
vo.setRemark(setting == null ? null : setting.getRemark());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private String truncate(String value, int maxLength) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
||||
}
|
||||
|
||||
private long defaultLong(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.imeeting.entity.biz.TenantMeetingPointsSetting;
|
||||
import com.imeeting.mapper.biz.TenantMeetingPointsSettingMapper;
|
||||
import com.imeeting.service.biz.TenantMeetingPointsSettingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TenantMeetingPointsSettingServiceImpl
|
||||
extends ServiceImpl<TenantMeetingPointsSettingMapper, TenantMeetingPointsSetting>
|
||||
implements TenantMeetingPointsSettingService {
|
||||
|
||||
private final TenantMeetingPointsSettingMapper tenantMeetingPointsSettingMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void initializeTenantSetting(Long tenantId) {
|
||||
if (tenantId == null) {
|
||||
return;
|
||||
}
|
||||
if (tenantMeetingPointsSettingMapper.selectForUpdate(tenantId) != null) {
|
||||
return;
|
||||
}
|
||||
TenantMeetingPointsSetting entity = new TenantMeetingPointsSetting();
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setStatus(1);
|
||||
entity.setBalanceCheckEnabled(1);
|
||||
entity.setIsDeleted(0);
|
||||
try {
|
||||
save(entity);
|
||||
} catch (DuplicateKeyException ignored) {
|
||||
// 并发创建租户配置时直接复用已存在记录即可。
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBalanceCheckEnabled(Long tenantId) {
|
||||
TenantMeetingPointsSetting entity = getByTenantId(tenantId);
|
||||
return entity == null || !Integer.valueOf(0).equals(entity.getBalanceCheckEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantMeetingPointsSetting getByTenantId(Long tenantId) {
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
return getOne(new LambdaQueryWrapper<TenantMeetingPointsSetting>()
|
||||
.eq(TenantMeetingPointsSetting::getTenantId, tenantId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantMeetingPointsSetting getByTenantIdForUpdate(Long tenantId) {
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
return tenantMeetingPointsSettingMapper.selectForUpdate(tenantId);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import http from "../http";
|
|||
export interface MeetingPointsOverviewVO {
|
||||
accountMode: string;
|
||||
chargePriority: string;
|
||||
balanceCheckEnabled: boolean;
|
||||
publicBalance: number;
|
||||
publicTotalPointsUsed: number;
|
||||
personalBalance: number;
|
||||
|
|
@ -84,6 +85,19 @@ export interface MeetingPointsLedgerDetailVO {
|
|||
chargeItems?: MeetingPointsChargeItemVO[];
|
||||
}
|
||||
|
||||
export interface TenantMeetingPointsSettingVO {
|
||||
tenantId: number;
|
||||
tenantCode?: string;
|
||||
tenantName?: string;
|
||||
balanceCheckEnabled: boolean;
|
||||
unlimitedBalanceMode: boolean;
|
||||
publicBalance: number;
|
||||
publicTotalPointsUsed: number;
|
||||
lastSwitchAt?: string;
|
||||
lastSwitchByName?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export async function getMeetingPointsOverview() {
|
||||
const resp = await http.get("/api/biz/meeting-points/management/overview");
|
||||
return resp.data.data as MeetingPointsOverviewVO;
|
||||
|
|
@ -112,3 +126,27 @@ export async function transferMeetingPoints(payload: {
|
|||
const resp = await http.post("/api/biz/meeting-points/transfer", payload);
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
|
||||
export async function pageTenantMeetingPointsSettings(params: {
|
||||
current: number;
|
||||
size: number;
|
||||
tenantName?: string;
|
||||
tenantCode?: string;
|
||||
balanceCheckEnabled?: boolean;
|
||||
}) {
|
||||
const resp = await http.get("/api/biz/tenant-meeting-points/settings", { params });
|
||||
return resp.data.data as { records: TenantMeetingPointsSettingVO[]; total: number };
|
||||
}
|
||||
|
||||
export async function getCurrentTenantMeetingPointsSetting() {
|
||||
const resp = await http.get("/api/biz/tenant-meeting-points/settings/current");
|
||||
return resp.data.data as TenantMeetingPointsSettingVO;
|
||||
}
|
||||
|
||||
export async function updateTenantMeetingPointsBalanceCheck(tenantId: number, payload: {
|
||||
balanceCheckEnabled: boolean;
|
||||
remark?: string;
|
||||
}) {
|
||||
const resp = await http.put(`/api/biz/tenant-meeting-points/settings/${tenantId}/balance-check`, payload);
|
||||
return resp.data.data as TenantMeetingPointsSettingVO;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { EyeOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import { PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import { listUsers } from "@/api";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
|
|
@ -14,8 +13,6 @@ import {
|
|||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
|
|
@ -24,12 +21,9 @@ import PageContainer from "@/components/shared/PageContainer";
|
|||
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import {
|
||||
getMeetingPointsLedgerDetail,
|
||||
getMeetingPointsLedgerPage,
|
||||
getMeetingPointsOverview,
|
||||
transferMeetingPoints,
|
||||
type MeetingPointsChargeItemVO,
|
||||
type MeetingPointsLedgerDetailVO,
|
||||
type MeetingPointsLedgerListItemVO,
|
||||
type MeetingPointsOverviewVO,
|
||||
type MeetingPointsPersonalAccountVO,
|
||||
|
|
@ -99,30 +93,41 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
|
|||
const isPersonalOnly = overview.accountMode === ACCOUNT_MODE_PERSONAL;
|
||||
const showPublicSummary = !isPersonalOnly || isAdmin;
|
||||
const showPersonalSummary = !isPublicOnly;
|
||||
const isUnlimitedBalanceMode = overview.balanceCheckEnabled === false;
|
||||
|
||||
const cards: Array<{
|
||||
key: string;
|
||||
title: string;
|
||||
value: number;
|
||||
accent: string;
|
||||
value: number | string;
|
||||
note: string;
|
||||
}> = [];
|
||||
}> = [
|
||||
{
|
||||
key: "available-balance",
|
||||
title: "当前可用额度",
|
||||
value: isUnlimitedBalanceMode ? "无限" : (overview.totalAvailableBalance ?? 0),
|
||||
note: isUnlimitedBalanceMode ? "关闭余额校验后只记录消耗和流水" : "按当前账户模式计算的可用额度",
|
||||
},
|
||||
{
|
||||
key: "charge-count",
|
||||
title: "累计消耗次数",
|
||||
value: overview.totalChargeCount ?? 0,
|
||||
note: "已发生扣费的总结记录数",
|
||||
},
|
||||
];
|
||||
|
||||
if (showPublicSummary) {
|
||||
cards.push(
|
||||
cards.unshift(
|
||||
{
|
||||
key: "public-balance",
|
||||
title: "公共账户余额",
|
||||
value: overview.publicBalance ?? 0,
|
||||
accent: "rgba(24, 144, 255, 0.08)",
|
||||
note: "用于统一分配和公共扣费",
|
||||
note: "当前账面公共余额",
|
||||
},
|
||||
{
|
||||
key: "public-used",
|
||||
title: "公共账户累计消耗",
|
||||
value: overview.publicTotalPointsUsed ?? 0,
|
||||
accent: "rgba(82, 196, 26, 0.08)",
|
||||
note: "公共账户历史累计扣减",
|
||||
note: "公共账户历史累计消耗",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -133,41 +138,28 @@ function buildSummaryCards(overview: MeetingPointsOverviewVO | null) {
|
|||
key: "personal-balance",
|
||||
title: isAdmin ? "个人账户余额汇总" : "个人账户余额",
|
||||
value: overview.personalBalance ?? 0,
|
||||
accent: "rgba(114, 46, 209, 0.08)",
|
||||
note: isAdmin ? "管理员视角下的个人账户汇总" : "当前账号可用积分",
|
||||
note: isAdmin ? "管理员视角下的个人账户余额汇总" : "当前账号账面余额",
|
||||
},
|
||||
{
|
||||
key: "personal-used",
|
||||
title: isAdmin ? "个人账户累计消耗汇总" : "个人账户累计消耗",
|
||||
value: overview.personalTotalPointsUsed ?? 0,
|
||||
accent: "rgba(250, 140, 22, 0.08)",
|
||||
note: isAdmin ? "管理员视角下的个人账户消耗汇总" : "当前账号历史累计扣减",
|
||||
note: isAdmin ? "管理员视角下的个人账户累计消耗" : "当前账号历史累计消耗",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
cards.push({
|
||||
key: "charge-count",
|
||||
title: "累计消耗次数",
|
||||
value: overview.totalChargeCount ?? 0,
|
||||
accent: "rgba(38, 38, 38, 0.06)",
|
||||
note: "已发生扣费的总结记录数",
|
||||
});
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
export default function MeetingPointsManagement() {
|
||||
const [overview, setOverview] = useState<MeetingPointsOverviewVO | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [transferLoading, setTransferLoading] = useState(false);
|
||||
const [usersLoading, setUsersLoading] = useState(false);
|
||||
const [records, setRecords] = useState<MeetingPointsLedgerListItemVO[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [transferOpen, setTransferOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<MeetingPointsLedgerDetailVO | null>(null);
|
||||
const [users, setUsers] = useState<SysUser[]>([]);
|
||||
const [contentTab, setContentTab] = useState("ledger");
|
||||
const [personalAccountPagination, setPersonalAccountPagination] = useState({
|
||||
|
|
@ -185,7 +177,8 @@ export default function MeetingPointsManagement() {
|
|||
const isAdmin = Boolean(overview?.admin);
|
||||
const isPublicOnly = overview?.accountMode === ACCOUNT_MODE_PUBLIC;
|
||||
const isPersonalOnly = overview?.accountMode === ACCOUNT_MODE_PERSONAL;
|
||||
const showTransferButton = isAdmin && !isPublicOnly;
|
||||
const isUnlimitedBalanceMode = overview?.balanceCheckEnabled === false;
|
||||
const showTransferButton = isAdmin && !isPublicOnly && !isUnlimitedBalanceMode;
|
||||
const showPersonalAccountSection = Boolean(overview) && !isPublicOnly;
|
||||
const summaryCards = useMemo(() => buildSummaryCards(overview), [overview]);
|
||||
|
||||
|
|
@ -279,17 +272,6 @@ export default function MeetingPointsManagement() {
|
|||
message.success("已刷新积分数据");
|
||||
};
|
||||
|
||||
const handleOpenDetail = async (ledgerId: number) => {
|
||||
setDetailLoading(true);
|
||||
setDetailOpen(true);
|
||||
try {
|
||||
const data = await getMeetingPointsLedgerDetail(ledgerId);
|
||||
setDetail(data);
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenTransfer = async () => {
|
||||
setTransferOpen(true);
|
||||
if (!users.length) {
|
||||
|
|
@ -361,17 +343,6 @@ export default function MeetingPointsManagement() {
|
|||
width: 180,
|
||||
render: (value: string) => <Text>{formatDateTime(value)}</Text>,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 88,
|
||||
fixed: "right" as const,
|
||||
render: (_: unknown, record: MeetingPointsLedgerListItemVO) => (
|
||||
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => void handleOpenDetail(record.id)}>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const personalAccountColumns = [
|
||||
|
|
@ -415,43 +386,8 @@ export default function MeetingPointsManagement() {
|
|||
},
|
||||
];
|
||||
|
||||
const chargeItemColumns = [
|
||||
{
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
const ledgerTableContent = (
|
||||
<div style={{ display: "flex", flexDirection: "column" , height:"400px"}}>
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "400px" }}>
|
||||
<div className="app-page__table-wrap" style={{ overflow: "auto", padding: "0 24px" }}>
|
||||
<ListTable<MeetingPointsLedgerListItemVO>
|
||||
rowKey="id"
|
||||
|
|
@ -459,7 +395,7 @@ export default function MeetingPointsManagement() {
|
|||
dataSource={records}
|
||||
loading={loading}
|
||||
totalCount={total}
|
||||
scroll={{ x: 1200,y: 400 }}
|
||||
scroll={{ x: 1100, y: 400 }}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -479,14 +415,14 @@ export default function MeetingPointsManagement() {
|
|||
);
|
||||
|
||||
const personalAccountTableContent = (
|
||||
<div style={{ display: "flex", flexDirection: "column",height:'400px' }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "400px" }}>
|
||||
<div className="app-page__table-wrap" style={{ overflow: "auto", padding: "0 24px" }}>
|
||||
<ListTable<MeetingPointsPersonalAccountVO>
|
||||
rowKey="userId"
|
||||
columns={personalAccountColumns}
|
||||
dataSource={pagedPersonalAccounts}
|
||||
totalCount={personalAccountRows.length}
|
||||
scroll={{ x: 1200,y: 400 }}
|
||||
scroll={{ x: 900, y: 400 }}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -506,7 +442,7 @@ export default function MeetingPointsManagement() {
|
|||
return (
|
||||
<PageContainer
|
||||
title="积分管理"
|
||||
subtitle="根据当前扣费模式查看公共账户、个人账户和会议积分消耗轨迹"
|
||||
subtitle="查看当前租户下的积分账面余额、累计消耗和会议消耗记录"
|
||||
style={{ height: "auto" }}
|
||||
headerExtra={
|
||||
<Space>
|
||||
|
|
@ -564,12 +500,16 @@ export default function MeetingPointsManagement() {
|
|||
<Tag color="blue" bordered={false}>
|
||||
优先级:{getChargePriorityLabel(overview?.chargePriority)}
|
||||
</Tag>
|
||||
<Tag color={isUnlimitedBalanceMode ? "volcano" : "green"} bordered={false}>
|
||||
{isUnlimitedBalanceMode ? "无限余额模式" : "校验余额模式"}
|
||||
</Tag>
|
||||
<Tag color={isAdmin ? "gold" : "default"} bordered={false}>
|
||||
{isAdmin ? "管理员视角" : "当前用户视角"}
|
||||
</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
<Row gutter={[40, 16]}>
|
||||
{summaryCards.map((item) => (
|
||||
<Col key={item.key}>
|
||||
|
|
@ -586,26 +526,22 @@ export default function MeetingPointsManagement() {
|
|||
|
||||
{isPersonalOnly ? (
|
||||
<Card className="app-page__content-card" styles={{ body: { padding: 0 } }}>
|
||||
<Tabs
|
||||
activeKey={contentTab}
|
||||
onChange={setContentTab}
|
||||
items={[
|
||||
{
|
||||
key: "ledger",
|
||||
label: "积分明细",
|
||||
children: ledgerTableContent,
|
||||
},
|
||||
{
|
||||
key: "personal-account",
|
||||
label: "个人账户",
|
||||
children: personalAccountTableContent,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div style={{ padding: "20px 24px 8px" }}>
|
||||
<Text strong style={{ fontSize: 16 }}>
|
||||
积分流水
|
||||
</Text>
|
||||
</div>
|
||||
{ledgerTableContent}
|
||||
</Card>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<Card className="app-page__content-card" styles={{ body: { padding: 0 } }}>
|
||||
<div style={{ padding: "20px 24px 8px" }}>
|
||||
<Text strong style={{ fontSize: 16 }}>
|
||||
积分流水
|
||||
</Text>
|
||||
|
||||
</div>
|
||||
{ledgerTableContent}
|
||||
</Card>
|
||||
|
||||
|
|
@ -627,61 +563,6 @@ export default function MeetingPointsManagement() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
|
||||
<Modal
|
||||
title="积分流水详情"
|
||||
open={detailOpen}
|
||||
onCancel={() => {
|
||||
setDetailOpen(false);
|
||||
setDetail(null);
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
setDetailOpen(false);
|
||||
setDetail(null);
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
width={900}
|
||||
confirmLoading={detailLoading}
|
||||
>
|
||||
{detail && (
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Descriptions bordered column={2} size="small">
|
||||
<Descriptions.Item label="用户">{detail.ownerUserName || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="当前流水账户">{getAccountTypeLabel(detail.chargeAccountType)}</Descriptions.Item>
|
||||
<Descriptions.Item label="消耗类型">{getPointsTypeLabel(detail.pointsType)}</Descriptions.Item>
|
||||
<Descriptions.Item label="消耗积分">{detail.consumedPoints ?? 0}</Descriptions.Item>
|
||||
<Descriptions.Item label="消耗时间">{formatDateTime(detail.createdAt)}</Descriptions.Item>
|
||||
<Descriptions.Item label="会议标题">{detail.meetingTitle || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="触发类型">{getChargeTriggerLabel(detail.chargeTriggerType)}</Descriptions.Item>
|
||||
<Descriptions.Item label="录音时长(秒)">{detail.audioDurationSeconds ?? "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="计费分钟数">{detail.chargedMinutes ?? "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="应计总积分">{detail.totalPoints ?? 0}</Descriptions.Item>
|
||||
<Descriptions.Item label="已扣总积分">{detail.chargedTotalPoints ?? 0}</Descriptions.Item>
|
||||
<Descriptions.Item label="扣费前余额">{detail.balanceBefore ?? "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="扣费后余额">{detail.balanceAfter ?? "-"}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Card title="本次总结扣费明细" size="small">
|
||||
<Table<MeetingPointsChargeItemVO>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={chargeItemColumns}
|
||||
dataSource={detail.chargeItems || []}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="从公共账户分配积分"
|
||||
open={transferOpen}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const ClientManagement = lazy(() => import("@/pages/business/ClientManagement"))
|
|||
const ExternalAppManagement = lazy(() => import("@/pages/business/ExternalAppManagement"));
|
||||
const ScreenSaverManagement = lazy(() => import("@/pages/business/ScreenSaverManagement"));
|
||||
const MeetingPointsManagement = lazy(() => import("@/pages/business/MeetingPointsManagement"));
|
||||
const TenantMeetingPointsSettings = lazy(() => import("@/pages/business/TenantMeetingPointsSettings"));
|
||||
const LicenseManagement = lazy(() => import("@/pages/business/LicenseManagement"));
|
||||
|
||||
import SpeakerReg from "../pages/business/SpeakerReg";
|
||||
|
|
@ -70,6 +71,7 @@ export const menuRoutes: MenuRoute[] = [
|
|||
{ path: "/external-apps", label: "外部应用管理", element: <LazyPage><ExternalAppManagement /></LazyPage>, perm: "menu:external-apps" },
|
||||
{ path: "/screen-savers", label: "屏保管理", element: <LazyPage><ScreenSaverManagement /></LazyPage>, perm: "menu:screen-savers" },
|
||||
{ path: "/meeting-points", label: "积分管理", element: <LazyPage><MeetingPointsManagement /></LazyPage>, perm: "menu:meeting-points" },
|
||||
{ path: "/tenant-meeting-points", label: "租户积分校验", element: <LazyPage><TenantMeetingPointsSettings /></LazyPage>, perm: "menu:tenant-meeting-points" },
|
||||
{ path: "/meetings", label: "会议中心", element: <LazyPage><Meetings /></LazyPage>, perm: "menu:meeting" },
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue