This commit is contained in:
zjf committed 2026-11-09 17:46:34 +08:00
1 parent 694789e0f8
commit a6d3eb943f
31 files changed
+2573 -1

No files matched your search

@@ -0,0 +1,32 @@
package com.bsd.pjcloud.supermarket.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(description = "酒店分销层级规则")
public class SmHotelFxLevelRuleDTO {
@Schema(description = "层级 1~5")
private Integer level;
@Schema(description = "岗位名")
private String name;
@Schema(description = "占奖金池比例,如 0.40")
private BigDecimal shareOfPool;
@Schema(description = "是否可晋升")
private Boolean canPromote;
@Schema(description = "升到本级所需累计 promote 分,如四级升三级则为三级门槛 50000")
private BigDecimal promoteMin;
@Schema(description = "保级门槛(二级:窗口内区域内新酒店锁定基数,默认 40000)")
private BigDecimal demoteMin;
@Schema(description = "保级窗口天数(二级默认 90)")
private Integer demoteWindowDays;
}
@@ -0,0 +1,82 @@
package com.bsd.pjcloud.supermarket.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TenantTable
@TableName("canteen_hotel_fx_agent")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "酒店分销员")
public class SmHotelFxAgentEntity extends Model<SmHotelFxAgentEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
@Schema(description = "系统用户 ID")
private Long userId;
private String username;
private String realName;
private String mobile;
@Schema(description = "层级 1~5")
private Integer levelNo;
@Schema(description = "上级分销员")
private Long parentId;
private String inviteCode;
private String cityAdcode;
private String regionJson;
@Schema(description = "ACTIVE/DISABLED/EXIT")
private String status;
@Schema(description = "晋升累计分")
private BigDecimal promoteScore;
@Schema(description = "二级保级窗口累计")
private BigDecimal demoteWindowScore;
private LocalDateTime demoteWindowStart;
private String remark;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
@TableField(exist = false)
@Schema(description = "上级姓名")
private String parentName;
@TableField(exist = false)
@Schema(description = "绑定酒店数")
private Integer hotelCount;
}
@@ -0,0 +1,89 @@
package com.bsd.pjcloud.supermarket.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TenantTable
@TableName("canteen_hotel_fx_commission")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "酒店分销佣金明细")
public class SmHotelFxCommissionEntity extends Model<SmHotelFxCommissionEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long orderId;
private Long hotelId;
private Long agentId;
private Long ownerAgentId;
@Schema(description = "本条对应层级份额 1~5")
private Integer chainLevel;
@Schema(description = "结算时分销员层级快照")
private Integer agentLevel;
private BigDecimal costAmount;
private BigDecimal poolAmount;
private BigDecimal shareRate;
private BigDecimal commissionAmount;
private BigDecimal promoteBaseAmount;
@Schema(description = "ESTIMATED/LOCKED/CANCELLED")
private String status;
private LocalDate checkOutDate;
private LocalDateTime lockTime;
@Schema(description = "提现单 ID")
private Long withdrawId;
@Schema(description = "配置快照 JSON")
private String configSnapshot;
private String remark;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
@TableField(exist = false)
private String agentName;
@TableField(exist = false)
private String hotelName;
@TableField(exist = false)
private String channelNo;
}
@@ -0,0 +1,57 @@
package com.bsd.pjcloud.supermarket.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TenantTable
@TableName("canteen_hotel_fx_config")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "酒店分销佣金配置")
public class SmHotelFxConfigEntity extends Model<SmHotelFxConfigEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
@Schema(description = "1开 0关")
private String enabled;
@Schema(description = "奖金池成本系数,默认 0.06")
private BigDecimal poolCostRate;
@Schema(description = "奖金池销售分佣池比例,默认 0.30")
private BigDecimal poolSalesRate;
@Schema(description = "晋升累计成本系数,默认 0.06")
private BigDecimal promoteBaseRate;
@Schema(description = "层级规则 JSON")
private String levelRulesJson;
private String remark;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
}
@@ -0,0 +1,64 @@
package com.bsd.pjcloud.supermarket.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TenantTable
@TableName("canteen_hotel_fx_exit")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "酒店分销退出")
public class SmHotelFxExitEntity extends Model<SmHotelFxExitEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long agentId;
private Long userId;
private String reason;
@Schema(description = "PENDING/DONE/REJECTED/CANCELLED")
private String status;
private Long successorAgentId;
private Integer hotelCount;
private String auditRemark;
private String auditBy;
private LocalDateTime auditTime;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
@TableField(exist = false)
private String agentName;
@TableField(exist = false)
private String successorName;
}
@@ -0,0 +1,57 @@
package com.bsd.pjcloud.supermarket.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TenantTable
@TableName("canteen_hotel_fx_hotel_bind")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "酒店与分销员绑定")
public class SmHotelFxHotelBindEntity extends Model<SmHotelFxHotelBindEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long hotelId;
private Long agentId;
@Schema(description = "BOUND/UNBOUND")
private String bindStatus;
private String auditRemark;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
@TableField(exist = false)
private String hotelName;
@TableField(exist = false)
private String hotelCode;
@TableField(exist = false)
private String agentName;
}
@@ -0,0 +1,67 @@
package com.bsd.pjcloud.supermarket.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TenantTable
@TableName("canteen_hotel_fx_l1_apply")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "酒店分销一级申请")
public class SmHotelFxL1ApplyEntity extends Model<SmHotelFxL1ApplyEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long agentId;
private Long userId;
private String cityAdcode;
private String cityName;
private BigDecimal promoteScore;
@Schema(description = "PENDING/APPROVED/REJECTED/CANCELLED")
private String status;
private String applyRemark;
private String auditRemark;
private String auditBy;
private LocalDateTime auditTime;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
@TableField(exist = false)
private String agentName;
@TableField(exist = false)
private Integer agentLevel;
}
@@ -0,0 +1,70 @@
package com.bsd.pjcloud.supermarket.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TenantTable
@TableName("canteen_hotel_fx_withdraw")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "酒店分销提现")
public class SmHotelFxWithdrawEntity extends Model<SmHotelFxWithdrawEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long agentId;
private Long userId;
private BigDecimal amount;
private Integer commissionCount;
private String accountName;
private String accountNo;
private String bankName;
@Schema(description = "PENDING/APPROVED/PAID/REJECTED")
private String status;
private String applyRemark;
private String auditRemark;
private String auditBy;
private LocalDateTime auditTime;
private LocalDateTime payTime;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
@TableField(exist = false)
private String agentName;
}
@@ -0,0 +1,241 @@
package com.bsd.pjcloud.supermarket.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxCommissionEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxConfigEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxExitEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxHotelBindEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxL1ApplyEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxWithdrawEntity;
import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxCommissionService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxConfigService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxExitService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxL1ApplyService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxWithdrawService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 酒店分销:配置 / 分销员 / 佣金 / 一级申请 / 提现 / 退出。
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/smHotelFx")
@Tag(name = "酒店分销")
public class SmHotelFxController {
private final SmHotelFxConfigService configService;
private final SmHotelFxAgentService agentService;
private final SmHotelFxCommissionService commissionService;
private final SmHotelFxL1ApplyService l1ApplyService;
private final SmHotelFxWithdrawService withdrawService;
private final SmHotelFxExitService exitService;
@Operation(summary = "分销配置")
@GetMapping("/config")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFx_view')")
public R<SmHotelFxConfigEntity> getConfig() {
return R.ok(configService.getOrCreateCurrent());
}
@SysLog("保存酒店分销配置")
@Operation(summary = "保存分销配置(仅影响新单)")
@PutMapping("/config")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFx_edit')")
public R<SmHotelFxConfigEntity> saveConfig(@RequestBody SmHotelFxConfigEntity body) {
return configService.saveCurrent(body);
}
@Operation(summary = "分销员分页")
@GetMapping("/agent/page")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxAgent_view')")
public R<Page<SmHotelFxAgentEntity>> agentPage(Page<SmHotelFxAgentEntity> page, SmHotelFxAgentEntity query) {
return R.ok(agentService.pageAgents(page, query));
}
@SysLog("保存酒店分销员")
@Operation(summary = "新增/编辑分销员")
@PostMapping("/agent")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxAgent_edit')")
public R<SmHotelFxAgentEntity> saveAgent(@RequestBody SmHotelFxAgentEntity body) {
return agentService.saveAgent(body);
}
@SysLog("划调酒店分销区域")
@Operation(summary = "划调二级负责区域(adcode 数组 JSON)")
@PutMapping("/agent/region")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxAgent_edit')")
public R<SmHotelFxAgentEntity> saveRegion(@RequestParam Long agentId,
@RequestParam(required = false) String regionJson,
@RequestParam(required = false) String cityAdcode) {
return agentService.saveRegion(agentId, regionJson, cityAdcode);
}
@SysLog("手动执行二级保级考核")
@Operation(summary = "手动扫描二级到期降级/滚动窗口")
@PostMapping("/agent/demoteDue")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFx_edit')")
public R<Integer> demoteDue() {
return R.ok(agentService.processDueDemotions());
}
@SysLog("绑定酒店分销员")
@Operation(summary = "酒店绑定分销员")
@PostMapping("/bind")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxAgent_edit')")
public R<Boolean> bind(@RequestParam Long hotelId, @RequestParam Long agentId,
@RequestParam(required = false) String remark) {
return agentService.bindHotel(hotelId, agentId, remark);
}
@SysLog("解绑酒店分销员")
@Operation(summary = "解绑酒店")
@PostMapping("/unbind")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxAgent_edit')")
public R<Boolean> unbind(@RequestParam Long hotelId) {
return agentService.unbindHotel(hotelId);
}
@Operation(summary = "酒店绑定分页")
@GetMapping("/bind/page")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxAgent_view')")
public R<Page<SmHotelFxHotelBindEntity>> bindPage(Page<SmHotelFxHotelBindEntity> page,
@RequestParam(required = false) Long agentId, @RequestParam(required = false) Long hotelId) {
return R.ok(agentService.pageBinds(page, agentId, hotelId));
}
@Operation(summary = "佣金明细分页(管理端)")
@GetMapping("/commission/page")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxCommission_view')")
public R<Page<SmHotelFxCommissionEntity>> commissionPage(Page<SmHotelFxCommissionEntity> page,
SmHotelFxCommissionEntity query) {
return R.ok(commissionService.pageAdmin(page, query));
}
@SysLog("手动锁定到期分销佣金")
@Operation(summary = "手动执行退房T+1锁定")
@PostMapping("/commission/lockDue")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFx_edit')")
public R<Integer> lockDue() {
return R.ok(commissionService.lockDueCommissions());
}
@Operation(summary = "一级申请分页")
@GetMapping("/l1/page")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxL1_view')")
public R<Page<SmHotelFxL1ApplyEntity>> l1Page(Page<SmHotelFxL1ApplyEntity> page, SmHotelFxL1ApplyEntity query) {
return R.ok(l1ApplyService.pageAdmin(page, query));
}
@SysLog("审核一级申请")
@Operation(summary = "审核一级申请")
@PostMapping("/l1/audit")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxL1_audit')")
public R<Boolean> l1Audit(@RequestParam Long id, @RequestParam boolean approved,
@RequestParam(required = false) String auditRemark) {
return l1ApplyService.audit(id, approved, auditRemark);
}
@Operation(summary = "提现分页(管理端)")
@GetMapping("/withdraw/page")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxWithdraw_view')")
public R<Page<SmHotelFxWithdrawEntity>> withdrawPage(Page<SmHotelFxWithdrawEntity> page,
SmHotelFxWithdrawEntity query) {
return R.ok(withdrawService.pageAdmin(page, query));
}
@SysLog("审核分销提现")
@Operation(summary = "审核提现")
@PostMapping("/withdraw/audit")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxWithdraw_audit')")
public R<Boolean> withdrawAudit(@RequestParam Long id, @RequestParam boolean approved,
@RequestParam(required = false) String auditRemark) {
return withdrawService.audit(id, approved, auditRemark);
}
@SysLog("分销提现打款")
@Operation(summary = "标记提现已打款")
@PostMapping("/withdraw/paid")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxWithdraw_audit')")
public R<Boolean> withdrawPaid(@RequestParam Long id, @RequestParam(required = false) String remark) {
return withdrawService.markPaid(id, remark);
}
@Operation(summary = "退出申请分页")
@GetMapping("/exit/page")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxExit_view')")
public R<Page<SmHotelFxExitEntity>> exitPage(Page<SmHotelFxExitEntity> page, SmHotelFxExitEntity query) {
return R.ok(exitService.pageAdmin(page, query));
}
@SysLog("审核分销退出")
@Operation(summary = "审核退出(通过须传 successorAgentId)")
@PostMapping("/exit/audit")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelFxExit_audit')")
public R<Boolean> exitAudit(@RequestParam Long id, @RequestParam boolean approved,
@RequestParam(required = false) Long successorAgentId,
@RequestParam(required = false) String auditRemark) {
return exitService.audit(id, approved, successorAgentId, auditRemark);
}
@Operation(summary = "我的佣金汇总(App)")
@GetMapping("/app/mySummary")
public R<Map<String, Object>> mySummary() {
return commissionService.mySummary();
}
@Operation(summary = "我的佣金明细(App)")
@GetMapping("/app/myPage")
public R<Page<SmHotelFxCommissionEntity>> myPage(Page<SmHotelFxCommissionEntity> page,
@RequestParam(required = false) String status) {
return R.ok(commissionService.pageMine(page, status));
}
@Operation(summary = "申请一级(App)")
@PostMapping("/app/l1/apply")
public R<SmHotelFxL1ApplyEntity> appL1Apply(@RequestParam String cityAdcode,
@RequestParam(required = false) String cityName, @RequestParam(required = false) String remark) {
return l1ApplyService.applyMine(cityAdcode, cityName, remark);
}
@Operation(summary = "可提现汇总(App)")
@GetMapping("/app/withdraw/summary")
public R<Map<String, Object>> appWithdrawSummary() {
return withdrawService.withdrawableMine();
}
@Operation(summary = "申请提现(App)")
@PostMapping("/app/withdraw/apply")
public R<SmHotelFxWithdrawEntity> appWithdrawApply(@RequestParam String accountName,
@RequestParam String accountNo, @RequestParam(required = false) String bankName,
@RequestParam(required = false) String remark) {
return withdrawService.applyMine(accountName, accountNo, bankName, remark);
}
@Operation(summary = "我的提现单(App)")
@GetMapping("/app/withdraw/page")
public R<Page<SmHotelFxWithdrawEntity>> appWithdrawPage(Page<SmHotelFxWithdrawEntity> page,
@RequestParam(required = false) String status) {
return R.ok(withdrawService.pageMine(page, status));
}
@Operation(summary = "申请退出(App)")
@PostMapping("/app/exit/apply")
public R<SmHotelFxExitEntity> appExitApply(@RequestParam(required = false) String reason) {
return exitService.applyMine(reason);
}
}
@@ -0,0 +1,43 @@
package com.bsd.pjcloud.supermarket.job;
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
import com.bsd.pjcloud.supermarket.service.SmHotelFxCommissionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 退房 T+1 锁定预计佣金:默认每天 01:15。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SmHotelFxCommissionLockSchedule {
private final SmHotelFxCommissionService commissionService;
@Value("${sm-hotel.fx.lock-schedule-enabled:true}")
private boolean scheduleEnabled;
@Value("${sm-hotel.fx.tenant-id:1}")
private Long tenantId;
@Scheduled(cron = "${sm-hotel.fx.lock-cron:0 15 1 * * ?}")
public void lockDue() {
if (!scheduleEnabled) {
return;
}
Long tid = tenantId == null ? 1L : tenantId;
try {
int n = TenantBroker.applyAs(tid, t -> commissionService.lockDueCommissions());
if (n > 0) {
log.info("|酒店分销|T+1锁定|tenant={}|count={}|", tid, n);
}
}
catch (Exception e) {
log.error("|酒店分销|T+1锁定异常|", e);
}
}
}
@@ -0,0 +1,43 @@
package com.bsd.pjcloud.supermarket.job;
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 二级区域保级考核:窗口到期未达 demoteMin 则降三级;达标则滚动新窗口。默认每天 02:15。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SmHotelFxDemoteSchedule {
private final SmHotelFxAgentService agentService;
@Value("${sm-hotel.fx.demote-schedule-enabled:true}")
private boolean scheduleEnabled;
@Value("${sm-hotel.fx.tenant-id:1}")
private Long tenantId;
@Scheduled(cron = "${sm-hotel.fx.demote-cron:0 15 2 * * ?}")
public void processDemote() {
if (!scheduleEnabled) {
return;
}
Long tid = tenantId == null ? 1L : tenantId;
try {
int n = TenantBroker.applyAs(tid, t -> agentService.processDueDemotions());
if (n > 0) {
log.info("|酒店分销|二级降级|tenant={}|count={}|", tid, n);
}
}
catch (Exception e) {
log.error("|酒店分销|二级降级任务异常|", e);
}
}
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmHotelFxAgentMapper extends PjcloudBaseMapper<SmHotelFxAgentEntity> {
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxCommissionEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmHotelFxCommissionMapper extends PjcloudBaseMapper<SmHotelFxCommissionEntity> {
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxConfigEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmHotelFxConfigMapper extends PjcloudBaseMapper<SmHotelFxConfigEntity> {
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxExitEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmHotelFxExitMapper extends PjcloudBaseMapper<SmHotelFxExitEntity> {
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxHotelBindEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmHotelFxHotelBindMapper extends PjcloudBaseMapper<SmHotelFxHotelBindEntity> {
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxL1ApplyEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmHotelFxL1ApplyMapper extends PjcloudBaseMapper<SmHotelFxL1ApplyEntity> {
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxWithdrawEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmHotelFxWithdrawMapper extends PjcloudBaseMapper<SmHotelFxWithdrawEntity> {
}
@@ -0,0 +1,40 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxHotelBindEntity;
import java.math.BigDecimal;
public interface SmHotelFxAgentService extends IService<SmHotelFxAgentEntity> {
Page<SmHotelFxAgentEntity> pageAgents(Page<SmHotelFxAgentEntity> page, SmHotelFxAgentEntity query);
R<SmHotelFxAgentEntity> saveAgent(SmHotelFxAgentEntity body);
/** 划调二级负责区域(district adcode 列表 JSON) */
R<SmHotelFxAgentEntity> saveRegion(Long agentId, String regionJson, String cityAdcode);
R<Boolean> bindHotel(Long hotelId, Long agentId, String remark);
R<Boolean> unbindHotel(Long hotelId);
Page<SmHotelFxHotelBindEntity> pageBinds(Page<SmHotelFxHotelBindEntity> page, Long agentId, Long hotelId);
SmHotelFxAgentEntity getByUserId(Long userId);
SmHotelFxAgentEntity getBoundAgentByHotelId(Long hotelId);
/** 锁定佣金后:累加终身晋升分,并尝试 4→3 / 3→2 自动晋升 */
void onPromoteBaseLocked(Long ownerAgentId, BigDecimal promoteBaseAmount);
/**
* 锁定佣金后:若开发员为二级,且酒店在其区域内、绑定时间≥窗口起算,则累加保级窗口分。
*/
void onDemoteWindowEligible(Long ownerAgentId, Long hotelId, BigDecimal promoteBaseAmount);
/** 扫描到期二级:窗口内未达 demoteMin 则降为三级 */
int processDueDemotions();
}
@@ -0,0 +1,31 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxCommissionEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
import java.util.Map;
/**
* 酒店分销佣金:下单记预计、退房 T+1 锁定、取消作废;金额按配置快照留存。
*/
public interface SmHotelFxCommissionService extends IService<SmHotelFxCommissionEntity> {
/** 订单创建/上游成功后生成预计佣金(幂等) */
void createEstimatedForOrder(SmHotelOrderEntity order);
/** 订单取消:预计佣金作废 */
void cancelForOrder(Long orderId);
/** 退房日次日锁定预计佣金 */
int lockDueCommissions();
Page<SmHotelFxCommissionEntity> pageAdmin(Page<SmHotelFxCommissionEntity> page, SmHotelFxCommissionEntity query);
/** App:当前登录用户作为分销员的佣金汇总 + 分页 */
R<Map<String, Object>> mySummary();
Page<SmHotelFxCommissionEntity> pageMine(Page<SmHotelFxCommissionEntity> page, String status);
}
@@ -0,0 +1,19 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.dto.SmHotelFxLevelRuleDTO;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxConfigEntity;
import java.util.List;
public interface SmHotelFxConfigService extends IService<SmHotelFxConfigEntity> {
SmHotelFxConfigEntity getOrCreateCurrent();
R<SmHotelFxConfigEntity> saveCurrent(SmHotelFxConfigEntity body);
List<SmHotelFxLevelRuleDTO> parseLevelRules(SmHotelFxConfigEntity cfg);
boolean isEnabled(SmHotelFxConfigEntity cfg);
}
@@ -0,0 +1,18 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxExitEntity;
public interface SmHotelFxExitService extends IService<SmHotelFxExitEntity> {
Page<SmHotelFxExitEntity> pageAdmin(Page<SmHotelFxExitEntity> page, SmHotelFxExitEntity query);
R<SmHotelFxExitEntity> applyMine(String reason);
/**
* 审核退出:通过时必须指定 successorAgentId,重分配酒店绑定、下级挂靠,并作废预计佣金。
*/
R<Boolean> audit(Long id, boolean approved, Long successorAgentId, String auditRemark);
}
@@ -0,0 +1,16 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxL1ApplyEntity;
public interface SmHotelFxL1ApplyService extends IService<SmHotelFxL1ApplyEntity> {
Page<SmHotelFxL1ApplyEntity> pageAdmin(Page<SmHotelFxL1ApplyEntity> page, SmHotelFxL1ApplyEntity query);
/** App:二级达标后申请城市合伙人 */
R<SmHotelFxL1ApplyEntity> applyMine(String cityAdcode, String cityName, String remark);
R<Boolean> audit(Long id, boolean approved, String auditRemark);
}
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.supermarket.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxWithdrawEntity;
import java.util.Map;
public interface SmHotelFxWithdrawService extends IService<SmHotelFxWithdrawEntity> {
Page<SmHotelFxWithdrawEntity> pageAdmin(Page<SmHotelFxWithdrawEntity> page, SmHotelFxWithdrawEntity query);
Page<SmHotelFxWithdrawEntity> pageMine(Page<SmHotelFxWithdrawEntity> page, String status);
/** 可提现:已锁定且未挂提现单的佣金合计 */
R<Map<String, Object>> withdrawableMine();
R<SmHotelFxWithdrawEntity> applyMine(String accountName, String accountNo, String bankName, String remark);
R<Boolean> audit(Long id, boolean approved, String auditRemark);
/** 已审通过后标记打款完成 */
R<Boolean> markPaid(Long id, String remark);
}
@@ -0,0 +1,489 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.dto.SmHotelFxLevelRuleDTO;
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxHotelBindEntity;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxAgentMapper;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxHotelBindMapper;
import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class SmHotelFxAgentServiceImpl extends ServiceImpl<SmHotelFxAgentMapper, SmHotelFxAgentEntity>
implements SmHotelFxAgentService {
private final SmHotelFxHotelBindMapper bindMapper;
private final SmHotelMapper hotelMapper;
private final SmHotelFxConfigService configService;
@Override
public Page<SmHotelFxAgentEntity> pageAgents(Page<SmHotelFxAgentEntity> page, SmHotelFxAgentEntity query) {
Page<SmHotelFxAgentEntity> result = page(page, Wrappers.<SmHotelFxAgentEntity>lambdaQuery()
.eq(query.getLevelNo() != null, SmHotelFxAgentEntity::getLevelNo, query.getLevelNo())
.eq(StrUtil.isNotBlank(query.getStatus()), SmHotelFxAgentEntity::getStatus, query.getStatus())
.like(StrUtil.isNotBlank(query.getRealName()), SmHotelFxAgentEntity::getRealName, query.getRealName())
.like(StrUtil.isNotBlank(query.getMobile()), SmHotelFxAgentEntity::getMobile, query.getMobile())
.like(StrUtil.isNotBlank(query.getUsername()), SmHotelFxAgentEntity::getUsername, query.getUsername())
.orderByAsc(SmHotelFxAgentEntity::getLevelNo)
.orderByDesc(SmHotelFxAgentEntity::getId));
fillExtras(result.getRecords());
return result;
}
private void fillExtras(List<SmHotelFxAgentEntity> list) {
if (list == null || list.isEmpty()) {
return;
}
List<Long> parentIds = list.stream().map(SmHotelFxAgentEntity::getParentId).filter(Objects::nonNull).distinct()
.toList();
Map<Long, String> parentName = Map.of();
if (!parentIds.isEmpty()) {
parentName = listByIds(parentIds).stream()
.collect(Collectors.toMap(SmHotelFxAgentEntity::getId,
a -> StrUtil.blankToDefault(a.getRealName(), a.getUsername()), (a, b) -> a));
}
List<Long> ids = list.stream().map(SmHotelFxAgentEntity::getId).toList();
List<SmHotelFxHotelBindEntity> binds = bindMapper.selectList(Wrappers.<SmHotelFxHotelBindEntity>lambdaQuery()
.in(SmHotelFxHotelBindEntity::getAgentId, ids)
.eq(SmHotelFxHotelBindEntity::getBindStatus, "BOUND"));
Map<Long, Long> cnt = binds.stream()
.collect(Collectors.groupingBy(SmHotelFxHotelBindEntity::getAgentId, Collectors.counting()));
for (SmHotelFxAgentEntity a : list) {
a.setParentName(a.getParentId() == null ? null : parentName.get(a.getParentId()));
a.setHotelCount(cnt.getOrDefault(a.getId(), 0L).intValue());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<SmHotelFxAgentEntity> saveAgent(SmHotelFxAgentEntity body) {
if (body == null || body.getUserId() == null) {
return R.failed("请选择系统用户");
}
int level = body.getLevelNo() == null ? 4 : body.getLevelNo();
if (level < 1 || level > 5) {
return R.failed("层级须在 1~5");
}
body.setLevelNo(level);
if (StrUtil.isBlank(body.getStatus())) {
body.setStatus("ACTIVE");
}
if (body.getId() == null) {
Long exists = count(Wrappers.<SmHotelFxAgentEntity>lambdaQuery()
.eq(SmHotelFxAgentEntity::getUserId, body.getUserId()));
if (exists != null && exists > 0) {
return R.failed("该用户已是分销员");
}
if (StrUtil.isBlank(body.getInviteCode())) {
body.setInviteCode("FX" + IdUtil.getSnowflakeNextIdStr().substring(10));
}
if (body.getPromoteScore() == null) {
body.setPromoteScore(BigDecimal.ZERO);
}
if (body.getDemoteWindowScore() == null) {
body.setDemoteWindowScore(BigDecimal.ZERO);
}
if (body.getParentId() != null && (body.getLevelNo() == null || body.getLevelNo() == 4)) {
SmHotelFxAgentEntity parent = getById(body.getParentId());
if (parent != null && Integer.valueOf(4).equals(parent.getLevelNo())) {
body.setLevelNo(5);
}
}
save(body);
if (Integer.valueOf(2).equals(body.getLevelNo())) {
initLevel2DemoteWindow(body);
updateById(body);
}
}
else {
SmHotelFxAgentEntity old = getById(body.getId());
if (old == null) {
return R.failed("分销员不存在");
}
Integer prevLevel = old.getLevelNo();
old.setRealName(body.getRealName());
old.setMobile(body.getMobile());
old.setUsername(body.getUsername());
old.setLevelNo(body.getLevelNo());
old.setParentId(body.getParentId());
old.setCityAdcode(body.getCityAdcode());
old.setRegionJson(body.getRegionJson());
old.setStatus(body.getStatus());
old.setRemark(body.getRemark());
// 人工调到二级:开启保级窗口;从二级调走:清窗口
if (Integer.valueOf(2).equals(old.getLevelNo()) && !Integer.valueOf(2).equals(prevLevel)) {
initLevel2DemoteWindow(old);
}
else if (!Integer.valueOf(2).equals(old.getLevelNo()) && Integer.valueOf(2).equals(prevLevel)) {
clearDemoteWindow(old);
}
updateById(old);
body = old;
}
return R.ok(body);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<SmHotelFxAgentEntity> saveRegion(Long agentId, String regionJson, String cityAdcode) {
if (agentId == null) {
return R.failed("分销员为空");
}
SmHotelFxAgentEntity agent = getById(agentId);
if (agent == null) {
return R.failed("分销员不存在");
}
if (StrUtil.isNotBlank(regionJson)) {
try {
List<?> parsed = JSONUtil.parseArray(regionJson);
if (parsed == null) {
return R.failed("区域 JSON 无效,需为 adcode 数组");
}
agent.setRegionJson(JSONUtil.toJsonStr(parsed));
}
catch (Exception e) {
return R.failed("区域 JSON 无效,需为 adcode 数组,如 [\"330402\",\"330411\"]");
}
}
else {
agent.setRegionJson(null);
}
if (cityAdcode != null) {
agent.setCityAdcode(StrUtil.blankToDefault(cityAdcode, null));
}
updateById(agent);
return R.ok(agent);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> bindHotel(Long hotelId, Long agentId, String remark) {
if (hotelId == null || agentId == null) {
return R.failed("酒店与分销员不能为空");
}
SmHotelEntity hotel = hotelMapper.selectById(hotelId);
if (hotel == null) {
return R.failed("酒店不存在");
}
SmHotelFxAgentEntity agent = getById(agentId);
if (agent == null || !"ACTIVE".equals(agent.getStatus())) {
return R.failed("分销员不可用");
}
SmHotelFxHotelBindEntity exist = bindMapper.selectOne(Wrappers.<SmHotelFxHotelBindEntity>lambdaQuery()
.eq(SmHotelFxHotelBindEntity::getHotelId, hotelId)
.last("limit 1"));
LocalDateTime now = LocalDateTime.now();
if (exist == null) {
SmHotelFxHotelBindEntity row = new SmHotelFxHotelBindEntity();
row.setHotelId(hotelId);
row.setAgentId(agentId);
row.setBindStatus("BOUND");
row.setAuditRemark(remark);
row.setCreateTime(now);
bindMapper.insert(row);
}
else {
boolean rebound = !"BOUND".equals(exist.getBindStatus())
|| !Objects.equals(exist.getAgentId(), agentId);
exist.setAgentId(agentId);
exist.setBindStatus("BOUND");
exist.setAuditRemark(remark);
// 解绑后再绑 / 换绑:视为「新审核酒店」,保级窗口可计入
if (rebound) {
exist.setCreateTime(now);
}
bindMapper.updateById(exist);
}
return R.ok(Boolean.TRUE);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> unbindHotel(Long hotelId) {
SmHotelFxHotelBindEntity exist = bindMapper.selectOne(Wrappers.<SmHotelFxHotelBindEntity>lambdaQuery()
.eq(SmHotelFxHotelBindEntity::getHotelId, hotelId)
.last("limit 1"));
if (exist == null) {
return R.ok(Boolean.TRUE);
}
exist.setBindStatus("UNBOUND");
bindMapper.updateById(exist);
return R.ok(Boolean.TRUE);
}
@Override
public Page<SmHotelFxHotelBindEntity> pageBinds(Page<SmHotelFxHotelBindEntity> page, Long agentId, Long hotelId) {
Page<SmHotelFxHotelBindEntity> result = bindMapper.selectPage(page,
Wrappers.<SmHotelFxHotelBindEntity>lambdaQuery()
.eq(agentId != null, SmHotelFxHotelBindEntity::getAgentId, agentId)
.eq(hotelId != null, SmHotelFxHotelBindEntity::getHotelId, hotelId)
.eq(SmHotelFxHotelBindEntity::getBindStatus, "BOUND")
.orderByDesc(SmHotelFxHotelBindEntity::getId));
for (SmHotelFxHotelBindEntity b : result.getRecords()) {
SmHotelEntity h = hotelMapper.selectById(b.getHotelId());
if (h != null) {
b.setHotelName(h.getHotelName());
b.setHotelCode(h.getHotelCode());
}
SmHotelFxAgentEntity a = getById(b.getAgentId());
if (a != null) {
b.setAgentName(StrUtil.blankToDefault(a.getRealName(), a.getUsername()));
}
}
return result;
}
@Override
public SmHotelFxAgentEntity getByUserId(Long userId) {
if (userId == null) {
return null;
}
return getOne(Wrappers.<SmHotelFxAgentEntity>lambdaQuery()
.eq(SmHotelFxAgentEntity::getUserId, userId)
.eq(SmHotelFxAgentEntity::getStatus, "ACTIVE")
.last("limit 1"), false);
}
@Override
public SmHotelFxAgentEntity getBoundAgentByHotelId(Long hotelId) {
if (hotelId == null) {
return null;
}
SmHotelFxHotelBindEntity bind = bindMapper.selectOne(Wrappers.<SmHotelFxHotelBindEntity>lambdaQuery()
.eq(SmHotelFxHotelBindEntity::getHotelId, hotelId)
.eq(SmHotelFxHotelBindEntity::getBindStatus, "BOUND")
.last("limit 1"));
if (bind == null) {
return null;
}
SmHotelFxAgentEntity agent = getById(bind.getAgentId());
if (agent == null || !"ACTIVE".equals(agent.getStatus())) {
return null;
}
return agent;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void onPromoteBaseLocked(Long ownerAgentId, BigDecimal promoteBaseAmount) {
if (ownerAgentId == null || promoteBaseAmount == null
|| promoteBaseAmount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
SmHotelFxAgentEntity agent = getById(ownerAgentId);
if (agent == null || !"ACTIVE".equals(agent.getStatus())) {
return;
}
BigDecimal score = agent.getPromoteScore() == null ? BigDecimal.ZERO : agent.getPromoteScore();
agent.setPromoteScore(score.add(promoteBaseAmount));
updateById(agent);
tryAutoPromote(agent);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void onDemoteWindowEligible(Long ownerAgentId, Long hotelId, BigDecimal promoteBaseAmount) {
if (ownerAgentId == null || hotelId == null || promoteBaseAmount == null
|| promoteBaseAmount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
SmHotelFxAgentEntity agent = getById(ownerAgentId);
if (agent == null || !"ACTIVE".equals(agent.getStatus()) || !Integer.valueOf(2).equals(agent.getLevelNo())) {
return;
}
if (agent.getDemoteWindowStart() == null) {
initLevel2DemoteWindow(agent);
updateById(agent);
}
if (!isHotelInAgentRegion(agent, hotelId)) {
return;
}
if (!isNewHotelForDemoteWindow(agent, hotelId)) {
return;
}
BigDecimal win = agent.getDemoteWindowScore() == null ? BigDecimal.ZERO : agent.getDemoteWindowScore();
agent.setDemoteWindowScore(win.add(promoteBaseAmount));
updateById(agent);
log.info("|酒店分销|保级窗口累加|agentId={}|hotelId={}|add={}|total={}|", ownerAgentId, hotelId,
promoteBaseAmount, agent.getDemoteWindowScore());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int processDueDemotions() {
SmHotelFxLevelRuleDTO rule2 = findLevelRule(2);
int windowDays = rule2 != null && rule2.getDemoteWindowDays() != null ? rule2.getDemoteWindowDays() : 90;
BigDecimal demoteMin = rule2 != null && rule2.getDemoteMin() != null ? rule2.getDemoteMin()
: new BigDecimal("40000");
LocalDateTime deadline = LocalDateTime.now().minusDays(windowDays);
List<SmHotelFxAgentEntity> due = list(Wrappers.<SmHotelFxAgentEntity>lambdaQuery()
.eq(SmHotelFxAgentEntity::getLevelNo, 2)
.eq(SmHotelFxAgentEntity::getStatus, "ACTIVE")
.isNotNull(SmHotelFxAgentEntity::getDemoteWindowStart)
.le(SmHotelFxAgentEntity::getDemoteWindowStart, deadline)
.last("limit 500"));
if (CollUtil.isEmpty(due)) {
return 0;
}
int n = 0;
for (SmHotelFxAgentEntity agent : due) {
BigDecimal win = agent.getDemoteWindowScore() == null ? BigDecimal.ZERO : agent.getDemoteWindowScore();
if (win.compareTo(demoteMin) >= 0) {
// 达标:滚动新窗口继续考核
initLevel2DemoteWindow(agent);
updateById(agent);
log.info("|酒店分销|二级保级通过|agentId={}|score={}|min={}|", agent.getId(), win, demoteMin);
continue;
}
demoteLevel2To3(agent);
n++;
}
return n;
}
/** 四级→三级、三级→二级自动升;二级→一级三期人工申请 */
private void tryAutoPromote(SmHotelFxAgentEntity agent) {
if (agent == null || agent.getLevelNo() == null) {
return;
}
int lv = agent.getLevelNo();
if (lv <= 2 || lv == 5) {
return;
}
List<SmHotelFxLevelRuleDTO> rules = configService.parseLevelRules(configService.getOrCreateCurrent());
int target = lv - 1;
SmHotelFxLevelRuleDTO targetRule = rules.stream().filter(r -> Objects.equals(r.getLevel(), target)).findFirst()
.orElse(null);
if (targetRule == null || targetRule.getPromoteMin() == null) {
return;
}
if (Boolean.FALSE.equals(targetRule.getCanPromote()) && target == 1) {
return;
}
BigDecimal score = agent.getPromoteScore() == null ? BigDecimal.ZERO : agent.getPromoteScore();
if (score.compareTo(targetRule.getPromoteMin()) < 0) {
return;
}
agent.setLevelNo(target);
if (target == 2) {
initLevel2DemoteWindow(agent);
}
updateById(agent);
log.info("|酒店分销|自动晋升|agentId={}|{}→{}|score={}|need={}|", agent.getId(), lv, target, score,
targetRule.getPromoteMin());
// 连续达标可连升(如一次锁定后从 4 直接够 2 的门槛)
if (target > 2) {
tryAutoPromote(agent);
}
}
private void demoteLevel2To3(SmHotelFxAgentEntity agent) {
BigDecimal win = agent.getDemoteWindowScore() == null ? BigDecimal.ZERO : agent.getDemoteWindowScore();
agent.setLevelNo(3);
clearDemoteWindow(agent);
// 佣金明细/历史锁定业绩保留;晋升分回落到「三级门槛」,避免立刻因终身分再升回二级
SmHotelFxLevelRuleDTO rule3 = findLevelRule(3);
BigDecimal floor = rule3 != null && rule3.getPromoteMin() != null ? rule3.getPromoteMin()
: new BigDecimal("50000");
agent.setPromoteScore(floor);
String tip = "二级保级未达标降三级(窗口分=" + win.toPlainString() + ")";
agent.setRemark(StrUtil.maxLength(StrUtil.blankToDefault(agent.getRemark(), "") + ";" + tip, 255));
updateById(agent);
log.info("|酒店分销|二级降级|agentId={}|windowScore={}|promoteScore→{}|", agent.getId(), win, floor);
}
private void initLevel2DemoteWindow(SmHotelFxAgentEntity agent) {
agent.setDemoteWindowStart(LocalDateTime.now());
agent.setDemoteWindowScore(BigDecimal.ZERO);
}
private void clearDemoteWindow(SmHotelFxAgentEntity agent) {
agent.setDemoteWindowStart(null);
agent.setDemoteWindowScore(BigDecimal.ZERO);
}
/** 酒店区县 adcode 是否在分销员 regionJson 内;未配置区域则不计保级分 */
private boolean isHotelInAgentRegion(SmHotelFxAgentEntity agent, Long hotelId) {
Set<String> codes = parseRegionAdcodes(agent.getRegionJson());
if (codes.isEmpty()) {
return false;
}
SmHotelEntity hotel = hotelMapper.selectById(hotelId);
if (hotel == null || hotel.getDistrictAdcode() == null) {
return false;
}
return codes.contains(String.valueOf(hotel.getDistrictAdcode()));
}
/** 升二级之后新绑定/再审的酒店才计入保级窗口 */
private boolean isNewHotelForDemoteWindow(SmHotelFxAgentEntity agent, Long hotelId) {
if (agent.getDemoteWindowStart() == null) {
return false;
}
SmHotelFxHotelBindEntity bind = bindMapper.selectOne(Wrappers.<SmHotelFxHotelBindEntity>lambdaQuery()
.eq(SmHotelFxHotelBindEntity::getHotelId, hotelId)
.eq(SmHotelFxHotelBindEntity::getAgentId, agent.getId())
.eq(SmHotelFxHotelBindEntity::getBindStatus, "BOUND")
.last("limit 1"));
if (bind == null || bind.getCreateTime() == null) {
return false;
}
return !bind.getCreateTime().isBefore(agent.getDemoteWindowStart());
}
private Set<String> parseRegionAdcodes(String regionJson) {
Set<String> set = new HashSet<>();
if (StrUtil.isBlank(regionJson)) {
return set;
}
try {
List<Object> arr = new ArrayList<>(JSONUtil.parseArray(regionJson));
for (Object o : arr) {
if (o == null) {
continue;
}
String s = String.valueOf(o).trim();
if (StrUtil.isNotBlank(s)) {
set.add(s);
}
}
}
catch (Exception ignored) {
// ignore bad json
}
return set;
}
private SmHotelFxLevelRuleDTO findLevelRule(int level) {
List<SmHotelFxLevelRuleDTO> rules = configService.parseLevelRules(configService.getOrCreateCurrent());
return rules.stream().filter(r -> Objects.equals(r.getLevel(), level)).findFirst().orElse(null);
}
}
@@ -0,0 +1,360 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.supermarket.dto.SmHotelFxLevelRuleDTO;
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxCommissionEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxConfigEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxCommissionMapper;
import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
import com.bsd.pjcloud.supermarket.mapper.SmHotelOrderMapper;
import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxCommissionService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class SmHotelFxCommissionServiceImpl extends ServiceImpl<SmHotelFxCommissionMapper, SmHotelFxCommissionEntity>
implements SmHotelFxCommissionService {
private static final String ST_EST = "ESTIMATED";
private static final String ST_LOCK = "LOCKED";
private static final String ST_CANCEL = "CANCELLED";
private final SmHotelFxConfigService configService;
private final SmHotelFxAgentService agentService;
private final SmHotelMapper hotelMapper;
private final SmHotelOrderMapper orderMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void createEstimatedForOrder(SmHotelOrderEntity order) {
if (order == null || order.getId() == null || order.getHotelId() == null) {
return;
}
SmHotelFxConfigEntity cfg = configService.getOrCreateCurrent();
if (!configService.isEnabled(cfg)) {
return;
}
long exists = count(Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getOrderId, order.getId())
.ne(SmHotelFxCommissionEntity::getStatus, ST_CANCEL));
if (exists > 0) {
return;
}
SmHotelFxAgentEntity owner = agentService.getBoundAgentByHotelId(order.getHotelId());
if (owner == null) {
return;
}
BigDecimal cost = order.getCostAmount();
if (cost == null || cost.compareTo(BigDecimal.ZERO) <= 0) {
log.info("|酒店分销|跳过无成本订单|orderId={}|", order.getId());
return;
}
BigDecimal poolCostRate = nvl(cfg.getPoolCostRate(), "0.06");
BigDecimal poolSalesRate = nvl(cfg.getPoolSalesRate(), "0.30");
BigDecimal promoteBaseRate = nvl(cfg.getPromoteBaseRate(), "0.06");
BigDecimal pool = cost.multiply(poolCostRate).multiply(poolSalesRate).setScale(4, RoundingMode.HALF_UP);
BigDecimal promoteBase = cost.multiply(promoteBaseRate).setScale(4, RoundingMode.HALF_UP);
List<SmHotelFxLevelRuleDTO> rules = configService.parseLevelRules(cfg);
Map<Integer, SmHotelFxLevelRuleDTO> ruleByLevel = rules.stream()
.filter(r -> r.getLevel() != null)
.collect(Collectors.toMap(SmHotelFxLevelRuleDTO::getLevel, r -> r, (a, b) -> a));
// 开发员向上链路:每人只拿「自身当前层级」对应份额(缺层则该份额不发)
List<SmHotelFxAgentEntity> chain = buildUplineChain(owner);
Map<String, Object> snapshot = buildSnapshot(cfg, rules, cost, pool, promoteBase, owner.getId());
List<SmHotelFxCommissionEntity> rows = new ArrayList<>();
for (SmHotelFxAgentEntity agent : chain) {
int lv = agent.getLevelNo() == null ? 0 : agent.getLevelNo();
SmHotelFxLevelRuleDTO rule = ruleByLevel.get(lv);
if (rule == null || rule.getShareOfPool() == null
|| rule.getShareOfPool().compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
BigDecimal amount = pool.multiply(rule.getShareOfPool()).setScale(4, RoundingMode.HALF_UP);
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
SmHotelFxCommissionEntity row = new SmHotelFxCommissionEntity();
row.setOrderId(order.getId());
row.setHotelId(order.getHotelId());
row.setAgentId(agent.getId());
row.setOwnerAgentId(owner.getId());
row.setChainLevel(lv);
row.setAgentLevel(lv);
row.setCostAmount(cost);
row.setPoolAmount(pool);
row.setShareRate(rule.getShareOfPool());
row.setCommissionAmount(amount);
row.setPromoteBaseAmount(promoteBase);
row.setStatus(ST_EST);
row.setCheckOutDate(order.getCheckOutDate());
row.setConfigSnapshot(JSONUtil.toJsonStr(snapshot));
row.setRemark("预计佣金");
rows.add(row);
}
if (rows.isEmpty()) {
return;
}
saveBatch(rows);
log.info("|酒店分销|已记预计佣金|orderId={}|hotelId={}|lines={}|pool={}|", order.getId(), order.getHotelId(),
rows.size(), pool);
}
/** 从开发员沿 parent 向上,同层级只保留最先遇到的人 */
private List<SmHotelFxAgentEntity> buildUplineChain(SmHotelFxAgentEntity owner) {
List<SmHotelFxAgentEntity> chain = new ArrayList<>();
Map<Integer, Boolean> seenLevel = new HashMap<>();
SmHotelFxAgentEntity cur = owner;
int guard = 0;
while (cur != null && guard++ < 16) {
Integer lv = cur.getLevelNo();
if (lv != null && !Boolean.TRUE.equals(seenLevel.get(lv))) {
seenLevel.put(lv, true);
chain.add(cur);
}
if (cur.getParentId() == null) {
break;
}
cur = agentService.getById(cur.getParentId());
if (cur == null || !"ACTIVE".equals(cur.getStatus())) {
break;
}
}
return chain;
}
private Map<String, Object> buildSnapshot(SmHotelFxConfigEntity cfg, List<SmHotelFxLevelRuleDTO> rules,
BigDecimal cost, BigDecimal pool, BigDecimal promoteBase, Long ownerAgentId) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("poolCostRate", cfg.getPoolCostRate());
m.put("poolSalesRate", cfg.getPoolSalesRate());
m.put("promoteBaseRate", cfg.getPromoteBaseRate());
m.put("levelRules", rules);
m.put("costAmount", cost);
m.put("poolAmount", pool);
m.put("promoteBaseAmount", promoteBase);
m.put("ownerAgentId", ownerAgentId);
m.put("formula", "pool = cost * poolCostRate * poolSalesRate; commission = pool * shareOfPool");
return m;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelForOrder(Long orderId) {
if (orderId == null) {
return;
}
List<SmHotelFxCommissionEntity> list = list(Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getOrderId, orderId)
.eq(SmHotelFxCommissionEntity::getStatus, ST_EST));
if (CollUtil.isEmpty(list)) {
return;
}
LocalDateTime now = LocalDateTime.now();
for (SmHotelFxCommissionEntity row : list) {
row.setStatus(ST_CANCEL);
row.setRemark("订单取消作废");
row.setUpdateTime(now);
}
updateBatchById(list);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int lockDueCommissions() {
// 退房日次日 00:00 起可锁:check_out_date < today
LocalDate today = LocalDate.now();
List<SmHotelFxCommissionEntity> due = list(Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getStatus, ST_EST)
.isNotNull(SmHotelFxCommissionEntity::getCheckOutDate)
.lt(SmHotelFxCommissionEntity::getCheckOutDate, today)
.last("limit 2000"));
if (CollUtil.isEmpty(due)) {
return 0;
}
LocalDateTime now = LocalDateTime.now();
// owner 晋升分按人合并;保级窗口按「每笔×酒店」逐条判断区域/新酒店
Map<Long, BigDecimal> promoteAdd = new HashMap<>();
List<SmHotelFxCommissionEntity> ownerRows = new ArrayList<>();
for (SmHotelFxCommissionEntity row : due) {
row.setStatus(ST_LOCK);
row.setLockTime(now);
row.setRemark("退房T+1锁定");
if (Objects.equals(row.getAgentId(), row.getOwnerAgentId()) && row.getPromoteBaseAmount() != null) {
promoteAdd.merge(row.getOwnerAgentId(), row.getPromoteBaseAmount(), BigDecimal::add);
ownerRows.add(row);
}
}
updateBatchById(due);
for (SmHotelFxCommissionEntity row : ownerRows) {
agentService.onDemoteWindowEligible(row.getOwnerAgentId(), row.getHotelId(), row.getPromoteBaseAmount());
}
for (Map.Entry<Long, BigDecimal> e : promoteAdd.entrySet()) {
agentService.onPromoteBaseLocked(e.getKey(), e.getValue());
}
return due.size();
}
@Override
public Page<SmHotelFxCommissionEntity> pageAdmin(Page<SmHotelFxCommissionEntity> page,
SmHotelFxCommissionEntity query) {
Page<SmHotelFxCommissionEntity> result = page(page, Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(query.getAgentId() != null, SmHotelFxCommissionEntity::getAgentId, query.getAgentId())
.eq(query.getHotelId() != null, SmHotelFxCommissionEntity::getHotelId, query.getHotelId())
.eq(query.getOrderId() != null, SmHotelFxCommissionEntity::getOrderId, query.getOrderId())
.eq(StrUtil.isNotBlank(query.getStatus()), SmHotelFxCommissionEntity::getStatus, query.getStatus())
.orderByDesc(SmHotelFxCommissionEntity::getId));
fillDisplay(result.getRecords());
return result;
}
@Override
public R<Map<String, Object>> mySummary() {
SmHotelFxAgentEntity me = currentAgent();
if (me == null) {
return R.failed("当前账号不是分销员");
}
List<SmHotelFxCommissionEntity> all = list(Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getAgentId, me.getId())
.ne(SmHotelFxCommissionEntity::getStatus, ST_CANCEL));
BigDecimal estimated = BigDecimal.ZERO;
BigDecimal locked = BigDecimal.ZERO;
BigDecimal withdrawn = BigDecimal.ZERO;
BigDecimal withdrawable = BigDecimal.ZERO;
for (SmHotelFxCommissionEntity c : all) {
BigDecimal amt = c.getCommissionAmount() == null ? BigDecimal.ZERO : c.getCommissionAmount();
if (ST_EST.equals(c.getStatus())) {
estimated = estimated.add(amt);
}
else if (ST_LOCK.equals(c.getStatus())) {
locked = locked.add(amt);
if (c.getWithdrawId() == null) {
withdrawable = withdrawable.add(amt);
}
}
else if ("WITHDRAWN".equals(c.getStatus())) {
withdrawn = withdrawn.add(amt);
}
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("agentId", me.getId());
data.put("levelNo", me.getLevelNo());
data.put("realName", me.getRealName());
data.put("promoteScore", me.getPromoteScore());
data.put("cityAdcode", me.getCityAdcode());
data.put("demoteWindowScore", me.getDemoteWindowScore());
data.put("demoteWindowStart", me.getDemoteWindowStart());
data.put("regionJson", me.getRegionJson());
data.put("estimatedAmount", estimated);
data.put("lockedAmount", locked);
data.put("withdrawableAmount", withdrawable);
data.put("withdrawnAmount", withdrawn);
data.put("totalAmount", estimated.add(locked));
List<SmHotelFxLevelRuleDTO> rules = configService.parseLevelRules(configService.getOrCreateCurrent());
if (Integer.valueOf(2).equals(me.getLevelNo())) {
SmHotelFxLevelRuleDTO r2 = rules.stream().filter(r -> Objects.equals(r.getLevel(), 2)).findFirst()
.orElse(null);
data.put("demoteMin", r2 != null ? r2.getDemoteMin() : new BigDecimal("40000"));
data.put("demoteWindowDays", r2 != null ? r2.getDemoteWindowDays() : 90);
SmHotelFxLevelRuleDTO r1 = rules.stream().filter(r -> Objects.equals(r.getLevel(), 1)).findFirst()
.orElse(null);
BigDecimal l1Need = r1 != null && r1.getPromoteMin() != null ? r1.getPromoteMin() : new BigDecimal("500000");
data.put("l1PromoteMin", l1Need);
BigDecimal score = me.getPromoteScore() == null ? BigDecimal.ZERO : me.getPromoteScore();
data.put("canApplyL1", score.compareTo(l1Need) >= 0);
}
else {
data.put("canApplyL1", false);
}
return R.ok(data);
}
@Override
public Page<SmHotelFxCommissionEntity> pageMine(Page<SmHotelFxCommissionEntity> page, String status) {
SmHotelFxAgentEntity me = currentAgent();
if (me == null) {
return page;
}
Page<SmHotelFxCommissionEntity> result = page(page, Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getAgentId, me.getId())
.eq(StrUtil.isNotBlank(status), SmHotelFxCommissionEntity::getStatus, status)
.ne(SmHotelFxCommissionEntity::getStatus, ST_CANCEL)
.orderByDesc(SmHotelFxCommissionEntity::getId));
fillDisplay(result.getRecords());
return result;
}
private SmHotelFxAgentEntity currentAgent() {
PjcloudUser user = SecurityUtils.getUser();
if (user == null || user.getId() == null) {
return null;
}
return agentService.getByUserId(user.getId());
}
private void fillDisplay(List<SmHotelFxCommissionEntity> list) {
if (CollUtil.isEmpty(list)) {
return;
}
for (SmHotelFxCommissionEntity c : list) {
if (c.getHotelId() != null) {
SmHotelEntity h = hotelMapper.selectById(c.getHotelId());
if (h != null) {
c.setHotelName(h.getHotelName());
}
}
if (c.getAgentId() != null) {
SmHotelFxAgentEntity a = agentService.getById(c.getAgentId());
if (a != null) {
c.setAgentName(StrUtil.blankToDefault(a.getRealName(), a.getUsername()));
}
}
if (c.getOrderId() != null) {
SmHotelOrderEntity o = orderMapper.selectById(c.getOrderId());
if (o != null) {
c.setChannelNo(o.getChannelNo());
}
}
}
}
private static BigDecimal nvl(BigDecimal v, String def) {
return v == null ? new BigDecimal(def) : v;
}
}
@@ -0,0 +1,105 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.supermarket.dto.SmHotelFxLevelRuleDTO;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxConfigEntity;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxConfigMapper;
import com.bsd.pjcloud.supermarket.service.SmHotelFxConfigService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Service
public class SmHotelFxConfigServiceImpl extends ServiceImpl<SmHotelFxConfigMapper, SmHotelFxConfigEntity>
implements SmHotelFxConfigService {
private static final String DEFAULT_RULES = "["
+ "{\"level\":1,\"name\":\"城市合伙人\",\"shareOfPool\":0.40,\"canPromote\":false,\"promoteMin\":500000,\"demoteMin\":null,\"demoteWindowDays\":null},"
+ "{\"level\":2,\"name\":\"区域负责人\",\"shareOfPool\":0.30,\"canPromote\":true,\"promoteMin\":200000,\"demoteMin\":40000,\"demoteWindowDays\":90},"
+ "{\"level\":3,\"name\":\"团队骨干\",\"shareOfPool\":0.20,\"canPromote\":true,\"promoteMin\":50000,\"demoteMin\":null,\"demoteWindowDays\":null},"
+ "{\"level\":4,\"name\":\"开发业务员\",\"shareOfPool\":0.10,\"canPromote\":true,\"promoteMin\":null,\"demoteMin\":null,\"demoteWindowDays\":null},"
+ "{\"level\":5,\"name\":\"被邀请人\",\"shareOfPool\":0.00,\"canPromote\":false,\"promoteMin\":null,\"demoteMin\":null,\"demoteWindowDays\":null}"
+ "]";
@Override
public SmHotelFxConfigEntity getOrCreateCurrent() {
SmHotelFxConfigEntity one = getOne(Wrappers.<SmHotelFxConfigEntity>lambdaQuery().last("limit 1"), false);
if (one != null) {
return one;
}
SmHotelFxConfigEntity created = new SmHotelFxConfigEntity();
created.setEnabled("1");
created.setPoolCostRate(new BigDecimal("0.06"));
created.setPoolSalesRate(new BigDecimal("0.30"));
created.setPromoteBaseRate(new BigDecimal("0.06"));
created.setLevelRulesJson(DEFAULT_RULES);
created.setRemark("系统自动初始化");
save(created);
return created;
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<SmHotelFxConfigEntity> saveCurrent(SmHotelFxConfigEntity body) {
if (body == null) {
return R.failed("参数为空");
}
SmHotelFxConfigEntity cur = getOrCreateCurrent();
if (body.getEnabled() != null) {
cur.setEnabled(body.getEnabled());
}
if (body.getPoolCostRate() != null) {
cur.setPoolCostRate(body.getPoolCostRate());
}
if (body.getPoolSalesRate() != null) {
cur.setPoolSalesRate(body.getPoolSalesRate());
}
if (body.getPromoteBaseRate() != null) {
cur.setPromoteBaseRate(body.getPromoteBaseRate());
}
if (body.getLevelRulesJson() != null) {
List<SmHotelFxLevelRuleDTO> rules = parseLevelRules(body);
if (rules.isEmpty()) {
return R.failed("层级规则 JSON 无效");
}
cur.setLevelRulesJson(JSONUtil.toJsonStr(rules));
}
if (body.getRemark() != null) {
cur.setRemark(body.getRemark());
}
updateById(cur);
return R.ok(cur);
}
@Override
public List<SmHotelFxLevelRuleDTO> parseLevelRules(SmHotelFxConfigEntity cfg) {
String json = cfg == null ? null : cfg.getLevelRulesJson();
if (StrUtil.isBlank(json)) {
json = DEFAULT_RULES;
}
try {
List<SmHotelFxLevelRuleDTO> list = JSONUtil.toList(json, SmHotelFxLevelRuleDTO.class);
if (list == null) {
return new ArrayList<>();
}
list.sort(Comparator.comparing(r -> r.getLevel() == null ? 99 : r.getLevel()));
return list;
}
catch (Exception e) {
return new ArrayList<>();
}
}
@Override
public boolean isEnabled(SmHotelFxConfigEntity cfg) {
return cfg != null && "1".equals(cfg.getEnabled());
}
}
@@ -0,0 +1,172 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxCommissionEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxExitEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxHotelBindEntity;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxCommissionMapper;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxExitMapper;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxHotelBindMapper;
import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxExitService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class SmHotelFxExitServiceImpl extends ServiceImpl<SmHotelFxExitMapper, SmHotelFxExitEntity>
implements SmHotelFxExitService {
private final SmHotelFxAgentService agentService;
private final SmHotelFxHotelBindMapper bindMapper;
private final SmHotelFxCommissionMapper commissionMapper;
@Override
public Page<SmHotelFxExitEntity> pageAdmin(Page<SmHotelFxExitEntity> page, SmHotelFxExitEntity query) {
Page<SmHotelFxExitEntity> result = page(page, Wrappers.<SmHotelFxExitEntity>lambdaQuery()
.eq(query.getAgentId() != null, SmHotelFxExitEntity::getAgentId, query.getAgentId())
.eq(StrUtil.isNotBlank(query.getStatus()), SmHotelFxExitEntity::getStatus, query.getStatus())
.orderByDesc(SmHotelFxExitEntity::getId));
fillNames(result.getRecords());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<SmHotelFxExitEntity> applyMine(String reason) {
PjcloudUser user = SecurityUtils.getUser();
if (user == null || user.getId() == null) {
return R.failed("未登录");
}
SmHotelFxAgentEntity me = agentService.getByUserId(user.getId());
if (me == null || !"ACTIVE".equals(me.getStatus())) {
return R.failed("当前不是在职分销员");
}
long pending = count(Wrappers.<SmHotelFxExitEntity>lambdaQuery()
.eq(SmHotelFxExitEntity::getAgentId, me.getId())
.eq(SmHotelFxExitEntity::getStatus, "PENDING"));
if (pending > 0) {
return R.failed("已有待审退出申请");
}
SmHotelFxExitEntity row = new SmHotelFxExitEntity();
row.setAgentId(me.getId());
row.setUserId(me.getUserId());
row.setReason(StrUtil.blankToDefault(reason, "主动申请退出"));
row.setStatus("PENDING");
save(row);
return R.ok(row);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> audit(Long id, boolean approved, Long successorAgentId, String auditRemark) {
SmHotelFxExitEntity row = getById(id);
if (row == null) {
return R.failed("退出单不存在");
}
if (!"PENDING".equals(row.getStatus())) {
return R.failed("已处理");
}
PjcloudUser user = SecurityUtils.getUser();
row.setAuditBy(user != null ? user.getUsername() : null);
row.setAuditTime(LocalDateTime.now());
row.setAuditRemark(auditRemark);
if (!approved) {
row.setStatus("REJECTED");
updateById(row);
return R.ok(Boolean.TRUE, "已驳回退出申请");
}
if (successorAgentId == null) {
return R.failed("通过退出须指定酒店接收分销员");
}
if (successorAgentId.equals(row.getAgentId())) {
return R.failed("接收人不能是本人");
}
SmHotelFxAgentEntity successor = agentService.getById(successorAgentId);
if (successor == null || !"ACTIVE".equals(successor.getStatus())) {
return R.failed("接收分销员不可用");
}
SmHotelFxAgentEntity agent = agentService.getById(row.getAgentId());
if (agent == null) {
return R.failed("退出分销员不存在");
}
// 1) 酒店绑定重分配
List<SmHotelFxHotelBindEntity> binds = bindMapper.selectList(Wrappers.<SmHotelFxHotelBindEntity>lambdaQuery()
.eq(SmHotelFxHotelBindEntity::getAgentId, agent.getId())
.eq(SmHotelFxHotelBindEntity::getBindStatus, "BOUND"));
LocalDateTime now = LocalDateTime.now();
for (SmHotelFxHotelBindEntity b : binds) {
b.setAgentId(successorAgentId);
b.setCreateTime(now);
b.setAuditRemark(StrUtil.maxLength("退出重分配自 agent=" + agent.getId(), 255));
bindMapper.updateById(b);
}
// 2) 下级挂靠到原上级(或接收人上级链之外:挂到退出者的 parent)
Long newParent = agent.getParentId();
List<SmHotelFxAgentEntity> children = agentService.list(Wrappers.<SmHotelFxAgentEntity>lambdaQuery()
.eq(SmHotelFxAgentEntity::getParentId, agent.getId())
.eq(SmHotelFxAgentEntity::getStatus, "ACTIVE"));
for (SmHotelFxAgentEntity child : children) {
child.setParentId(newParent);
agentService.updateById(child);
}
// 3) 作废本人预计佣金(已锁定可继续提现)
List<SmHotelFxCommissionEntity> est = commissionMapper.selectList(Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getAgentId, agent.getId())
.eq(SmHotelFxCommissionEntity::getStatus, "ESTIMATED"));
for (SmHotelFxCommissionEntity c : est) {
c.setStatus("CANCELLED");
c.setRemark("分销员退出作废预计佣金");
commissionMapper.updateById(c);
}
agent.setStatus("EXIT");
agent.setRemark(StrUtil.maxLength(StrUtil.blankToDefault(agent.getRemark(), "") + ";已退出", 255));
agentService.updateById(agent);
row.setSuccessorAgentId(successorAgentId);
row.setHotelCount(binds.size());
row.setStatus("DONE");
updateById(row);
log.info("|酒店分销|退出完成|agentId={}|successor={}|hotels={}|children={}|", agent.getId(), successorAgentId,
binds.size(), children.size());
return R.ok(Boolean.TRUE, "已退出并重分配 " + binds.size() + " 家酒店");
}
private void fillNames(List<SmHotelFxExitEntity> list) {
if (CollUtil.isEmpty(list)) {
return;
}
for (SmHotelFxExitEntity row : list) {
SmHotelFxAgentEntity a = agentService.getById(row.getAgentId());
if (a != null) {
row.setAgentName(StrUtil.blankToDefault(a.getRealName(), a.getUsername()));
}
if (row.getSuccessorAgentId() != null) {
SmHotelFxAgentEntity s = agentService.getById(row.getSuccessorAgentId());
if (s != null) {
row.setSuccessorName(StrUtil.blankToDefault(s.getRealName(), s.getUsername()));
}
}
}
}
}
@@ -0,0 +1,163 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.supermarket.dto.SmHotelFxLevelRuleDTO;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxL1ApplyEntity;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxL1ApplyMapper;
import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxConfigService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxL1ApplyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
public class SmHotelFxL1ApplyServiceImpl extends ServiceImpl<SmHotelFxL1ApplyMapper, SmHotelFxL1ApplyEntity>
implements SmHotelFxL1ApplyService {
private final SmHotelFxAgentService agentService;
private final SmHotelFxConfigService configService;
@Override
public Page<SmHotelFxL1ApplyEntity> pageAdmin(Page<SmHotelFxL1ApplyEntity> page, SmHotelFxL1ApplyEntity query) {
Page<SmHotelFxL1ApplyEntity> result = page(page, Wrappers.<SmHotelFxL1ApplyEntity>lambdaQuery()
.eq(query.getAgentId() != null, SmHotelFxL1ApplyEntity::getAgentId, query.getAgentId())
.eq(StrUtil.isNotBlank(query.getStatus()), SmHotelFxL1ApplyEntity::getStatus, query.getStatus())
.eq(StrUtil.isNotBlank(query.getCityAdcode()), SmHotelFxL1ApplyEntity::getCityAdcode, query.getCityAdcode())
.orderByDesc(SmHotelFxL1ApplyEntity::getId));
fillAgent(result.getRecords());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<SmHotelFxL1ApplyEntity> applyMine(String cityAdcode, String cityName, String remark) {
SmHotelFxAgentEntity me = requireActiveAgent();
if (me == null) {
return R.failed("当前账号不是在职分销员");
}
if (!Integer.valueOf(2).equals(me.getLevelNo())) {
return R.failed("仅二级可申请城市合伙人");
}
if (StrUtil.isBlank(cityAdcode)) {
return R.failed("请填写申请城市 adcode");
}
BigDecimal need = level1PromoteMin();
BigDecimal score = me.getPromoteScore() == null ? BigDecimal.ZERO : me.getPromoteScore();
if (score.compareTo(need) < 0) {
return R.failed("晋升累计未达门槛(需≥" + need.toPlainString() + ")");
}
long pending = count(Wrappers.<SmHotelFxL1ApplyEntity>lambdaQuery()
.eq(SmHotelFxL1ApplyEntity::getAgentId, me.getId())
.eq(SmHotelFxL1ApplyEntity::getStatus, "PENDING"));
if (pending > 0) {
return R.failed("已有待审核申请,请勿重复提交");
}
SmHotelFxL1ApplyEntity row = new SmHotelFxL1ApplyEntity();
row.setAgentId(me.getId());
row.setUserId(me.getUserId());
row.setCityAdcode(cityAdcode.trim());
row.setCityName(cityName);
row.setPromoteScore(score);
row.setStatus("PENDING");
row.setApplyRemark(remark);
save(row);
return R.ok(row);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> audit(Long id, boolean approved, String auditRemark) {
SmHotelFxL1ApplyEntity row = getById(id);
if (row == null) {
return R.failed("申请不存在");
}
if (!"PENDING".equals(row.getStatus())) {
return R.failed("申请已处理");
}
PjcloudUser user = SecurityUtils.getUser();
row.setAuditBy(user != null ? user.getUsername() : null);
row.setAuditTime(LocalDateTime.now());
row.setAuditRemark(auditRemark);
if (!approved) {
row.setStatus("REJECTED");
updateById(row);
return R.ok(Boolean.TRUE, "已驳回");
}
SmHotelFxAgentEntity agent = agentService.getById(row.getAgentId());
if (agent == null || !"ACTIVE".equals(agent.getStatus())) {
return R.failed("申请人状态不可用");
}
if (!Integer.valueOf(2).equals(agent.getLevelNo())) {
return R.failed("申请人当前已非二级,无法任命");
}
// 一城一人:原一级降为二级并开保级窗口
List<SmHotelFxAgentEntity> cityL1 = agentService.list(Wrappers.<SmHotelFxAgentEntity>lambdaQuery()
.eq(SmHotelFxAgentEntity::getLevelNo, 1)
.eq(SmHotelFxAgentEntity::getStatus, "ACTIVE")
.eq(SmHotelFxAgentEntity::getCityAdcode, row.getCityAdcode())
.ne(SmHotelFxAgentEntity::getId, agent.getId()));
for (SmHotelFxAgentEntity old : cityL1) {
old.setLevelNo(2);
old.setDemoteWindowStart(LocalDateTime.now());
old.setDemoteWindowScore(BigDecimal.ZERO);
old.setRemark(StrUtil.maxLength(
StrUtil.blankToDefault(old.getRemark(), "") + ";被新城市合伙人替换降二级", 255));
agentService.updateById(old);
log.info("|酒店分销|一级替换降二级|oldAgentId={}|city={}|", old.getId(), row.getCityAdcode());
}
agent.setLevelNo(1);
agent.setCityAdcode(row.getCityAdcode());
agent.setDemoteWindowStart(null);
agent.setDemoteWindowScore(BigDecimal.ZERO);
agentService.updateById(agent);
row.setStatus("APPROVED");
updateById(row);
log.info("|酒店分销|任命一级|agentId={}|city={}|", agent.getId(), row.getCityAdcode());
return R.ok(Boolean.TRUE, "已任命为城市合伙人");
}
private BigDecimal level1PromoteMin() {
List<SmHotelFxLevelRuleDTO> rules = configService.parseLevelRules(configService.getOrCreateCurrent());
SmHotelFxLevelRuleDTO r1 = rules.stream().filter(r -> Objects.equals(r.getLevel(), 1)).findFirst().orElse(null);
return r1 != null && r1.getPromoteMin() != null ? r1.getPromoteMin() : new BigDecimal("500000");
}
private SmHotelFxAgentEntity requireActiveAgent() {
PjcloudUser user = SecurityUtils.getUser();
if (user == null || user.getId() == null) {
return null;
}
return agentService.getByUserId(user.getId());
}
private void fillAgent(List<SmHotelFxL1ApplyEntity> list) {
if (CollUtil.isEmpty(list)) {
return;
}
for (SmHotelFxL1ApplyEntity row : list) {
SmHotelFxAgentEntity a = agentService.getById(row.getAgentId());
if (a != null) {
row.setAgentName(StrUtil.blankToDefault(a.getRealName(), a.getUsername()));
row.setAgentLevel(a.getLevelNo());
}
}
}
}
@@ -0,0 +1,216 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxCommissionEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelFxWithdrawEntity;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxCommissionMapper;
import com.bsd.pjcloud.supermarket.mapper.SmHotelFxWithdrawMapper;
import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService;
import com.bsd.pjcloud.supermarket.service.SmHotelFxWithdrawService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class SmHotelFxWithdrawServiceImpl extends ServiceImpl<SmHotelFxWithdrawMapper, SmHotelFxWithdrawEntity>
implements SmHotelFxWithdrawService {
private static final String ST_LOCK = "LOCKED";
private static final String ST_WITHDRAWN = "WITHDRAWN";
private final SmHotelFxAgentService agentService;
private final SmHotelFxCommissionMapper commissionMapper;
@Override
public Page<SmHotelFxWithdrawEntity> pageAdmin(Page<SmHotelFxWithdrawEntity> page, SmHotelFxWithdrawEntity query) {
Page<SmHotelFxWithdrawEntity> result = page(page, Wrappers.<SmHotelFxWithdrawEntity>lambdaQuery()
.eq(query.getAgentId() != null, SmHotelFxWithdrawEntity::getAgentId, query.getAgentId())
.eq(StrUtil.isNotBlank(query.getStatus()), SmHotelFxWithdrawEntity::getStatus, query.getStatus())
.orderByDesc(SmHotelFxWithdrawEntity::getId));
fillAgent(result.getRecords());
return result;
}
@Override
public Page<SmHotelFxWithdrawEntity> pageMine(Page<SmHotelFxWithdrawEntity> page, String status) {
SmHotelFxAgentEntity me = currentAgent();
if (me == null) {
return page;
}
return page(page, Wrappers.<SmHotelFxWithdrawEntity>lambdaQuery()
.eq(SmHotelFxWithdrawEntity::getAgentId, me.getId())
.eq(StrUtil.isNotBlank(status), SmHotelFxWithdrawEntity::getStatus, status)
.orderByDesc(SmHotelFxWithdrawEntity::getId));
}
@Override
public R<Map<String, Object>> withdrawableMine() {
SmHotelFxAgentEntity me = currentAgent();
if (me == null) {
return R.failed("当前账号不是分销员");
}
List<SmHotelFxCommissionEntity> list = listWithdrawable(me.getId());
BigDecimal amount = BigDecimal.ZERO;
for (SmHotelFxCommissionEntity c : list) {
amount = amount.add(c.getCommissionAmount() == null ? BigDecimal.ZERO : c.getCommissionAmount());
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("agentId", me.getId());
data.put("withdrawableAmount", amount);
data.put("commissionCount", list.size());
return R.ok(data);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<SmHotelFxWithdrawEntity> applyMine(String accountName, String accountNo, String bankName, String remark) {
SmHotelFxAgentEntity me = currentAgent();
if (me == null || !"ACTIVE".equals(me.getStatus())) {
return R.failed("当前账号不是在职分销员");
}
if (StrUtil.isBlank(accountName) || StrUtil.isBlank(accountNo)) {
return R.failed("请填写收款户名与账号");
}
List<SmHotelFxCommissionEntity> list = listWithdrawable(me.getId());
if (CollUtil.isEmpty(list)) {
return R.failed("暂无可提现锁定佣金");
}
BigDecimal amount = BigDecimal.ZERO;
for (SmHotelFxCommissionEntity c : list) {
amount = amount.add(c.getCommissionAmount() == null ? BigDecimal.ZERO : c.getCommissionAmount());
}
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return R.failed("可提现金额为 0");
}
SmHotelFxWithdrawEntity wd = new SmHotelFxWithdrawEntity();
wd.setAgentId(me.getId());
wd.setUserId(me.getUserId());
wd.setAmount(amount);
wd.setCommissionCount(list.size());
wd.setAccountName(accountName.trim());
wd.setAccountNo(accountNo.trim());
wd.setBankName(bankName);
wd.setStatus("PENDING");
wd.setApplyRemark(remark);
save(wd);
for (SmHotelFxCommissionEntity c : list) {
c.setWithdrawId(wd.getId());
commissionMapper.updateById(c);
}
log.info("|酒店分销|提现申请|agentId={}|amount={}|lines={}|wdId={}|", me.getId(), amount, list.size(),
wd.getId());
return R.ok(wd);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> audit(Long id, boolean approved, String auditRemark) {
SmHotelFxWithdrawEntity wd = getById(id);
if (wd == null) {
return R.failed("提现单不存在");
}
if (!"PENDING".equals(wd.getStatus())) {
return R.failed("仅待审提现可审核");
}
PjcloudUser user = SecurityUtils.getUser();
wd.setAuditBy(user != null ? user.getUsername() : null);
wd.setAuditTime(LocalDateTime.now());
wd.setAuditRemark(auditRemark);
if (!approved) {
wd.setStatus("REJECTED");
updateById(wd);
releaseCommissions(wd.getId());
return R.ok(Boolean.TRUE, "已驳回,佣金已释放可再提");
}
wd.setStatus("APPROVED");
updateById(wd);
return R.ok(Boolean.TRUE, "已通过,待打款");
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> markPaid(Long id, String remark) {
SmHotelFxWithdrawEntity wd = getById(id);
if (wd == null) {
return R.failed("提现单不存在");
}
if (!"APPROVED".equals(wd.getStatus()) && !"PENDING".equals(wd.getStatus())) {
return R.failed("仅待审/已审通过的提现可标记打款");
}
PjcloudUser user = SecurityUtils.getUser();
if ("PENDING".equals(wd.getStatus())) {
wd.setAuditBy(user != null ? user.getUsername() : null);
wd.setAuditTime(LocalDateTime.now());
}
wd.setStatus("PAID");
wd.setPayTime(LocalDateTime.now());
if (StrUtil.isNotBlank(remark)) {
wd.setAuditRemark(StrUtil.blankToDefault(wd.getAuditRemark(), "") + ";" + remark);
}
updateById(wd);
List<SmHotelFxCommissionEntity> list = commissionMapper.selectList(Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getWithdrawId, id)
.eq(SmHotelFxCommissionEntity::getStatus, ST_LOCK));
for (SmHotelFxCommissionEntity c : list) {
c.setStatus(ST_WITHDRAWN);
c.setRemark("已打款提现");
commissionMapper.updateById(c);
}
return R.ok(Boolean.TRUE, "已标记打款");
}
private List<SmHotelFxCommissionEntity> listWithdrawable(Long agentId) {
return commissionMapper.selectList(Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getAgentId, agentId)
.eq(SmHotelFxCommissionEntity::getStatus, ST_LOCK)
.isNull(SmHotelFxCommissionEntity::getWithdrawId));
}
private void releaseCommissions(Long withdrawId) {
List<SmHotelFxCommissionEntity> list = commissionMapper.selectList(Wrappers.<SmHotelFxCommissionEntity>lambdaQuery()
.eq(SmHotelFxCommissionEntity::getWithdrawId, withdrawId));
for (SmHotelFxCommissionEntity c : list) {
c.setWithdrawId(null);
commissionMapper.updateById(c);
}
}
private SmHotelFxAgentEntity currentAgent() {
PjcloudUser user = SecurityUtils.getUser();
if (user == null || user.getId() == null) {
return null;
}
return agentService.getByUserId(user.getId());
}
private void fillAgent(List<SmHotelFxWithdrawEntity> list) {
if (CollUtil.isEmpty(list)) {
return;
}
for (SmHotelFxWithdrawEntity row : list) {
SmHotelFxAgentEntity a = agentService.getById(row.getAgentId());
if (a != null) {
row.setAgentName(StrUtil.blankToDefault(a.getRealName(), a.getUsername()));
}
}
}
}
@@ -96,9 +96,19 @@ spring:
# price-roll-day-offset: 90
# # 滚价按本地酒店 id 游标每批数量(防 OOM)
# price-roll-batch-size: 100
# # 全量房价房量:每天最多次数 / 最小间隔小时(「拉取全部启用详情」受此限制)
# # 全量房价房量:每天最多次数 / 最小间隔小时(全量详情 + 渠道范围详情共用)
# full-price-sync-max-per-day: 3
# full-price-sync-min-interval-hours: 6
# # 上游渠道「全量详情」定时(默认关;Cron 改后需重启;开关可库表热更)
# detail-pull-daily-enabled: false
# detail-pull-daily-cron: "0 0 4 * * ?"
# # 全部下游渠道范围合集「全量详情」定时(只拉范围内上游酒店,跳过本地;默认关)
# scope-detail-pull-daily-enabled: false
# scope-detail-pull-daily-cron: "0 0 5 * * ?"
# # 近窗详情:刷渠道范围合集未来 N 天(默认 15;可改 7);不占全量 6h/每日次数;默认开、每 2 小时
# near-detail-pull-enabled: true
# near-detail-pull-days: 15
# near-detail-pull-cron: "0 0 */2 * * ?"
# # 全量拉详情:每家间隔毫秒(并行时主要靠 price-api-qps,默认 0)
# detail-pull-interval-ms: 0
# # 全量拉详情酒店并发(吃满上游约 20 QPS;建议 4~8;多实例请调低)